const dateParser = require("any-date-parser"); export interface QueryType { name: string; description?: string; getter: string; bool: boolean; string: boolean; date: boolean; dependsOnElement?: string; // Format for querySelector show?: boolean; } export interface SearchConfiguration { filterArea: HTMLElement; sortingByButton: HTMLButtonElement; searchOptionsHeader: HTMLElement; notFoundPanel: HTMLElement; notFoundCallback?: (notFound: boolean) => void; filterList: HTMLElement; clearSearchButtonSelector: string; search: HTMLInputElement; queries: { [field: string]: QueryType }; setVisibility: (items: string[], visible: boolean) => void; onSearchCallback: (visibleCount: number, newItems: boolean, loadAll: boolean) => void; loadMore?: () => void; } export interface SearchableItem { matchesSearch: (query: string) => boolean; } export class Search { private _c: SearchConfiguration; private _ordering: string[] = []; private _items: { [id: string]: SearchableItem }; inSearch: boolean; search = (query: String): string[] => { this._c.filterArea.textContent = ""; query = query.toLowerCase(); let result: string[] = [...this._ordering]; 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, "")); console.log("pushed", words); queryStart = -1; } } } query = ""; for (let word of words) { if (!word.includes(":")) { let cachedResult = [...result]; for (let id of cachedResult) { const u = this._items[id]; if (!u.matchesSearch(word)) { result.splice(result.indexOf(id), 1); } } continue; } 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]]; if (queryFormat.bool) { let isBool = false; let boolState = false; if (split[1] == "true" || split[1] == "yes" || split[1] == "t" || split[1] == "y") { isBool = true; boolState = true; } else if (split[1] == "false" || split[1] == "no" || split[1] == "f" || split[1] == "n") { isBool = true; boolState = false; } if (isBool) { const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full"); filterCard.innerHTML = ` ${queryFormat.name} `; filterCard.addEventListener("click", () => { 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)); }) this._c.filterArea.appendChild(filterCard); // console.log("is bool, state", boolState); // So removing elements doesn't affect us let cachedResult = [...result]; for (let id of cachedResult) { const u = this._items[id]; const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u); // console.log("got", queryFormat.getter + ":", value); // Remove from result if not matching query if (!((value && boolState) || (!value && !boolState))) { // console.log("not matching, result is", result); result.splice(result.indexOf(id), 1); } } continue } } if (queryFormat.string) { const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); filterCard.classList.add("button", "~neutral", "@low", "center", "mx-2", "h-full"); filterCard.innerHTML = ` ${queryFormat.name}: "${split[1]}" `; filterCard.addEventListener("click", () => { 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)); }) this._c.filterArea.appendChild(filterCard); let cachedResult = [...result]; for (let id of cachedResult) { const u = this._items[id]; const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u).toLowerCase(); if (!(value.includes(split[1]))) { result.splice(result.indexOf(id), 1); } } continue; } if (queryFormat.date) { // -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0 let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2)); let unmodifiedValue = split[1]; if (compareType != 2) { split[1] = split[1].substring(1); } if (compareType == 2) compareType = 0; let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]); // Month in Date objects is 0-based, so make our parsed date that way too if ("month" in attempt) attempt.month -= 1; let date: Date = (Date as any).fromString(split[1]) as Date; console.log("Read", attempt, "and", date); if ("invalid" in (date as any)) continue; const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full"); filterCard.innerHTML = ` ${queryFormat.name}: ${(compareType == 1) ? window.lang.strings("after")+" " : ((compareType == -1) ? window.lang.strings("before")+" " : "")}${split[1]} `; filterCard.addEventListener("click", () => { for (let quote of [`"`, `'`, ``]) { let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig"); this._c.search.value = this._c.search.value.replace(regex, ""); } this._c.search.oninput((null as Event)); }) this._c.filterArea.appendChild(filterCard); let cachedResult = [...result]; for (let id of cachedResult) { const u = this._items[id]; const unixValue = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u); if (unixValue == 0) { result.splice(result.indexOf(id), 1); continue; } let value = new Date(unixValue*1000); const getterPairs: [string, () => number][] = [["year", Date.prototype.getFullYear], ["month", Date.prototype.getMonth], ["day", Date.prototype.getDate], ["hour", Date.prototype.getHours], ["minute", Date.prototype.getMinutes]]; // When doing > or <