{
return this._items;
}
// set items(v: { [id: string]: SearchableItem }) {
// this._items = v;
// }
// The order of -all- elements (even those hidden), by their identifier.
get ordering(): string[] {
return this._ordering;
}
// Specifically dis-allow setting ordering itself, so that setOrdering is used instead (for the field and ascending params).
// set ordering(v: string[]) { this._ordering = v; }
setOrdering = (v: string[], field: string, ascending: boolean) => {
this._ordering = v;
this._sortField = field;
this._ascending = ascending;
};
get sortField(): string {
return this._sortField;
}
get ascending(): boolean {
return this._ascending;
}
// FIXME: This is being called by navigate, and triggering a "load more" when we haven't loaded at all, and loading without a searchc when we have one!
onSearchBoxChange = (
newItems: boolean = false,
appendedItems: boolean = false,
loadAll: boolean = false,
callback?: (resp: PaginatedDTO) => void,
) => {
const query = this._c.search.value;
if (!query) {
this.inSearch = false;
} else {
this.inSearch = true;
}
const results = this.search(query);
this._c.setVisibility(results, true, appendedItems);
this._c.onSearchCallback(newItems, loadAll, callback);
if (this.inSearch) {
if (this.inServerSearch) {
this._serverSearchButtons.forEach((v: HTMLElement) => {
v.classList.add("@low");
v.classList.remove("@high");
});
} else {
this._serverSearchButtons.forEach((v: HTMLElement) => {
v.classList.add("@high");
v.classList.remove("@low");
});
}
}
this.showHideSearchOptionsHeader();
this.setNotFoundPanelVisibility(results.length == 0);
if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0);
};
setNotFoundPanelVisibility = (visible: boolean) => {
if (this._inServerSearch || !this.inSearch) {
this._c.notFoundLocallyText?.classList.add("unfocused");
} else if (this.inSearch) {
this._c.notFoundLocallyText?.classList.remove("unfocused");
}
if (visible) {
this._c.notFoundPanel?.classList.remove("unfocused");
} else {
this._c.notFoundPanel?.classList.add("unfocused");
}
};
fillInFilter = (name: string, value: string, offset?: number) => {
this._c.search.value = name + ":" + value + " " + this._c.search.value;
this._c.search.focus();
let newPos = name.length + 1 + value.length;
if (typeof offset !== "undefined") newPos += offset;
this._c.search.setSelectionRange(newPos, newPos);
this._c.search.oninput(null as any);
};
generateFilterList = () => {
if (!this._c.filterList) return;
const filterListContainer = document.createElement("div");
filterListContainer.classList.add("flex", "flex-row", "flex-wrap", "gap-2");
// Generate filter buttons
for (let queryName of Object.keys(this._c.queries)) {
const query = this._c.queries[queryName];
if ("show" in query && !query.show) continue;
if ("dependsOnElement" in query && query.dependsOnElement) {
const el = document.querySelector(query.dependsOnElement);
if (el === null) continue;
}
const container = document.createElement("span") as HTMLSpanElement;
container.classList.add(
"button",
"button-xl",
"~neutral",
"@low",
"align-bottom",
"flex",
"flex-row",
"gap-2",
);
container.innerHTML = `
${query.name}
${query.description || ""}
`;
if (query.bool) {
const pos = document.createElement("button") as HTMLButtonElement;
pos.type = "button";
pos.ariaLabel = `Filter by "${query.name}": True`;
pos.classList.add("button", "~positive");
pos.innerHTML = ``;
pos.addEventListener("click", () => this.fillInFilter(queryName, "true"));
const neg = document.createElement("button") as HTMLButtonElement;
neg.type = "button";
neg.ariaLabel = `Filter by "${query.name}": False`;
neg.classList.add("button", "~critical");
neg.innerHTML = ``;
neg.addEventListener("click", () => this.fillInFilter(queryName, "false"));
container.appendChild(pos);
container.appendChild(neg);
}
if (query.string) {
const button = document.createElement("button") as HTMLButtonElement;
button.type = "button";
button.classList.add("button", "~urge", "flex", "flex-row", "gap-2");
button.innerHTML = `${window.lang.strings("matchText")}`;
// Position cursor between quotes
button.addEventListener("click", () => this.fillInFilter(queryName, `""`, -1));
container.appendChild(button);
}
if (query.date) {
const onDate = document.createElement("button") as HTMLButtonElement;
onDate.type = "button";
onDate.classList.add("button", "~urge", "flex", "flex-row", "gap-2");
onDate.innerHTML = `On Date`;
onDate.addEventListener("click", () => this.fillInFilter(queryName, `"="`, -1));
const beforeDate = document.createElement("button") as HTMLButtonElement;
beforeDate.type = "button";
beforeDate.classList.add("button", "~urge", "flex", "flex-row", "gap-2");
beforeDate.innerHTML = `Before Date`;
beforeDate.addEventListener("click", () => this.fillInFilter(queryName, `"<"`, -1));
const afterDate = document.createElement("button") as HTMLButtonElement;
afterDate.type = "button";
afterDate.classList.add("button", "~urge", "flex", "flex-row", "gap-2");
afterDate.innerHTML = `After Date`;
afterDate.addEventListener("click", () => this.fillInFilter(queryName, `">"`, -1));
container.appendChild(onDate);
container.appendChild(beforeDate);
container.appendChild(afterDate);
}
filterListContainer.appendChild(container);
}
this._c.filterList.appendChild(filterListContainer);
};
onServerSearch = (then?: () => void) => {
const newServerSearch = !this.inServerSearch;
this.inServerSearch = true;
// this.setQueryParam();
this.searchServer(newServerSearch, then);
};
searchServer = (newServerSearch: boolean, then?: () => void) => {
this._c.searchServer(this.serverSearchParams(this._searchTerms, this._queries), newServerSearch, then);
};
serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => {
let req: ServerSearchReqDTO = {
searchTerms: searchTerms,
queries: [], // queries.map((q: Query) => q.asDTO()) won't work as localOnly queries return null
limit: -1,
page: 0,
sortByField: this.sortField,
ascending: this.ascending,
};
for (const q of queries) {
const dto = q.asDTO();
if (dto !== null) req.queries.push(dto);
}
return req;
};
private _qps: URLSearchParams = new URLSearchParams();
private _clearWithoutNavigate = false;
// clearQueryParam removes the "search" query parameter --without-- triggering a navigate call.
clearQueryParam = () => {
if (!this._qps.has("search")) return;
this._clearWithoutNavigate = true;
this.setQueryParam("");
};
// setQueryParam sets the ?search query param to the current searchbox content,
// or value if given. If everything is set up correctly, this should trigger a search when it is
// set to a new value.
setQueryParam = (value?: string) => {
let triggerManually = false;
if (value === undefined || value == null) value = this._c.search.value;
const url = new URL(window.location.href);
// FIXME: do better and make someone else clear this
if (value.trim()) {
url.searchParams.delete("user");
url.searchParams.set("search", value);
} else {
// If the query param is already blank, no change will mean no call to navigate()
triggerManually = !url.searchParams.has("search");
url.searchParams.delete("search");
}
console.log("pushing", url.toString());
window.history.pushState(null, "", url.toString());
if (triggerManually) this.navigate();
};
setServerSearchButtonsDisabled = (disabled: boolean) => {
this._serverSearchButtons.forEach((v: HTMLButtonElement) => (v.disabled = disabled));
};
isURL = (url?: string) => {
const urlParams = new URLSearchParams(url || window.location.search);
const searchContent = urlParams.get("search");
return Boolean(searchContent);
};
// navigate pulls the current "search" query param, puts it in the search box and searches it.
navigate = (url?: string, then?: () => void) => {
this._qps = new URLSearchParams(url || window.location.search);
if (this._clearWithoutNavigate) {
this._clearWithoutNavigate = false;
return;
}
const searchContent = this._qps.get("search") || "";
this._c.search.value = searchContent;
this.onSearchBoxChange();
this.onServerSearch(then);
};
constructor(c: SearchConfiguration) {
this._c = c;
if (!this._c.search) {
// Make a dummy one
this._c.search = document.createElement("input") as HTMLInputElement;
}
this._c.search.oninput = () => {
this.inServerSearch = false;
this.clearQueryParam();
this.onSearchBoxChange();
};
this._c.search.addEventListener("keyup", (ev: KeyboardEvent) => {
if (ev.key == "Enter") {
this.setQueryParam();
}
});
if (this._c.clearSearchButtonSelector) {
const clearSearchButtons = Array.from(
document.querySelectorAll(this._c.clearSearchButtonSelector),
) as Array;
for (let b of clearSearchButtons) {
b.addEventListener("click", () => {
this.inServerSearch = false;
this.setQueryParam("");
});
}
}
this._serverSearchButtons = this._c.serverSearchButtonSelector
? (Array.from(document.querySelectorAll(this._c.serverSearchButtonSelector)) as Array)
: [];
for (let b of this._serverSearchButtons) {
b.addEventListener("click", () => {
this.setQueryParam();
});
}
}
}