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