Add search filter state syncing

This commit is contained in:
thePR0M3TH3AN committed 2026-01-23 12:07:55 -05:00
1 parent f8b4d8356d
commit 460ba22d51
5 files changed
+589 -61

No files matched your search

+42 -23
View File
@@ -18,10 +18,13 @@ import {
} from "./themeController.js";
import { setHashView } from "./hashView.js";
import { devLogger, userLogger } from "./utils/logger.js";
import { parseFilterQuery } from "./search/searchFilters.js";
import {
parseFilterQuery,
serializeFiltersToQuery,
} from "./search/searchFilters.js";
buildSearchHashFromState,
getSearchFilterState,
resetSearchFilters,
setSearchFilterState,
} from "./search/searchFilterState.js";
import {
prepareStaticModal,
openStaticModal,
@@ -49,6 +52,7 @@ import { nostrClient } from "./nostrClientFacade.js";
import { userBlocks } from "./userBlocks.js";
import { relayManager } from "./relayManager.js";
import createApplication from "./bootstrap.js";
import attachSearchFiltersPopover from "./ui/components/SearchFiltersPopover.js";
validateInstanceConfig();
@@ -738,16 +742,12 @@ async function bootstrapInterface() {
if (parsedFilters.errors.length > 0) {
devLogger.warn("[Search] Filter parsing errors", parsedFilters.errors);
}
const params = new URLSearchParams();
if (parsedFilters.text) {
params.set("q", parsedFilters.text);
}
const serializedFilters = serializeFiltersToQuery(parsedFilters.filters);
if (serializedFilters) {
params.set("filters", serializedFilters);
}
const queryString = params.toString();
setHashView(queryString ? `search&${queryString}` : "search");
const nextState = {
text: parsedFilters.text || "",
filters: parsedFilters.filters,
};
setSearchFilterState(nextState);
setHashView(buildSearchHashFromState(nextState));
});
}
@@ -848,22 +848,41 @@ async function bootstrapInterface() {
if (parsedFilters.errors.length > 0) {
devLogger.warn("[Search] Filter parsing errors", parsedFilters.errors);
}
const params = new URLSearchParams();
if (parsedFilters.text) {
params.set("q", parsedFilters.text);
}
const serializedFilters = serializeFiltersToQuery(parsedFilters.filters);
if (serializedFilters) {
params.set("filters", serializedFilters);
}
const queryString = params.toString();
setHashView(queryString ? `search&${queryString}` : "search");
const nextState = {
text: parsedFilters.text || "",
filters: parsedFilters.filters,
};
setSearchFilterState(nextState);
setHashView(buildSearchHashFromState(nextState));
// Close search on submit
mobileSearchContainer.classList.add("hidden");
mobileSearchFab.classList.remove("hidden");
});
}
const searchFilterButtons = document.querySelectorAll(".header-search__filter");
const applySearchFilters = (filters) => {
const currentState = getSearchFilterState();
const nextState = {
text: currentState.text || "",
filters,
};
setSearchFilterState(nextState);
setHashView(buildSearchHashFromState(nextState));
};
const clearSearchFilters = () => {
resetSearchFilters();
const nextState = getSearchFilterState();
setHashView(buildSearchHashFromState(nextState));
};
searchFilterButtons.forEach((button) => {
attachSearchFiltersPopover(button, {
getState: getSearchFilterState,
onApply: applySearchFilters,
onReset: clearSearchFilters,
});
});
const sidebar = document.getElementById("sidebar");
const collapseToggle = document.getElementById("sidebarCollapseToggle");
if (sidebar && !sidebar.hasAttribute("data-footer-state")) {
+112
View File
@@ -0,0 +1,112 @@
import {
DEFAULT_FILTERS,
parseFilterQuery,
serializeFiltersToQuery,
} from "./searchFilters.js";
const listeners = new Set();
const cloneFilters = (filters = DEFAULT_FILTERS) => ({
dateRange: {
after: filters?.dateRange?.after ?? null,
before: filters?.dateRange?.before ?? null,
},
sort: filters?.sort ?? DEFAULT_FILTERS.sort,
authorPubkeys: Array.isArray(filters?.authorPubkeys)
? [...filters.authorPubkeys]
: [],
tags: Array.isArray(filters?.tags) ? [...filters.tags] : [],
textScope: filters?.textScope ?? DEFAULT_FILTERS.textScope,
duration: {
minSeconds: filters?.duration?.minSeconds ?? null,
maxSeconds: filters?.duration?.maxSeconds ?? null,
},
hasMagnet:
typeof filters?.hasMagnet === "boolean" ? filters.hasMagnet : null,
hasUrl: typeof filters?.hasUrl === "boolean" ? filters.hasUrl : null,
nsfw: filters?.nsfw ?? DEFAULT_FILTERS.nsfw,
relay: filters?.relay ?? DEFAULT_FILTERS.relay,
kind:
Number.isFinite(filters?.kind) && filters.kind !== null
? filters.kind
: null,
});
let state = {
text: "",
filters: cloneFilters(DEFAULT_FILTERS),
};
function notify() {
const snapshot = getSearchFilterState();
listeners.forEach((listener) => listener(snapshot));
}
export function getSearchFilterState() {
return {
text: state.text,
filters: cloneFilters(state.filters),
};
}
export function setSearchFilterState(nextState = {}, options = {}) {
state = {
text: typeof nextState.text === "string" ? nextState.text : "",
filters: cloneFilters(nextState.filters),
};
if (options.notify !== false) {
notify();
}
}
export function resetSearchFilters(options = {}) {
state = {
text: state.text,
filters: cloneFilters(DEFAULT_FILTERS),
};
if (options.notify !== false) {
notify();
}
}
export function subscribeSearchFilterState(listener) {
if (typeof listener !== "function") {
return () => {};
}
listeners.add(listener);
return () => listeners.delete(listener);
}
export function syncSearchFilterStateFromHash(hash = "") {
const hashString =
typeof hash === "string" && hash ? hash : window?.location?.hash || "";
const hashParams = new URLSearchParams(
hashString.split("?")[1] || hashString.slice(1),
);
const rawQuery = hashParams.get("q") || "";
const rawFilters = hashParams.get("filters") || "";
const parsed = parseFilterQuery(
[rawQuery, rawFilters].filter(Boolean).join(" "),
);
setSearchFilterState(
{
text: parsed.text || "",
filters: parsed.filters,
},
{ notify: true },
);
return parsed;
}
export function buildSearchHashFromState(nextState = state) {
const params = new URLSearchParams();
if (nextState.text) {
params.set("q", nextState.text);
}
const serialized = serializeFiltersToQuery(nextState.filters);
if (serialized) {
params.set("filters", serialized);
}
const queryString = params.toString();
return queryString ? `search&${queryString}` : "search";
}
+220 -7
View File
@@ -8,7 +8,14 @@ import { formatShortNpub } from "./utils/formatters.js";
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
import { devLogger } from "./utils/logger.js";
import { attachFeedInfoPopover } from "./ui/components/FeedInfoPopover.js";
import { parseFilterQuery } from "./search/searchFilters.js";
import { setHashView } from "./hashView.js";
import {
buildSearchHashFromState,
getSearchFilterState,
resetSearchFilters,
setSearchFilterState,
syncSearchFilterStateFromHash,
} from "./search/searchFilterState.js";
function getApp() {
return getApplication();
@@ -101,17 +108,14 @@ function renderProfileCards(profiles, container) {
let currentSearchToken = 0;
export async function initSearchView() {
const hashParams = new URLSearchParams(window.location.hash.split("?")[1] || window.location.hash.slice(1));
// The hash format is #view=search&q=...&filters=...
// URLSearchParams handles 'view=search&q=...' correctly if we pass the string after #
const rawQuery = hashParams.get("q") || "";
const rawFilters = hashParams.get("filters") || "";
const parsedQuery = parseFilterQuery([rawQuery, rawFilters].filter(Boolean).join(" "));
const parsedQuery = syncSearchFilterStateFromHash(window.location.hash);
if (parsedQuery.errors.length > 0) {
devLogger.warn("[Search] Filter parsing errors", parsedQuery.errors);
}
const query = parsedQuery.text || "";
renderActiveFilters(parsedQuery.filters);
const titleEl = document.getElementById("searchTitle");
if (titleEl) {
titleEl.textContent = query ? `Search Results for "${query}"` : "Search Results";
@@ -167,6 +171,215 @@ export async function initSearchView() {
});
}
const formatDateLabel = (timestampSeconds) => {
if (!Number.isFinite(timestampSeconds)) return "";
const formatter = new Intl.DateTimeFormat("en-CA", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
return formatter.format(new Date(timestampSeconds * 1000));
};
const formatDurationLabel = (seconds) => {
if (!Number.isFinite(seconds)) return "";
if (seconds % 3600 === 0) {
return `${seconds / 3600}h`;
}
if (seconds % 60 === 0) {
return `${seconds / 60}m`;
}
return `${seconds}s`;
};
function buildNextFilters() {
const current = getSearchFilterState();
const filters = current.filters || {};
return {
...filters,
dateRange: { ...filters.dateRange },
duration: { ...filters.duration },
authorPubkeys: Array.isArray(filters.authorPubkeys)
? [...filters.authorPubkeys]
: [],
tags: Array.isArray(filters.tags) ? [...filters.tags] : [],
};
}
function applyFiltersAndRefresh(nextFilters) {
const current = getSearchFilterState();
const nextState = {
text: current.text || "",
filters: nextFilters,
};
setSearchFilterState(nextState);
setHashView(buildSearchHashFromState(nextState));
}
function renderActiveFilters(filters) {
const wrapper = document.getElementById("searchActiveFilters");
const list = document.getElementById("searchActiveFiltersList");
const clearBtn = document.getElementById("searchActiveFiltersClear");
if (!wrapper || !list || !clearBtn) {
return;
}
list.innerHTML = "";
const pills = [];
filters.authorPubkeys?.forEach((author) => {
pills.push({
label: `Author: ${author}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.authorPubkeys = nextFilters.authorPubkeys.filter(
(value) => value !== author,
);
applyFiltersAndRefresh(nextFilters);
},
});
});
filters.tags?.forEach((tag) => {
pills.push({
label: `#${tag}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.tags = nextFilters.tags.filter((value) => value !== tag);
applyFiltersAndRefresh(nextFilters);
},
});
});
if (Number.isFinite(filters.kind)) {
pills.push({
label: `Kind: ${filters.kind}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.kind = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (filters.relay) {
pills.push({
label: `Relay: ${filters.relay}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.relay = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (Number.isFinite(filters.dateRange?.after)) {
pills.push({
label: `After: ${formatDateLabel(filters.dateRange.after)}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.dateRange.after = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (Number.isFinite(filters.dateRange?.before)) {
pills.push({
label: `Before: ${formatDateLabel(filters.dateRange.before)}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.dateRange.before = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (Number.isFinite(filters.duration?.minSeconds)) {
pills.push({
label: `Min duration: ${formatDurationLabel(filters.duration.minSeconds)}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.duration.minSeconds = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (Number.isFinite(filters.duration?.maxSeconds)) {
pills.push({
label: `Max duration: ${formatDurationLabel(filters.duration.maxSeconds)}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.duration.maxSeconds = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (filters.hasMagnet === true) {
pills.push({
label: "Has magnet",
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.hasMagnet = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (filters.hasUrl === true) {
pills.push({
label: "Has URL",
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.hasUrl = null;
applyFiltersAndRefresh(nextFilters);
},
});
}
if (filters.nsfw && filters.nsfw !== "any") {
pills.push({
label: `NSFW: ${filters.nsfw}`,
onRemove: () => {
const nextFilters = buildNextFilters();
nextFilters.nsfw = "any";
applyFiltersAndRefresh(nextFilters);
},
});
}
if (!pills.length) {
wrapper.classList.add("hidden");
return;
}
const fragment = document.createDocumentFragment();
pills.forEach((pill) => {
const button = document.createElement("button");
button.type = "button";
button.className =
"search-filter-chip inline-flex items-center gap-2 text-xs";
const label = document.createElement("span");
label.textContent = pill.label;
const remove = document.createElement("span");
remove.setAttribute("aria-hidden", "true");
remove.textContent = "✕";
button.append(label, remove);
button.addEventListener("click", pill.onRemove);
fragment.appendChild(button);
});
list.appendChild(fragment);
wrapper.classList.remove("hidden");
clearBtn.onclick = () => {
resetSearchFilters();
const nextState = getSearchFilterState();
setHashView(buildSearchHashFromState(nextState));
};
}
async function renderSearchVideos(videos, container, app) {
if (!container) return;
container.innerHTML = "";
+201 -31
View File
@@ -1,4 +1,5 @@
import createPopover from "../overlay/popoverEngine.js";
import { DEFAULT_FILTERS } from "../../search/searchFilters.js";
const FOCUSABLE_SELECTOR =
"a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
@@ -115,6 +116,32 @@ function buildLabeledInput(doc, { id, label, type = "text", placeholder } = {})
return { wrapper, inputEl };
}
const parseDateInputValue = (value) => {
if (!value) return null;
const parsed = Date.parse(value);
if (Number.isNaN(parsed)) {
return null;
}
return Math.floor(parsed / 1000);
};
const formatDateInputValue = (timestampSeconds) => {
if (!Number.isFinite(timestampSeconds)) {
return "";
}
const date = new Date(timestampSeconds * 1000);
const year = date.getFullYear();
const month = `${date.getMonth() + 1}`.padStart(2, "0");
const day = `${date.getDate()}`.padStart(2, "0");
return `${year}-${month}-${day}`;
};
const normalizeTagValue = (value) =>
value
.split(/[, ]+/)
.map((tag) => tag.trim().replace(/^#/, ""))
.filter(Boolean);
export function attachSearchFiltersPopover(triggerElement, options = {}) {
if (!triggerElement) {
return null;
@@ -125,6 +152,8 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
return null;
}
let controlState = null;
const render = ({ container, close }) => {
const panel = createElement(doc, "div", {
className:
@@ -170,7 +199,15 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
const chipRow = createElement(doc, "div", {
className: "flex flex-wrap gap-2",
});
["24h", "7d", "30d", "90d", "All time"].forEach((label) => {
const dateChips = [];
const dateChipValues = [
{ label: "24h", days: 1 },
{ label: "7d", days: 7 },
{ label: "30d", days: 30 },
{ label: "90d", days: 90 },
{ label: "All time", days: null },
];
dateChipValues.forEach(({ label, days }) => {
const chip = createElement(doc, "button", {
className:
"search-filter-chip focus-ring",
@@ -178,22 +215,50 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
attrs: { type: "button", "aria-pressed": "false" },
});
chip.dataset.state = "off";
chip.addEventListener("click", () => toggleChip(chip));
chip.addEventListener("click", () => {
dateChips.forEach((entry) => {
entry.button.setAttribute("aria-pressed", "false");
entry.button.dataset.state = "off";
});
chip.setAttribute("aria-pressed", "true");
chip.dataset.state = "on";
if (days === null) {
startInputEl.value = "";
endInputEl.value = "";
return;
}
const now = new Date();
const start = new Date();
start.setDate(now.getDate() - days);
startInputEl.value = formatDateInputValue(
Math.floor(start.getTime() / 1000),
);
endInputEl.value = formatDateInputValue(
Math.floor(now.getTime() / 1000),
);
});
dateChips.push({ button: chip, days });
chipRow.appendChild(chip);
});
const dateInputs = createElement(doc, "div", {
className: "grid gap-3 sm:grid-cols-2",
});
const { wrapper: startWrapper } = buildLabeledInput(doc, {
const { wrapper: startWrapper, inputEl: startInputEl } = buildLabeledInput(
doc,
{
id: "search-filters-date-start",
label: "Start date",
type: "date",
});
const { wrapper: endWrapper } = buildLabeledInput(doc, {
},
);
const { wrapper: endWrapper, inputEl: endInputEl } = buildLabeledInput(
doc,
{
id: "search-filters-date-end",
label: "End date",
type: "date",
});
},
);
dateInputs.append(startWrapper, endWrapper);
dateSection.append(chipRow, dateInputs);
panel.appendChild(dateSection);
@@ -224,11 +289,14 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
sortSection.appendChild(sortSelect);
panel.appendChild(sortSection);
const { wrapper: authorWrapper } = buildLabeledInput(doc, {
const { wrapper: authorWrapper, inputEl: authorInput } = buildLabeledInput(
doc,
{
id: "search-filters-author",
label: "Author",
placeholder: "npub1...",
});
},
);
panel.appendChild(authorWrapper);
const tagSection = createElement(doc, "div", {
@@ -247,6 +315,7 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
const tagList = createElement(doc, "div", {
className: "flex flex-wrap gap-2",
});
const tagChips = new Map();
["nostr", "music", "live"].forEach((tag) => {
const chip = createElement(doc, "button", {
className: "search-filter-chip focus-ring",
@@ -255,6 +324,7 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
});
chip.dataset.state = "on";
chip.addEventListener("click", () => toggleChip(chip));
tagChips.set(tag, chip);
tagList.appendChild(chip);
});
tagSection.append(tagInput, tagList);
@@ -272,6 +342,7 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
const durationGrid = createElement(doc, "div", {
className: "grid gap-2",
});
const durationChecks = {};
[
{ id: "duration-short", label: "Short (under 5 min)" },
{ id: "duration-medium", label: "Medium (5-20 min)" },
@@ -286,6 +357,7 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
className: "checkbox",
attrs: { id, type: "checkbox" },
});
durationChecks[id] = checkbox;
row.append(checkbox, createElement(doc, "span", { text: label }));
durationGrid.appendChild(row);
});
@@ -320,10 +392,9 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
row.append(text, toggle);
return row;
};
toggleSection.append(
toggleRow("NSFW content", "search-filters-nsfw"),
toggleRow("Followed only", "search-filters-followed"),
);
const nsfwToggle = toggleRow("NSFW content", "search-filters-nsfw");
const followedToggle = toggleRow("Followed only", "search-filters-followed");
toggleSection.append(nsfwToggle, followedToggle);
panel.appendChild(toggleSection);
const footer = createElement(doc, "div", {
@@ -339,33 +410,118 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
text: "Apply filters",
attrs: { type: "button" },
});
resetButton.addEventListener("click", () => {
const chips = panel.querySelectorAll(".search-filter-chip");
chips.forEach((chip) => {
chip.setAttribute("aria-pressed", "false");
chip.dataset.state = "off";
const applyStateToControls = (filters = DEFAULT_FILTERS) => {
const safeFilters = filters || DEFAULT_FILTERS;
startInputEl.value = formatDateInputValue(safeFilters.dateRange?.after);
endInputEl.value = formatDateInputValue(safeFilters.dateRange?.before);
const hasDateRange =
Number.isFinite(safeFilters.dateRange?.after) ||
Number.isFinite(safeFilters.dateRange?.before);
dateChips.forEach(({ button, days }) => {
const isAllTime = days === null && !hasDateRange;
button.setAttribute("aria-pressed", isAllTime ? "true" : "false");
button.dataset.state = isAllTime ? "on" : "off";
});
panel.querySelectorAll("input").forEach((input) => {
if (input.type === "checkbox") {
input.checked = false;
} else {
input.value = "";
authorInput.value = safeFilters.authorPubkeys?.join(", ") || "";
tagInput.value = "";
tagChips.forEach((chip, tag) => {
const isActive = safeFilters.tags?.includes(tag);
chip.setAttribute("aria-pressed", isActive ? "true" : "false");
chip.dataset.state = isActive ? "on" : "off";
});
const extraTags = (safeFilters.tags || []).filter(
(tag) => !tagChips.has(tag),
);
if (extraTags.length) {
tagInput.value = extraTags.join(", ");
}
Object.values(durationChecks).forEach((checkbox) => {
checkbox.checked = false;
});
const minSeconds = safeFilters.duration?.minSeconds ?? null;
const maxSeconds = safeFilters.duration?.maxSeconds ?? null;
if (minSeconds === null && maxSeconds === 300) {
durationChecks["duration-short"].checked = true;
} else if (minSeconds === 300 && maxSeconds === 1200) {
durationChecks["duration-medium"].checked = true;
} else if (minSeconds === 1200 && maxSeconds === null) {
durationChecks["duration-long"].checked = true;
}
const nsfwSwitch = nsfwToggle.querySelector(".switch");
if (nsfwSwitch) {
const isOn = safeFilters.nsfw === "true" || safeFilters.nsfw === "only";
nsfwSwitch.setAttribute("aria-checked", isOn ? "true" : "false");
nsfwSwitch.classList.toggle("is-on", isOn);
}
const followedSwitch = followedToggle.querySelector(".switch");
if (followedSwitch) {
followedSwitch.setAttribute("aria-checked", "false");
followedSwitch.classList.remove("is-on");
}
if (sortSelect) {
sortSelect.selectedIndex = 0;
}
};
const buildFiltersFromControls = () => {
const filters = {
...DEFAULT_FILTERS,
dateRange: { ...DEFAULT_FILTERS.dateRange },
duration: { ...DEFAULT_FILTERS.duration },
};
filters.dateRange.after = parseDateInputValue(startInputEl.value);
filters.dateRange.before = parseDateInputValue(endInputEl.value);
const authorValues = authorInput.value
.split(/[, ]+/)
.map((value) => value.trim())
.filter(Boolean);
filters.authorPubkeys = authorValues;
const selectedTags = new Set();
tagChips.forEach((chip, tag) => {
if (chip.getAttribute("aria-pressed") === "true") {
selectedTags.add(tag);
}
});
panel.querySelectorAll("select").forEach((select) => {
select.selectedIndex = 0;
});
panel.querySelectorAll(".switch").forEach((toggle) => {
toggle.setAttribute("aria-checked", "false");
toggle.classList.remove("is-on");
});
normalizeTagValue(tagInput.value).forEach((tag) =>
selectedTags.add(tag),
);
filters.tags = Array.from(selectedTags);
const ranges = [];
if (durationChecks["duration-short"].checked) {
ranges.push({ min: null, max: 300 });
}
if (durationChecks["duration-medium"].checked) {
ranges.push({ min: 300, max: 1200 });
}
if (durationChecks["duration-long"].checked) {
ranges.push({ min: 1200, max: null });
}
if (ranges.length) {
const mins = ranges
.map((range) => range.min)
.filter((value) => value !== null);
const maxes = ranges
.map((range) => range.max)
.filter((value) => value !== null);
filters.duration.minSeconds = mins.length ? Math.min(...mins) : null;
filters.duration.maxSeconds = maxes.length ? Math.max(...maxes) : null;
}
const nsfwSwitch = nsfwToggle.querySelector(".switch");
if (nsfwSwitch?.getAttribute("aria-checked") === "true") {
filters.nsfw = "true";
}
return filters;
};
resetButton.addEventListener("click", () => {
applyStateToControls(DEFAULT_FILTERS);
if (typeof options.onReset === "function") {
options.onReset();
}
});
applyButton.addEventListener("click", () => {
if (typeof options.onApply === "function") {
options.onApply();
options.onApply(buildFiltersFromControls());
}
close();
});
@@ -374,6 +530,10 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
trapFocus(panel);
container.appendChild(panel);
controlState = {
applyStateToControls,
};
return panel;
};
@@ -385,7 +545,17 @@ export function attachSearchFiltersPopover(triggerElement, options = {}) {
triggerElement.addEventListener("click", async (event) => {
event.preventDefault();
await popover.toggle();
if (popover.isOpen()) {
popover.close();
return;
}
popover.preload();
if (controlState?.applyStateToControls) {
const nextState =
typeof options.getState === "function" ? options.getState() : null;
controlState.applyStateToControls(nextState?.filters || DEFAULT_FILTERS);
}
await popover.open();
});
return popover;
+14
View File
@@ -30,6 +30,20 @@
</button>
</header>
<div
id="searchActiveFilters"
class="hidden flex flex-wrap items-center gap-2 rounded-xl border border-border bg-surface/70 px-3 py-2 text-xs text-text"
>
<div id="searchActiveFiltersList" class="flex flex-wrap gap-2"></div>
<button
id="searchActiveFiltersClear"
type="button"
class="btn-ghost text-xs"
>
Clear all
</button>
</div>
<section class="bv-stack">
<h2 class="text-lg font-semibold uppercase tracking-wide text-muted-strong">
Channels