Files
SubMiner/vendor/hachidori/extension/settings-search.js
T
sudacode d9fdc7ef6d feat(dictionary): add Hachidori backend support
- Add backend selection, setup gating, Anki integration, and external host support
- Add launcher flags, documentation, packaging, and focused tests
- Open on-demand overlay modals on the first attempt
2026-09-22 00:21:19 -07:00

106 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* SPDX-License-Identifier: GPL-3.0-or-later */
const text = node => node?.textContent.replace(/\s+/g, " ").trim() || "";
const normalise = value => value.normalize("NFKC").toLocaleLowerCase();
export function createSettingsSearch({ document, navigate }) {
const input = document.getElementById("settings-search");
const panel = document.getElementById("settings-search-results");
const results = document.getElementById("settings-search-matches");
const count = document.getElementById("settings-search-count");
function clear() {
input.value = "";
panel.hidden = true;
results.replaceChildren();
}
function appendResult(section, target, sectionName, sectionGroup, group, label, hint) {
const item = document.createElement("li");
const link = document.createElement("a");
link.href = `#${section.id}`;
const breadcrumb = document.createElement("small");
const sectionPath = sectionGroup ? `${sectionGroup} ${sectionName}` : sectionName;
let breadcrumbText = sectionGroup && label === sectionName ? sectionGroup : sectionPath;
if (group && group !== label) breadcrumbText = `${sectionPath} ${group}`;
breadcrumb.textContent = breadcrumbText;
const title = document.createElement("strong");
title.textContent = label;
link.append(breadcrumb, title);
if (hint) {
const detail = document.createElement("span");
detail.textContent = hint;
link.append(detail);
}
link.addEventListener("click", event => {
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
navigate(section.id);
let destination = target;
for (let parent = target; parent && parent !== section; parent = parent.parentElement) {
// Conditional settings stay governed by their own enable control.
// Lead to the enclosing group when the matching control is hidden.
if (parent.hidden) destination = parent.parentElement;
else if (parent.tagName === "DETAILS") parent.open = true;
}
const controls = [destination.control, ...destination.querySelectorAll("input, select, textarea, button")];
const control = controls.find(node => node && !node.disabled && !node.closest("[hidden]")) || destination;
if (!control.matches("input, select, textarea, button, summary, a[href], [tabindex]")) control.tabIndex = -1;
control.focus({ preventScroll: true });
destination.scrollIntoView({ block: "center" });
});
item.append(link);
results.append(item);
}
function searchSection(section, words) {
section.hidden = true;
const sectionName = section.dataset.settingsName || text(section.querySelector("h1"));
const sectionGroup = section.dataset.settingsGroup || "";
// Read mounted labels on demand, including lazy controls once populated.
const candidates = section.querySelectorAll("h1, h2, h3, legend, summary, label, button[id]");
for (const target of candidates) {
if (target.closest("template, [role=status], output")) continue;
const label = text(target.querySelector(".field-label"))
|| text(target.querySelector("span")) || text(target);
if (!label) continue;
const group = text(target.closest("fieldset")?.querySelector("legend"));
const hint = text(target.querySelector(".field-hint"));
const searchable = normalise(`${sectionGroup} ${sectionName} ${group} ${label} ${hint} ${target.dataset.searchKeywords || ""}`);
if (!words.every(word => searchable.includes(word))) continue;
appendResult(section, target, sectionName, sectionGroup, group, label, hint);
}
}
function search() {
const words = normalise(input.value.trim()).split(/\s+/).filter(Boolean);
if (!words.length) { navigate(); return; }
results.replaceChildren();
panel.hidden = false;
document.getElementById("library-navigation").hidden = true;
for (const section of document.querySelectorAll("main > section:not([data-settings-unavailable='true'])")) {
searchSection(section, words);
}
const matches = results.childElementCount;
const noun = matches === 1 ? "setting" : "settings";
count.textContent = matches ? `${matches} ${noun} found` : "No settings found. Try another word.";
}
input.addEventListener("input", search);
input.addEventListener("keydown", event => {
if (event.key === "Escape") { event.preventDefault(); navigate(); input.focus(); }
if (event.key === "ArrowDown") { event.preventDefault(); results.querySelector("a")?.focus(); }
});
results.addEventListener("keydown", event => {
if (event.key === "Escape") { event.preventDefault(); navigate(); input.focus(); return; }
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
const links = [...results.querySelectorAll("a")];
const index = links.indexOf(event.target.closest("a"));
if (index < 0) return;
event.preventDefault();
if (event.key === "ArrowUp") (links[index - 1] || input).focus();
else links[Math.min(index + 1, links.length - 1)].focus();
});
return { clear };
}