Files
brk/website/scripts/panes/search.js
2026-04-16 22:17:41 +02:00

188 lines
5.4 KiB
JavaScript

import {
searchInput,
searchLabelElement,
searchResultsElement,
} from "../utils/elements.js";
import { QuickMatch } from "../modules/quickmatch-js/0.4.1/src/index.js";
import { brk } from "../utils/client.js";
/**
* @param {Options} options
*/
export function init(options) {
console.log("search: init");
const haystack = options.list.map((option) => option.title.toLowerCase());
const titleToOption = new Map(
options.list.map((option) => [option.title.toLowerCase(), option]),
);
const matcher = new QuickMatch(haystack);
/** @type {HTMLLIElement | undefined} */
let highlighted;
/** @param {HTMLLIElement} [li] */
function setHighlight(li) {
if (highlighted) delete highlighted.dataset.highlight;
highlighted = li;
if (li) li.dataset.highlight = "";
}
const HEX64_RE = /^[0-9a-f]{64}$/i;
const ADDR_RE = /^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{8,87})$/;
/** @param {string} label @param {string} href @param {Element | null} [before] */
function createResultLink(label, href, before) {
const li = window.document.createElement("li");
const a = window.document.createElement("a");
a.href = href;
a.textContent = label;
a.title = label;
if (href === window.location.pathname) setHighlight(li);
a.addEventListener("click", (e) => {
e.preventDefault();
setHighlight(li);
history.pushState(null, "", href);
options.resolveUrl();
});
li.append(a);
searchResultsElement.insertBefore(li, before ?? null);
}
/** @type {AbortController | undefined} */
let lookupController;
/** @param {string} needle @param {AbortSignal} signal */
async function lookup(needle, signal) {
/** @type {Array<[string, string]>} */
const results = [];
if (HEX64_RE.test(needle)) {
const [blockRes, txRes] = await Promise.allSettled([
brk.getBlock(needle, { signal }),
brk.getTx(needle, { signal }),
]);
if (signal.aborted) return;
if (blockRes.status === "fulfilled") results.push(["Block", `/block/${needle}`]);
if (txRes.status === "fulfilled") results.push(["Transaction", `/tx/${needle}`]);
} else if (ADDR_RE.test(needle)) {
try {
const { isvalid } = await brk.validateAddress(needle, { signal });
if (signal.aborted || !isvalid) return;
results.push(["Address", `/address/${needle}`]);
} catch { return; }
} else {
return;
}
const before = searchResultsElement.firstElementChild;
for (const [label, href] of results) {
createResultLink(`${label} ${needle}`, href, before);
}
// Remove "No results" placeholder if present
const last = searchResultsElement.lastElementChild;
if (last && !last.querySelector("a")) last.remove();
}
function inputEvent() {
const needle = /** @type {string} */ (searchInput.value).trim();
if (lookupController) lookupController.abort();
searchResultsElement.scrollTo({ top: 0 });
searchResultsElement.innerHTML = "";
setHighlight();
if (!needle.length) return;
const matches = matcher.matches(needle);
const indexMatch = needle.match(
/^(?:(block|b)|(transaction|tx))?\s*#?\s*(\d+)$/i,
);
if (indexMatch) {
const num = indexMatch[3];
const entries = indexMatch[1]
? [["Block", `/block/${num}`]]
: indexMatch[2]
? [["Transaction", `/tx/${num}`]]
: [
["Block", `/block/${num}`],
["Transaction", `/tx/${num}`],
];
for (const [label, href] of entries) {
createResultLink(`${label} #${num}`, href);
}
}
lookupController = new AbortController();
lookup(needle, lookupController.signal);
if (matches.length) {
matches.forEach((title) => {
const option = titleToOption.get(title);
if (!option) return;
const li = window.document.createElement("li");
searchResultsElement.appendChild(li);
if (option === options.selected.value) setHighlight(li);
const element = options.createOptionElement({
option,
name: option.title,
});
if (element) li.append(element);
});
}
if (!searchResultsElement.children.length) {
const li = window.document.createElement("li");
li.textContent = "No results";
li.style.color = "var(--off-color)";
searchResultsElement.appendChild(li);
}
}
options.selected.onChange(() => {
const selected = options.selected.value;
const href =
selected?.kind === "explorer"
? window.location.pathname
: selected?.path.length
? `/${selected.path.join("/")}`
: null;
if (!href) return setHighlight();
for (const li of searchResultsElement.children) {
const a = li.querySelector("a");
if (a && a.getAttribute("href") === href) {
return setHighlight(/** @type {HTMLLIElement} */ (li));
}
}
setHighlight();
});
inputEvent();
searchInput.addEventListener("input", inputEvent);
const len = searchInput.value.length;
searchInput.setSelectionRange(len, len);
}
document.addEventListener("keydown", (e) => {
const el = document.activeElement;
const isTextInput =
el?.tagName === "INPUT" &&
/** @type {HTMLInputElement} */ (el).type === "text";
if (e.key === "/" && !isTextInput) {
e.preventDefault();
searchLabelElement.click();
searchInput.focus();
}
});