mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-03-18 21:50:33 +01:00
Clicking once will select all visible records, and show as indeterminate. Clicking again will load all records, and select them all once done.
712 lines
26 KiB
TypeScript
712 lines
26 KiB
TypeScript
import { ListItem } from "./list";
|
|
import { parseDateString } from "./common";
|
|
|
|
declare var window: GlobalWindow;
|
|
|
|
export enum QueryOperator {
|
|
Greater = ">",
|
|
Lower = "<",
|
|
Equal = "="
|
|
}
|
|
|
|
export function QueryOperatorToDateText(op: QueryOperator): string {
|
|
switch (op) {
|
|
case QueryOperator.Greater:
|
|
return window.lang.strings("after");
|
|
case QueryOperator.Lower:
|
|
return window.lang.strings("before");
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
export interface QueryType {
|
|
name: string;
|
|
description?: string;
|
|
getter: string;
|
|
bool: boolean;
|
|
string: boolean;
|
|
date: boolean;
|
|
dependsOnElement?: string; // Format for querySelector
|
|
show?: boolean;
|
|
localOnly?: boolean // Indicates can't be performed server-side.
|
|
}
|
|
|
|
export interface SearchConfiguration {
|
|
filterArea: HTMLElement;
|
|
sortingByButton?: HTMLButtonElement;
|
|
searchOptionsHeader: HTMLElement;
|
|
notFoundPanel: HTMLElement;
|
|
notFoundLocallyText: HTMLElement;
|
|
notFoundCallback?: (notFound: boolean) => void;
|
|
filterList: HTMLElement;
|
|
clearSearchButtonSelector: string;
|
|
serverSearchButtonSelector: string;
|
|
search: HTMLInputElement;
|
|
queries: { [field: string]: QueryType };
|
|
setVisibility: (items: string[], visible: boolean, appendedItems: boolean) => void;
|
|
onSearchCallback: (newItems: boolean, loadAll: boolean, callback?: (resp: paginatedDTO) => void) => void;
|
|
searchServer: (params: PaginatedReqDTO, newSearch: boolean) => void;
|
|
clearServerSearch: () => void;
|
|
loadMore?: () => void;
|
|
}
|
|
|
|
export interface ServerSearchReqDTO extends PaginatedReqDTO {
|
|
searchTerms: string[];
|
|
queries: QueryDTO[];
|
|
}
|
|
|
|
export interface QueryDTO {
|
|
class: "bool" | "string" | "date";
|
|
// QueryType.getter
|
|
field: string;
|
|
operator: QueryOperator;
|
|
value: boolean | string | DateAttempt;
|
|
};
|
|
|
|
export abstract class Query {
|
|
protected _subject: QueryType;
|
|
protected _operator: QueryOperator;
|
|
protected _card: HTMLElement;
|
|
public type: string;
|
|
|
|
constructor(subject: QueryType | null, operator: QueryOperator) {
|
|
this._subject = subject;
|
|
this._operator = operator;
|
|
if (subject != null) {
|
|
this._card = document.createElement("span");
|
|
this._card.ariaLabel = window.lang.strings("clickToRemoveFilter");
|
|
}
|
|
}
|
|
|
|
set onclick(v: () => void) {
|
|
this._card.addEventListener("click", v);
|
|
}
|
|
|
|
asElement(): HTMLElement { return this._card; }
|
|
|
|
public abstract compare(subjectValue: any): boolean;
|
|
|
|
asDTO(): QueryDTO | null {
|
|
if (this.localOnly) return null;
|
|
let out = {} as QueryDTO;
|
|
out.field = this._subject.getter;
|
|
out.operator = this._operator;
|
|
return out;
|
|
}
|
|
|
|
get subject(): QueryType { return this._subject; }
|
|
|
|
getValueFromItem(item: SearchableItem): any {
|
|
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(item), this.subject.getter).get.call(item);
|
|
}
|
|
|
|
compareItem(item: SearchableItem): boolean {
|
|
return this.compare(this.getValueFromItem(item));
|
|
}
|
|
|
|
get localOnly(): boolean { return this._subject.localOnly ? true : false; }
|
|
}
|
|
|
|
export class BoolQuery extends Query {
|
|
protected _value: boolean;
|
|
constructor(subject: QueryType, value: boolean) {
|
|
super(subject, QueryOperator.Equal);
|
|
this.type = "bool";
|
|
this._value = value;
|
|
this._card.classList.add("button", "~" + (this._value ? "positive" : "critical"), "@high", "center");
|
|
this._card.innerHTML = `
|
|
<span class="font-bold mr-2">${subject.name}</span>
|
|
<i class="text-2xl ri-${this._value? "checkbox" : "close"}-circle-fill"></i>
|
|
`;
|
|
}
|
|
|
|
public static paramsFromString(valueString: string): [boolean, boolean] {
|
|
let isBool = false;
|
|
let boolState = false;
|
|
if (valueString == "true" || valueString == "yes" || valueString == "t" || valueString == "y") {
|
|
isBool = true;
|
|
boolState = true;
|
|
} else if (valueString == "false" || valueString == "no" || valueString == "f" || valueString == "n") {
|
|
isBool = true;
|
|
boolState = false;
|
|
}
|
|
return [boolState, isBool]
|
|
}
|
|
|
|
get value(): boolean { return this._value; }
|
|
|
|
// Ripped from old code. Why it's like this, I don't know
|
|
public compare(subjectBool: boolean): boolean {
|
|
return ((subjectBool && this._value) || (!subjectBool && !this._value))
|
|
}
|
|
|
|
asDTO(): QueryDTO | null {
|
|
let out = super.asDTO();
|
|
if (out === null) return null;
|
|
out.class = "bool";
|
|
out.value = this._value;
|
|
return out;
|
|
}
|
|
}
|
|
|
|
export class StringQuery extends Query {
|
|
protected _value: string;
|
|
constructor(subject: QueryType, value: string) {
|
|
super(subject, QueryOperator.Equal);
|
|
this.type = "string";
|
|
this._value = value.toLowerCase();
|
|
this._card.classList.add("button", "~neutral", "@low", "center");
|
|
this._card.innerHTML = `
|
|
<span class="font-bold mr-2">${subject.name}:</span> "${this._value}"
|
|
`;
|
|
}
|
|
|
|
get value(): string { return this._value; }
|
|
|
|
public compare(subjectString: string): boolean {
|
|
return subjectString.toLowerCase().includes(this._value);
|
|
}
|
|
|
|
asDTO(): QueryDTO | null {
|
|
let out = super.asDTO();
|
|
if (out === null) return null;
|
|
out.class = "string";
|
|
out.value = this._value;
|
|
return out;
|
|
}
|
|
}
|
|
|
|
const dateGetters: Map<string, () => number> = (() => {
|
|
let m = new Map<string, () => number>();
|
|
m.set("year", Date.prototype.getFullYear);
|
|
m.set("month", Date.prototype.getMonth);
|
|
m.set("day", Date.prototype.getDate);
|
|
m.set("hour", Date.prototype.getHours);
|
|
m.set("minute", Date.prototype.getMinutes);
|
|
return m;
|
|
})();
|
|
const dateSetters: Map<string, (v: number) => void> = (() => {
|
|
let m = new Map<string, (v: number) => void>();
|
|
m.set("year", Date.prototype.setFullYear);
|
|
m.set("month", Date.prototype.setMonth);
|
|
m.set("day", Date.prototype.setDate);
|
|
m.set("hour", Date.prototype.setHours);
|
|
m.set("minute", Date.prototype.setMinutes);
|
|
return m;
|
|
})();
|
|
|
|
export class DateQuery extends Query {
|
|
protected _value: ParsedDate;
|
|
|
|
constructor(subject: QueryType, operator: QueryOperator, value: ParsedDate) {
|
|
super(subject, operator);
|
|
this.type = "date";
|
|
this._value = value;
|
|
this._card.classList.add("button", "~neutral", "@low", "center");
|
|
let dateText = QueryOperatorToDateText(operator);
|
|
this._card.innerHTML = `
|
|
<span class="font-bold mr-2">${subject.name}:</span> ${dateText != "" ? dateText+" " : ""}${value.text}
|
|
`;
|
|
}
|
|
|
|
public static paramsFromString(valueString: string): [ParsedDate, QueryOperator, boolean] {
|
|
let op = QueryOperator.Equal;
|
|
if ((Object.values(QueryOperator) as string[]).includes(valueString.charAt(0))) {
|
|
op = valueString.charAt(0) as QueryOperator;
|
|
// Trim the operator from the string
|
|
valueString = valueString.substring(1);
|
|
}
|
|
let out = parseDateString(valueString);
|
|
let isValid = true;
|
|
if (out.invalid) isValid = false;
|
|
|
|
return [out, op, isValid];
|
|
}
|
|
|
|
get value(): ParsedDate { return this._value; }
|
|
|
|
public compare(subjectDate: Date): boolean {
|
|
// We want to compare only the fields given in this._value,
|
|
// so we copy subjectDate and apply on those fields from this._value.
|
|
const temp = new Date(subjectDate.valueOf());
|
|
for (let [field] of dateGetters) {
|
|
if (field in this._value.attempt) {
|
|
dateSetters.get(field).call(
|
|
temp,
|
|
dateGetters.get(field).call(this._value.date)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (this._operator == QueryOperator.Equal) {
|
|
return subjectDate.getTime() == temp.getTime();
|
|
} else if (this._operator == QueryOperator.Lower) {
|
|
return subjectDate < temp;
|
|
}
|
|
return subjectDate > temp;
|
|
}
|
|
|
|
asDTO(): QueryDTO | null {
|
|
let out = super.asDTO();
|
|
if (out === null) return null;
|
|
out.class = "date";
|
|
out.value = this._value.attempt;
|
|
return out;
|
|
}
|
|
}
|
|
|
|
export interface SearchableItem extends ListItem {
|
|
matchesSearch: (query: string) => boolean;
|
|
}
|
|
|
|
export const SearchableItemDataAttribute = "data-search-item";
|
|
|
|
export type SearchableItems = { [id: string]: SearchableItem };
|
|
|
|
export class Search {
|
|
private _c: SearchConfiguration;
|
|
private _sortField: string = "";
|
|
private _ascending: boolean = true;
|
|
private _ordering: string[] = [];
|
|
private _items: SearchableItems = {};
|
|
// Search queries (filters)
|
|
private _queries: Query[] = [];
|
|
// Plain-text search terms
|
|
private _searchTerms: string[] = [];
|
|
inSearch: boolean = false;
|
|
private _inServerSearch: boolean = false;
|
|
get inServerSearch(): boolean { return this._inServerSearch; }
|
|
set inServerSearch(v: boolean) {
|
|
const previous = this._inServerSearch;
|
|
this._inServerSearch = v;
|
|
if (!v && previous != v) {
|
|
this._c.clearServerSearch();
|
|
}
|
|
}
|
|
|
|
// Intended to be set from the JS console, if true searches are timed.
|
|
timeSearches: boolean = false;
|
|
|
|
private _serverSearchButtons: HTMLElement[];
|
|
|
|
static tokenizeSearch = (query: string): string[] => {
|
|
query = query.toLowerCase();
|
|
|
|
let words: string[] = [];
|
|
let quoteSymbol = ``;
|
|
let queryStart = -1;
|
|
let lastQuote = -1;
|
|
for (let i = 0; i < query.length; i++) {
|
|
if (queryStart == -1 && query[i] != " " && query[i] != `"` && query[i] != `'`) {
|
|
queryStart = i;
|
|
}
|
|
if ((query[i] == `"` || query[i] == `'`) && (quoteSymbol == `` || query[i] == quoteSymbol)) {
|
|
if (lastQuote != -1) {
|
|
lastQuote = -1;
|
|
quoteSymbol = ``;
|
|
} else {
|
|
lastQuote = i;
|
|
quoteSymbol = query[i];
|
|
}
|
|
}
|
|
|
|
if (query[i] == " " || i == query.length-1) {
|
|
if (lastQuote != -1) {
|
|
continue;
|
|
} else {
|
|
let end = i+1;
|
|
if (query[i] == " ") {
|
|
end = i;
|
|
while (i+1 < query.length && query[i+1] == " ") {
|
|
i += 1;
|
|
}
|
|
}
|
|
words.push(query.substring(queryStart, end).replace(/['"]/g, ""));
|
|
queryStart = -1;
|
|
}
|
|
}
|
|
}
|
|
return words;
|
|
}
|
|
|
|
parseTokens = (tokens: string[]): [string[], Query[]] => {
|
|
let queries: Query[] = [];
|
|
let searchTerms: string[] = [];
|
|
|
|
for (let word of tokens) {
|
|
// 1. Normal search text, no filters or anything
|
|
if (!word.includes(":")) {
|
|
searchTerms.push(word);
|
|
continue;
|
|
}
|
|
// 2. A filter query of some sort.
|
|
const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)];
|
|
|
|
if (!(split[0] in this._c.queries)) continue;
|
|
|
|
const queryFormat = this._c.queries[split[0]];
|
|
|
|
let q: Query | null = null;
|
|
|
|
if (queryFormat.bool) {
|
|
let [boolState, isBool] = BoolQuery.paramsFromString(split[1]);
|
|
if (isBool) {
|
|
q = new BoolQuery(queryFormat, boolState);
|
|
q.onclick = () => {
|
|
for (let quote of [`"`, `'`, ``]) {
|
|
this._c.search.value = this._c.search.value.replace(split[0] + ":" + quote + split[1] + quote, "");
|
|
}
|
|
this._c.search.oninput((null as Event));
|
|
};
|
|
queries.push(q);
|
|
continue;
|
|
}
|
|
}
|
|
if (queryFormat.string) {
|
|
q = new StringQuery(queryFormat, split[1]);
|
|
|
|
q.onclick = () => {
|
|
for (let quote of [`"`, `'`, ``]) {
|
|
let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig");
|
|
this._c.search.value = this._c.search.value.replace(regex, "");
|
|
}
|
|
this._c.search.oninput((null as Event));
|
|
}
|
|
queries.push(q);
|
|
continue;
|
|
}
|
|
if (queryFormat.date) {
|
|
let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]);
|
|
if (!isDate) continue;
|
|
q = new DateQuery(queryFormat, op, parsedDate);
|
|
|
|
q.onclick = () => {
|
|
for (let quote of [`"`, `'`, ``]) {
|
|
let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig");
|
|
this._c.search.value = this._c.search.value.replace(regex, "");
|
|
}
|
|
|
|
this._c.search.oninput((null as Event));
|
|
}
|
|
queries.push(q);
|
|
continue;
|
|
}
|
|
// if (q != null) queries.push(q);
|
|
}
|
|
return [searchTerms, queries];
|
|
}
|
|
|
|
// Returns a list of identifiers (used as keys in items, values in ordering).
|
|
searchParsed = (searchTerms: string[], queries: Query[]): string[] => {
|
|
let result: string[] = [...this._ordering];
|
|
// If we didn't care about rendering the query cards, we could run this to (maybe) return early.
|
|
// if (this.inServerSearch) {
|
|
// let hasLocalOnlyQueries = false;
|
|
// for (const q of queries) {
|
|
// if (q.localOnly) {
|
|
// hasLocalOnlyQueries = true;
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// Normal searches can be evaluated by the server, so skip this if we've already ran one.
|
|
if (!this.inServerSearch) {
|
|
for (let term of searchTerms) {
|
|
let cachedResult = [...result];
|
|
for (let id of cachedResult) {
|
|
const u = this.items[id];
|
|
if (!u.matchesSearch(term)) {
|
|
result.splice(result.indexOf(id), 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let q of queries) {
|
|
this._c.filterArea.appendChild(q.asElement());
|
|
// Skip if this query has already been performed by the server.
|
|
if (this.inServerSearch && !(q.localOnly)) continue;
|
|
|
|
let cachedResult = [...result];
|
|
if (q.type == "bool") {
|
|
for (let id of cachedResult) {
|
|
const u = this.items[id];
|
|
// Remove from result if not matching query
|
|
if (!q.compareItem(u)) {
|
|
// console.log("not matching, result is", result);
|
|
result.splice(result.indexOf(id), 1);
|
|
}
|
|
}
|
|
} else if (q.type == "string") {
|
|
for (let id of cachedResult) {
|
|
const u = this.items[id];
|
|
// We want to compare case-insensitively, so we get value, lower-case it then compare,
|
|
// rather than doing both with compareItem.
|
|
const value = q.getValueFromItem(u).toLowerCase();
|
|
if (!q.compare(value)) {
|
|
result.splice(result.indexOf(id), 1);
|
|
}
|
|
}
|
|
} else if (q.type == "date") {
|
|
for (let id of cachedResult) {
|
|
const u = this.items[id];
|
|
// Getter here returns a unix timestamp rather than a date, so we can't use compareItem.
|
|
const unixValue = q.getValueFromItem(u);
|
|
if (unixValue == 0) {
|
|
result.splice(result.indexOf(id), 1);
|
|
continue;
|
|
}
|
|
let value = new Date(unixValue*1000);
|
|
|
|
if (!q.compare(value)) {
|
|
result.splice(result.indexOf(id), 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Returns a list of identifiers (used as keys in items, values in ordering).
|
|
search = (query: string): string[] => {
|
|
let timer = this.timeSearches ? performance.now() : null;
|
|
this._c.filterArea.textContent = "";
|
|
|
|
const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query));
|
|
|
|
let result = this.searchParsed(searchTerms, queries);
|
|
|
|
this._queries = queries;
|
|
this._searchTerms = searchTerms;
|
|
|
|
if (this.timeSearches) {
|
|
const totalTime = performance.now() - timer;
|
|
console.debug(`Search took ${totalTime}ms`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// postServerSearch performs local-only queries after a server search if necessary.
|
|
postServerSearch = () => {
|
|
this.searchParsed(this._searchTerms, this._queries);
|
|
};
|
|
|
|
showHideSearchOptionsHeader = () => {
|
|
let sortingBy = false;
|
|
if (this._c.sortingByButton) sortingBy = !(this._c.sortingByButton.classList.contains("hidden"));
|
|
const hasFilters = this._c.filterArea.textContent != "";
|
|
if (sortingBy || hasFilters) {
|
|
this._c.searchOptionsHeader.classList.remove("hidden");
|
|
} else {
|
|
this._c.searchOptionsHeader.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
// -all- elements.
|
|
get items(): { [id: string]: SearchableItem } { 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; }
|
|
|
|
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 = () => {
|
|
// 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", "mb-1", "mr-2", "align-bottom");
|
|
container.innerHTML = `
|
|
<div class="flex flex-col mr-2">
|
|
<span>${query.name}</span>
|
|
<span class="support">${query.description || ""}</span>
|
|
</div>
|
|
`;
|
|
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", "ml-2");
|
|
pos.innerHTML = `<i class="ri-checkbox-circle-fill"></i>`;
|
|
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", "ml-2");
|
|
neg.innerHTML = `<i class="ri-close-circle-fill"></i>`;
|
|
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", "ml-2");
|
|
button.innerHTML = `<i class="ri-equal-line mr-2"></i>${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", "ml-2");
|
|
onDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>On Date`;
|
|
onDate.addEventListener("click", () => this.fillInFilter(queryName, `"="`, -1));
|
|
|
|
const beforeDate = document.createElement("button") as HTMLButtonElement;
|
|
beforeDate.type = "button";
|
|
beforeDate.classList.add("button", "~urge", "ml-2");
|
|
beforeDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>Before Date`;
|
|
beforeDate.addEventListener("click", () => this.fillInFilter(queryName, `"<"`, -1));
|
|
|
|
const afterDate = document.createElement("button") as HTMLButtonElement;
|
|
afterDate.type = "button";
|
|
afterDate.classList.add("button", "~urge", "ml-2");
|
|
afterDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>After Date`;
|
|
afterDate.addEventListener("click", () => this.fillInFilter(queryName, `">"`, -1));
|
|
|
|
container.appendChild(onDate);
|
|
container.appendChild(beforeDate);
|
|
container.appendChild(afterDate);
|
|
}
|
|
|
|
this._c.filterList.appendChild(container);
|
|
}
|
|
}
|
|
|
|
onServerSearch = () => {
|
|
const newServerSearch = !this.inServerSearch;
|
|
this.inServerSearch = true;
|
|
this.searchServer(newServerSearch);
|
|
}
|
|
|
|
searchServer = (newServerSearch: boolean) => {
|
|
this._c.searchServer(this.serverSearchParams(this._searchTerms, this._queries), newServerSearch);
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
setServerSearchButtonsDisabled = (disabled: boolean) => {
|
|
this._serverSearchButtons.forEach((v: HTMLButtonElement) => v.disabled = disabled);
|
|
}
|
|
|
|
constructor(c: SearchConfiguration) {
|
|
this._c = c;
|
|
|
|
this._c.search.oninput = () => {
|
|
this.inServerSearch = false;
|
|
this.onSearchBoxChange();
|
|
}
|
|
this._c.search.addEventListener("keyup", (ev: KeyboardEvent) => {
|
|
if (ev.key == "Enter") {
|
|
this.onServerSearch();
|
|
}
|
|
});
|
|
|
|
const clearSearchButtons = Array.from(document.querySelectorAll(this._c.clearSearchButtonSelector)) as Array<HTMLSpanElement>;
|
|
for (let b of clearSearchButtons) {
|
|
b.addEventListener("click", () => {
|
|
this._c.search.value = "";
|
|
this.inServerSearch = false;
|
|
this.onSearchBoxChange();
|
|
});
|
|
}
|
|
|
|
this._serverSearchButtons = Array.from(document.querySelectorAll(this._c.serverSearchButtonSelector)) as Array<HTMLSpanElement>;
|
|
for (let b of this._serverSearchButtons) {
|
|
b.addEventListener("click", () => {
|
|
this.onServerSearch();
|
|
});
|
|
}
|
|
}
|
|
}
|