From f179b0805cca41be0d391f2c2ce602a8a99ac2b6 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 24 Jun 2020 17:12:11 +0200 Subject: [PATCH] Global search! --- src/TabManager.ts | 22 +++++--- src/index.ts | 5 +- src/search.ts | 137 ++++++++++++++++++++++++++++++++-------------- src/userscript.ts | 38 +++++++------ style/bgus.scss | 7 +++ 5 files changed, 142 insertions(+), 67 deletions(-) diff --git a/src/TabManager.ts b/src/TabManager.ts index 0548fe6..0d52e22 100644 --- a/src/TabManager.ts +++ b/src/TabManager.ts @@ -96,6 +96,8 @@ interface Section { } export default class TabManager { + static instance: TabManager; + sectionListContainer: HTMLElement; tabListContainer: HTMLElement; @@ -104,6 +106,8 @@ export default class TabManager { sections: Record = {}; + sectionMap: Record = {}; + loading: boolean; constructor( @@ -114,6 +118,7 @@ export default class TabManager { this.sectionListContainer = sectionlist; this.tabListContainer = tablist; this.tabContentContainer = tabcontent; + TabManager.instance = this; } /** @@ -208,7 +213,7 @@ export default class TabManager { if (tabListItem.classList.contains("active")) { return; } - this.setActive(section, page); + this.setActive(page); }); const iconElement = document.createElement("img"); iconElement.src = icon || unknown; @@ -228,6 +233,7 @@ export default class TabManager { // Create tab entry this.sections[section].tabs[page] = { tabListItem, tabContentItem }; + this.sectionMap[page] = section; // Hide tab if section is hidden if (!this.sections[section].element.classList.contains("active")) { @@ -243,17 +249,17 @@ export default class TabManager { // If asked for, set it to active if (active) { - this.setActive(section, page); + this.setActive(page); } } /** * Set a specific page to be the active/visible one - * @param section Section name * @param page Page name */ - setActive(section: string, page: string): void { + setActive(page: string): void { // Make sure tab exists (why wouldn't it?!) + const section = this.sectionMap[page]; if (!(section in this.sections)) { throw new Error("section not found"); } @@ -263,9 +269,6 @@ export default class TabManager { } // Deactivate current active tab - this.sectionListContainer - .querySelectorAll(".active") - .forEach((it) => it.classList.remove("active")); this.tabListContainer .querySelectorAll(".active") .forEach((it) => it.classList.remove("active")); @@ -274,7 +277,10 @@ export default class TabManager { .forEach((it) => it.classList.remove("active")); // If section is not shown, show it! - if (!this.sections[section].element.classList.contains("active")) { + const isSectionActive = this.sections[section].element.classList.contains( + "active" + ); + if (!isSectionActive) { this.showSection(section); } diff --git a/src/index.ts b/src/index.ts index 36cefbb..1b34708 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import TabManager from "./TabManager"; import sections from "./sections"; import { nextAnimationFrame } from "./utils"; +import { searchBox } from "./search"; // @ts-expect-error: Parcel image import import unknown from "~/assets/images/tab-icons/unknown.svg"; @@ -50,7 +51,7 @@ async function load() { manager.setLoading(false); // Set first page as active - manager.setActive("Medical", "Guide_to_chemistry"); + manager.setActive("Guide_to_chemistry"); }); } if ("serviceWorker" in navigator) { @@ -66,3 +67,5 @@ if ("serviceWorker" in navigator) { } load(); + +document.body.appendChild(searchBox()); diff --git a/src/search.ts b/src/search.ts index cc60828..08489b9 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,29 +1,51 @@ -interface SearchOption { +import { nextAnimationFrame } from "./utils"; +import TabManager from "./TabManager"; + +interface SearchEntry { + page: string; + element: HTMLElement; + name: string; + id: number; alignment: ScrollLogicalPosition; } -export function searchBox( - el: HTMLElement[], - searchCandidate, - options: SearchOption = { - alignment: "center", - } -): HTMLElement { +const allEntries: SearchEntry[] = []; + +/** + * Add one or more entries to the global search database + * @param entries Search entries to add + */ +export function registerSearchEntries(entries: SearchEntry[]): void { + allEntries.push(...entries); +} + +export function searchBox(): HTMLElement { // Fuzzy search box const resultList = document.createElement("ul"); const searchBoxElem = document.createElement("div"); - let selectedResult = null; + let selectedResult = 0; let results = []; - const jumpTo = (id: number) => { - el[id].scrollIntoView({ - block: options.alignment, + let global = false; + + const jumpTo = (entry: SearchEntry) => { + // If page is different jump to that + if (global) { + const currentPage = document.querySelector(".page.active") + .dataset.tab; + if (currentPage !== entry.page) { + TabManager.instance.setActive(entry.page); + } + } + + entry.element.scrollIntoView({ + block: entry.alignment, inline: "nearest", behavior: "auto", }); document .querySelectorAll("table.wikitable .bgus_fz_selected") .forEach((sel) => sel.classList.remove("bgus_fz_selected")); - el[id].parentElement.classList.add("bgus_fz_selected"); + entry.element.parentElement.classList.add("bgus_fz_selected"); }; const setSelectedResult = (i) => { @@ -32,21 +54,39 @@ export function searchBox( .querySelectorAll(".selected") .forEach((sel) => sel.classList.remove("selected")); resultList.children[i].classList.add("selected"); - jumpTo(results[i].id); + jumpTo(results[i]); }; - const search = (str) => { - if (!str) { + const search = async (str: string, currentPage: string) => { + if (!str || str.length < 1) { return; } + // Check for special flags + let entries: SearchEntry[] = allEntries; + global = str[0] === ","; + + // Unless we're doing a global search don't show entries for other pages + if (!global) { + entries = allEntries.filter((e) => e.page === currentPage); + } else { + // Remove prefix from string + str = str.substr(1); + } + + // Re-check string lenght after prefix removal + if (str.length < 1) { + return; + } + const combinations = str .split("") - .map((c) => (c.includes(["\\", "]", "^"]) ? `\\${c}` : c)) + .map((c) => (["\\", "]", "^"].includes(c) ? `\\${c}` : c)) .join("])(.*?)(["); const regex = new RegExp(`^(.*?)([${combinations}])(.*?)$`, "i"); - const arr = searchCandidate - .map((o) => { - o.matches = (o.str.match(regex) || []) + results = entries + .map((o) => ({ + ...o, + matches: (o.name.match(regex) || []) .slice(1) .reduce((list, group, i, or) => { // Initialize first placeholder (always empty) and first matching "sections" @@ -62,10 +102,9 @@ export function searchBox( list.push([group]); } return list; - }, []) - .map((cstr) => cstr.join("")); - return o; - }) + }, [] as string[][]) + .map((cstr) => cstr.join("")), + })) // Strike non-matching rows .filter((o) => o.matches.length > 0) .sort((oA, oB) => { @@ -96,24 +135,35 @@ export function searchBox( // Make the search stable since ECMAScript doesn't mandate it return iA - iB; }); - results = arr; - window.requestAnimationFrame(() => { - resultList.innerHTML = ""; - arr.forEach(({ matches, id }) => { - const li = document.createElement("li"); - li.innerHTML = matches - .map((c, i) => (i % 2 ? `${c}` : c)) - .join(""); - li.addEventListener("click", () => { - jumpTo(id); - searchBoxElem.classList.add("bgus_hidden"); - }); - resultList.appendChild(li); + + await nextAnimationFrame(); + + console.log(results); + resultList.innerHTML = ""; + results.forEach((elem) => { + const li = document.createElement("li"); + elem.matches.forEach((match, i) => { + const cont = document.createElement(i % 2 ? "strong" : "span"); + cont.appendChild(document.createTextNode(match)); + li.appendChild(cont); }); - if (results.length > 0) { - setSelectedResult(0); + if (global) { + const source = document.createElement("span"); + source.className = "source"; + source.appendChild( + document.createTextNode(elem.page.replace(/_/g, " ")) + ); + li.appendChild(source); } + li.addEventListener("click", () => { + jumpTo(elem); + searchBoxElem.classList.add("bgus_hidden"); + }); + resultList.appendChild(li); }); + if (results.length > 0) { + setSelectedResult(0); + } }; // Create fuzzy search box @@ -128,7 +178,7 @@ export function searchBox( return; case 13: // Enter - Jump to first result and hide bar if (results.length > 0) { - jumpTo(results[selectedResult].id); + jumpTo(results[selectedResult]); } searchBoxElem.classList.add("bgus_hidden"); return; @@ -144,7 +194,10 @@ export function searchBox( return; default: if (sel.value !== oldValue) { - search(sel.value); + const currentPage = document.querySelector( + ".page.active" + ); + search(sel.value, currentPage.dataset.tab); oldValue = sel.value; } } @@ -178,4 +231,4 @@ export function searchBox( return searchBoxElem; } -export default searchBox; +export default { searchBox, registerSearchEntries }; diff --git a/src/userscript.ts b/src/userscript.ts index 158d907..9eb5214 100644 --- a/src/userscript.ts +++ b/src/userscript.ts @@ -1,5 +1,5 @@ import { darken, ColorFmt, lighten } from "./darkmode"; -import searchBox from "./search"; +import { registerSearchEntries } from "./search"; import { findParent } from "./utils"; // This is used for cache busting when userscript changes significantly. @@ -301,14 +301,18 @@ function chemistryScript(root: HTMLElement) { "table.wikitable > tbody > tr:not(:first-child) > th" ) ); - const name = el.map((elem) => - elem.querySelector(".reagent-header").textContent.trim().replace("▮", "") + registerSearchEntries( + el.map((element, id) => ({ + page: "Guide_to_chemistry", + name: element + .querySelector(".reagent-header") + .textContent.trim() + .replace("▮", ""), + element, + alignment: "center", + id, + })) ); - const box = searchBox( - el, - name.map((e, i) => ({ id: i, str: e })) - ); - document.body.appendChild(box); document.body.addEventListener("keydown", (ev) => { if (ev.shiftKey) { @@ -353,19 +357,21 @@ function chemistryScript(root: HTMLElement) { }); } -function genericScript(root: HTMLElement) { +function genericScript(root: HTMLElement, docname: string) { const el = Array.from( root.querySelectorAll("div.mw-headline-cont[id][data-name]") ); - const name = el.map((elem: HTMLDivElement) => elem.dataset.name.trim()); // Init fuzzy search with headlines - const box = searchBox( - el, - name.map((e, i) => ({ id: i, str: e })), - { alignment: "start" } + registerSearchEntries( + el.map((element: HTMLDivElement, id) => ({ + id, + page: docname, + name: element.dataset.name.trim(), + element, + alignment: "start", + })) ); - root.appendChild(box); } export function bindFunctions(root: HTMLElement, docname: string): void { @@ -374,7 +380,7 @@ export function bindFunctions(root: HTMLElement, docname: string): void { chemistryScript(root); break; default: - genericScript(root); + genericScript(root, docname); break; } } diff --git a/style/bgus.scss b/style/bgus.scss index 7b01c73..fa30af9 100644 --- a/style/bgus.scss +++ b/style/bgus.scss @@ -48,6 +48,13 @@ margin: 0; padding: 5px; cursor: pointer; + + .source { + color: #ccc; + font-size: 8pt; + display: block; + line-height: 1.4em; + } } #bgus_fz_searchbox li:hover { background-color: rgba(100, 100, 100, 0.5);