Separate search and fix subrecipes

This commit is contained in:
Hamcha 2020-06-17 18:11:55 +02:00
parent 5ed479af20
commit 2066991942
Signed by untrusted user: hamcha
GPG key ID: 41467804B19A3315
3 changed files with 199 additions and 197 deletions

181
lib/search.ts Normal file
View file

@ -0,0 +1,181 @@
interface SearchOption {
alignment: ScrollLogicalPosition;
}
export function searchBox(
el: HTMLElement[],
searchCandidate,
options: SearchOption = {
alignment: "center",
}
): HTMLElement {
// Fuzzy search box
const resultList = document.createElement("ul");
const searchBoxElem = document.createElement("div");
let selectedResult = null;
let results = [];
const jumpTo = (id: number) => {
el[id].scrollIntoView({
block: options.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");
};
const setSelectedResult = (i) => {
selectedResult = i;
resultList
.querySelectorAll(".selected")
.forEach((sel) => sel.classList.remove("selected"));
resultList.children[i].classList.add("selected");
jumpTo(results[i].id);
};
const search = (str) => {
if (!str) {
return;
}
const combinations = str
.split("")
.map((c) => (c.includes(["\\", "]", "^"]) ? `\\${c}` : c))
.join("])(.*?)([");
const regex = new RegExp(`^(.*?)([${combinations}])(.*?)$`, "i");
const arr = searchCandidate
.map((o) => {
o.matches = (o.str.match(regex) || [])
.slice(1)
.reduce((list, group, i, or) => {
// Initialize first placeholder (always empty) and first matching "sections"
if (i < 2) {
list.push([group]);
}
// If group is second match in a row join to previous section
else if (or[i - 1] === "") {
list[list.length - 1].push(group);
}
// If group is a match create a new section
else if (group !== "") {
list.push([group]);
}
return list;
}, [])
.map((cstr) => cstr.join(""));
return o;
})
// Strike non-matching rows
.filter((o) => o.matches.length > 0)
.sort((oA, oB) => {
const iA = oA.id,
a = oA.matches;
const iB = oB.id,
b = oB.matches;
// Exact match
if (a.length === 1 && b.length !== 1) return -1;
if (a.length !== 1 && b.length === 1) return 1;
// Most complete groups (alphanumeric)
const clean = (cel) => !/[^a-zA-Z0-9]*$/.test(cel);
const cLen = a.filter(clean).length - b.filter(clean).length;
if (cLen !== 0) return cLen;
// Least distant first gropus
for (let i = 0; i < Math.min(a.length, b.length) - 1; i += 2) {
const gLen = a[i].length - b[i].length;
if (gLen !== 0) return gLen;
}
// Most complete groups (raw)
const len = a.length - b.length;
if (len !== 0) return len;
// 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 ? `<strong>${c}</strong>` : c))
.join("");
li.addEventListener("click", () => {
jumpTo(id);
searchBoxElem.classList.add("bgus_hidden");
});
resultList.appendChild(li);
});
if (results.length > 0) {
setSelectedResult(0);
}
});
};
// Create fuzzy search box
const sel = document.createElement("input");
// Bind events
let oldValue = "";
sel.addEventListener("keyup", (event) => {
switch (event.keyCode) {
case 27: // Escape - Hide bar
searchBoxElem.classList.add("bgus_hidden");
return;
case 13: // Enter - Jump to first result and hide bar
if (results.length > 0) {
jumpTo(results[selectedResult].id);
}
searchBoxElem.classList.add("bgus_hidden");
return;
case 40: // Down arrow - Select next result
if (selectedResult < results.length - 1) {
setSelectedResult(selectedResult + 1);
}
return;
case 38: // Up arrow - Select previous result
if (selectedResult > 0) {
setSelectedResult(selectedResult - 1);
}
return;
default:
if (sel.value !== oldValue) {
search(sel.value);
oldValue = sel.value;
}
}
});
document.body.addEventListener("keyup", (ev) => {
if (ev.keyCode === 83) {
sel.focus();
}
});
document.body.addEventListener("keydown", (ev) => {
if (ev.shiftKey) {
switch (ev.keyCode) {
// SHIFT+S = Fuzzy search
case 83: {
searchBoxElem.classList.remove("bgus_hidden");
sel.value = "";
break;
}
default:
// Do nothing
}
}
});
searchBoxElem.id = "bgus_fz_searchbox";
searchBoxElem.classList.add("bgus_hidden");
searchBoxElem.appendChild(sel);
searchBoxElem.appendChild(resultList);
return searchBoxElem;
}
export default searchBox;

View file

@ -1,8 +1,5 @@
import { darken, ColorFmt, lighten } from "./darkmode"; import { darken, ColorFmt, lighten } from "./darkmode";
import searchBox from "./search";
const DEFAULT_OPTS = {
alignment: "center",
};
export default function userscript(root: HTMLElement, docname: string): void { export default function userscript(root: HTMLElement, docname: string): void {
root.querySelectorAll(".mw-editsection").forEach((editLink) => { root.querySelectorAll(".mw-editsection").forEach((editLink) => {
@ -20,7 +17,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
} }
td.setAttribute("bgcolor", darken(bgcolor, ColorFmt.HEX).slice(1)); td.setAttribute("bgcolor", darken(bgcolor, ColorFmt.HEX).slice(1));
}); });
root.querySelectorAll("*[style]").forEach((td: HTMLElement) => { root.querySelectorAll<HTMLElement>("*[style]").forEach((td) => {
if (td.style.backgroundColor !== "") { if (td.style.backgroundColor !== "") {
td.style.backgroundColor = darken(td.style.backgroundColor, ColorFmt.RGB); td.style.backgroundColor = darken(td.style.backgroundColor, ColorFmt.RGB);
} }
@ -63,7 +60,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
div.className = "mw-headline-cont"; div.className = "mw-headline-cont";
}); });
root.querySelectorAll(".mw-headline").forEach((span: HTMLElement) => { root.querySelectorAll<HTMLElement>(".mw-headline").forEach((span) => {
// Find nearest container // Find nearest container
let parent = span.parentElement; let parent = span.parentElement;
while (parent !== null) { while (parent !== null) {
@ -71,6 +68,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
parent.id = span.id; parent.id = span.id;
span.id += "-span"; span.id += "-span";
parent.dataset.name = span.innerText; parent.dataset.name = span.innerText;
return;
} }
parent = parent.parentElement; parent = parent.parentElement;
} }
@ -87,179 +85,6 @@ export default function userscript(root: HTMLElement, docname: string): void {
</table>`; </table>`;
postbody.insertBefore(statusMessage, postbody.firstChild); postbody.insertBefore(statusMessage, postbody.firstChild);
// TODO Refactor this mess
function searchBox(el, searchCandidate, options = DEFAULT_OPTS) {
// Fuzzy search box
const resultList = document.createElement("ul");
const searchBoxElem = document.createElement("div");
let selectedResult = null;
let results = [];
const jumpTo = (id) => {
el[id].scrollIntoView({
block: options.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");
};
const setSelectedResult = (i) => {
selectedResult = i;
resultList
.querySelectorAll(".selected")
.forEach((sel) => sel.classList.remove("selected"));
resultList.children[i].classList.add("selected");
jumpTo(results[i].id);
};
const search = (str) => {
if (!str) {
return;
}
const regex = new RegExp(
`^(.*?)([${str
.split("")
.map((c) => (c.includes(["\\", "]", "^"]) ? `\\${c}` : c))
.join("])(.*?)([")}])(.*?)$`,
"i"
);
const arr = searchCandidate
.map((o) => {
o.matches = (o.str.match(regex) || [])
.slice(1)
.reduce((list, group, i, or) => {
// Initialize first placeholder (always empty) and first matching "sections"
if (i < 2) {
list.push([group]);
}
// If group is second match in a row join to previous section
else if (or[i - 1] === "") {
list[list.length - 1].push(group);
}
// If group is a match create a new section
else if (group !== "") {
list.push([group]);
}
return list;
}, [])
.map((cstr) => cstr.join(""));
return o;
})
// Strike non-matching rows
.filter((o) => o.matches.length > 0)
.sort((oA, oB) => {
const iA = oA.id,
a = oA.matches;
const iB = oB.id,
b = oB.matches;
// Exact match
if (a.length === 1 && b.length !== 1) return -1;
if (a.length !== 1 && b.length === 1) return 1;
// Most complete groups (alphanumeric)
const clean = (cel) => !/[^a-zA-Z0-9]*$/.test(cel);
const cLen = a.filter(clean).length - b.filter(clean).length;
if (cLen !== 0) return cLen;
// Least distant first gropus
for (let i = 0; i < Math.min(a.length, b.length) - 1; i += 2) {
const gLen = a[i].length - b[i].length;
if (gLen !== 0) return gLen;
}
// Most complete groups (raw)
const len = a.length - b.length;
if (len !== 0) return len;
// 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 ? `<strong>${c}</strong>` : c))
.join("");
li.addEventListener("click", () => {
jumpTo(id);
searchBoxElem.classList.add("bgus_hidden");
});
resultList.appendChild(li);
});
if (results.length > 0) {
setSelectedResult(0);
}
});
};
// Create fuzzy search box
const sel = document.createElement("input");
searchBoxElem.id = "bgus_fz_searchbox";
searchBoxElem.classList.add("bgus_hidden");
searchBoxElem.appendChild(sel);
searchBoxElem.appendChild(resultList);
root.appendChild(searchBoxElem);
// Bind events
let oldValue = "";
sel.addEventListener("keyup", (event) => {
switch (event.keyCode) {
case 27: // Escape - Hide bar
searchBoxElem.classList.add("bgus_hidden");
return;
case 13: // Enter - Jump to first result and hide bar
if (results.length > 0) {
jumpTo(results[selectedResult].id);
}
searchBoxElem.classList.add("bgus_hidden");
return;
case 40: // Down arrow - Select next result
if (selectedResult < results.length - 1) {
setSelectedResult(selectedResult + 1);
}
return;
case 38: // Up arrow - Select previous result
if (selectedResult > 0) {
setSelectedResult(selectedResult - 1);
}
return;
default:
if (sel.value !== oldValue) {
search(sel.value);
oldValue = sel.value;
}
}
});
document.body.addEventListener("keyup", (ev) => {
if (ev.keyCode === 83) {
sel.focus();
}
});
document.body.addEventListener("keydown", (ev) => {
if (ev.shiftKey) {
switch (ev.keyCode) {
// SHIFT+S = Fuzzy search
case 83: {
searchBoxElem.classList.remove("bgus_hidden");
sel.value = "";
break;
}
default:
// Do nothing
}
}
});
}
function betterChemistry() { function betterChemistry() {
// Fix inconsistencies with <p> on random parts // Fix inconsistencies with <p> on random parts
// Ideally I'd like a <p> or something on every part, wrapping it completely, but for now let's just kill 'em // Ideally I'd like a <p> or something on every part, wrapping it completely, but for now let's just kill 'em
@ -272,13 +97,11 @@ export default function userscript(root: HTMLElement, docname: string): void {
// The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop. // The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop.
// Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index. // Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index.
Array.from(td.childNodes).forEach((el) => { Array.from(td.childNodes).forEach((el) => {
if (el instanceof HTMLElement) { if (el instanceof Element && el.tagName === "P") {
if (el.tagName === "P") {
tmp.append(...el.childNodes); tmp.append(...el.childNodes);
} else { } else {
tmp.append(el); tmp.append(el);
} }
}
}); });
td.parentNode.replaceChild(tmp, td); td.parentNode.replaceChild(tmp, td);
}); });
@ -324,7 +147,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
}); });
// Wrap every recipe with extra metadata // Wrap every recipe with extra metadata
root.querySelectorAll(".bgus_part").forEach((el: HTMLElement) => { root.querySelectorAll<HTMLElement>(".bgus_part").forEach((el) => {
if ("parts" in el.parentElement.dataset) { if ("parts" in el.parentElement.dataset) {
el.parentElement.dataset.parts = ( el.parentElement.dataset.parts = (
parseInt(el.parentElement.dataset.parts, 10) + parseInt(el.parentElement.dataset.parts, 10) +
@ -365,7 +188,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
root.classList.add("bchem"); root.classList.add("bchem");
// Init fuzzy search with elements // Init fuzzy search with elements
const el = Array.from( const el = Array.from(
root.querySelectorAll( root.querySelectorAll<HTMLElement>(
"table.wikitable > tbody > tr:not(:first-child) > th" "table.wikitable > tbody > tr:not(:first-child) > th"
) )
); );
@ -378,10 +201,11 @@ export default function userscript(root: HTMLElement, docname: string): void {
}); });
return partial.trim(); return partial.trim();
}); });
searchBox( const box = searchBox(
el, el,
name.map((e, i) => ({ id: i, str: e })) name.map((e, i) => ({ id: i, str: e }))
); );
root.appendChild(box);
// Remove "Removed medicines" section // Remove "Removed medicines" section
const remTable = root.querySelector( const remTable = root.querySelector(
@ -390,8 +214,8 @@ export default function userscript(root: HTMLElement, docname: string): void {
remTable.parentElement.removeChild(remTable); remTable.parentElement.removeChild(remTable);
root root
.querySelectorAll("div[data-name] .wikitable.sortable tr") .querySelectorAll<HTMLElement>("div[data-name] .wikitable.sortable tr")
.forEach((row: HTMLElement) => { .forEach((row) => {
let sectionEl = row.parentElement; let sectionEl = row.parentElement;
while (!sectionEl.dataset.name) { while (!sectionEl.dataset.name) {
sectionEl = sectionEl.parentElement; sectionEl = sectionEl.parentElement;
@ -504,16 +328,17 @@ export default function userscript(root: HTMLElement, docname: string): void {
function betterGeneric() { function betterGeneric() {
const el = Array.from( const el = Array.from(
root.querySelectorAll("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()); const name = el.map((elem: HTMLDivElement) => elem.dataset.name.trim());
// Init fuzzy search with headlines // Init fuzzy search with headlines
searchBox( const box = searchBox(
el, el,
name.map((e, i) => ({ id: i, str: e })), name.map((e, i) => ({ id: i, str: e })),
{ alignment: "start" } { alignment: "start" }
); );
root.appendChild(box);
} }
switch (docname) { switch (docname) {

View file

@ -55,11 +55,7 @@
div.tooltiptext { div.tooltiptext {
display: none; display: none;
border: 1px solid #384e68; border: 1px solid #384e68;
background: linear-gradient( background: linear-gradient(to bottom, darken(#384e68, 20%), darken(#384e68, 25%));
to bottom,
darken(#384e68, 20%),
darken(#384e68, 25%)
);
} }
span.bgus_nested_element:not(.bgus_collapsed) + div.tooltiptext { span.bgus_nested_element:not(.bgus_collapsed) + div.tooltiptext {
z-index: unset; z-index: unset;