website: swap ufuzzy for quickmatch

This commit is contained in:
nym21
2026-01-22 18:32:57 +01:00
parent 3c87d36535
commit 6ef43ce7ff
10 changed files with 494 additions and 1500 deletions

View File

@@ -1,6 +1,4 @@
/**
* @import * as _ from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts"
*
* @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateLCChart, LineStyle, createSeriesMarkers as CreateSeriesMarkers, SeriesMarker, ISeriesMarkersPluginApi } from './modules/lightweight-charts/5.1.0/dist/typings.js'
*
* @import * as Brk from "./modules/brk-client/index.js"

View File

@@ -109,6 +109,7 @@ export function initOptions(brk) {
* @param {Option} option
*/
function selectOption(option) {
if (selected.value === option) return;
pushHistory(option.path);
resetParams(option);
writeToStorage(LS_SELECTED_KEY, JSON.stringify(option.path));

View File

@@ -3,7 +3,7 @@ import {
searchLabelElement,
searchResultsElement,
} from "../utils/elements.js";
import ufuzzy from "../modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs";
import { QuickMatch } from "../modules/quickmatch-js/0.3.1/src/index.js";
/**
* @param {Options} options
@@ -11,148 +11,47 @@ import ufuzzy from "../modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs";
export function initSearch(options) {
console.log("search: init");
const haystack = options.list.map((option) => option.title);
const RESULTS_PER_PAGE = 100;
/**
* @param {uFuzzy.SearchResult} searchResult
* @param {number} pageIndex
*/
function computeResultPage(searchResult, pageIndex) {
/** @type {{ option: Option, title: string }[]} */
let list = [];
let [indexes, _info, order] = searchResult || [null, null, null];
const minIndex = pageIndex * RESULTS_PER_PAGE;
if (indexes?.length) {
const maxIndex = Math.min(
(order || indexes).length - 1,
minIndex + RESULTS_PER_PAGE - 1,
);
list = Array(maxIndex - minIndex + 1);
for (let i = minIndex; i <= maxIndex; i++) {
let index = indexes[i];
const title = haystack[index];
list[i % 100] = {
option: options.list[index],
title,
};
}
}
return list;
}
/** @type {uFuzzy.Options} */
const config = {
intraIns: Infinity,
intraChars: `[a-z\d' ]`,
};
const fuzzyMultiInsert = /** @type {uFuzzy} */ (
ufuzzy({
intraIns: 1,
})
);
const fuzzyMultiInsertFuzzier = /** @type {uFuzzy} */ (ufuzzy(config));
const fuzzySingleError = /** @type {uFuzzy} */ (
ufuzzy({
intraMode: 1,
...config,
})
);
const fuzzySingleErrorFuzzier = /** @type {uFuzzy} */ (
ufuzzy({
intraMode: 1,
...config,
})
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);
function inputEvent() {
const needle = /** @type {string} */ (searchInput.value);
const needle = /** @type {string} */ (searchInput.value).trim();
searchResultsElement.scrollTo({
top: 0,
});
searchResultsElement.scrollTo({ top: 0 });
searchResultsElement.innerHTML = "";
if (!needle) {
searchResultsElement.innerHTML = "";
if (needle.length < 3) {
const li = window.document.createElement("li");
li.textContent = 'e.g. "BTC"';
li.style.color = "var(--off-color)";
searchResultsElement.appendChild(li);
return;
}
const outOfOrder = 5;
const infoThresh = 5_000;
const matches = matcher.matches(needle);
let result = fuzzyMultiInsert?.search(
haystack,
needle,
undefined,
infoThresh,
);
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsert?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
if (!matches.length) {
const li = window.document.createElement("li");
li.textContent = "No results";
li.style.color = "var(--off-color)";
searchResultsElement.appendChild(li);
return;
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleError?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
matches.forEach((title) => {
const option = titleToOption.get(title);
if (!option) return;
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleErrorFuzzier?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier?.search(
haystack,
needle,
undefined,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
searchResultsElement.innerHTML = "";
const list = computeResultPage(result, 0);
list.forEach(({ option, title }) => {
const li = window.document.createElement("li");
searchResultsElement.appendChild(li);
const element = options.createOptionElement({
option,
name: title,
name: option.title,
});
if (element) {
@@ -161,11 +60,11 @@ export function initSearch(options) {
});
}
if (searchInput.value) {
inputEvent();
}
inputEvent();
searchInput.addEventListener("input", inputEvent);
const len = searchInput.value.length;
searchInput.setSelectionRange(len, len);
}
document.addEventListener("keydown", (e) => {

View File

@@ -165,52 +165,6 @@ export function createLabeledInput({
};
}
/**
* @param {HTMLElement} parent
* @param {HTMLElement} child
* @param {number} index
*/
export function insertElementAtIndex(parent, child, index) {
if (!index) index = 0;
if (index >= parent.children.length) {
parent.appendChild(child);
} else {
parent.insertBefore(child, parent.children[index]);
}
}
/**
* @param {string} url
* @param {boolean} [targetBlank]
*/
export function open(url, targetBlank) {
console.log(`open: ${url}`);
const a = window.document.createElement("a");
window.document.body.append(a);
a.href = url;
if (targetBlank) {
a.target = "_blank";
a.rel = "noopener noreferrer";
}
a.click();
a.remove();
}
/**
* @param {string} href
*/
export function importStyle(href) {
const link = document.createElement("link");
link.href = href;
link.type = "text/css";
link.rel = "stylesheet";
link.media = "screen,print";
const head = window.document.getElementsByTagName("head")[0];
head.appendChild(link);
return link;
}
/**
* @template T
@@ -233,9 +187,6 @@ export function createRadios({
const field = window.document.createElement("div");
field.classList.add("field");
const div = window.document.createElement("div");
field.append(div);
const initialKey = toKey(initialValue);
/** @param {string} key */
@@ -245,7 +196,7 @@ export function createRadios({
if (choices.length === 1) {
const span = window.document.createElement("span");
span.textContent = toLabel(choices[0]);
div.append(span);
field.append(span);
} else {
const fieldId = id ?? "";
choices.forEach((choice) => {
@@ -261,7 +212,7 @@ export function createRadios({
const text = window.document.createTextNode(choiceLabel);
label.append(text);
div.append(label);
field.append(label);
});
field.addEventListener("change", (event) => {
@@ -297,9 +248,8 @@ export function createSelect({
? unsortedChoices.toSorted((a, b) => toLabel(a).localeCompare(toLabel(b)))
: unsortedChoices;
const select = window.document.createElement("select");
select.id = id ?? "";
select.name = id ?? "";
const field = window.document.createElement("div");
field.classList.add("field");
const initialKey = toKey(initialValue);
@@ -307,21 +257,39 @@ export function createSelect({
const fromKey = (key) =>
choices.find((c) => toKey(c) === key) ?? initialValue;
choices.forEach((choice) => {
const option = window.document.createElement("option");
option.value = toKey(choice);
option.textContent = toLabel(choice);
if (toKey(choice) === initialKey) {
option.selected = true;
if (choices.length === 1) {
const span = window.document.createElement("span");
span.textContent = toLabel(choices[0]);
field.append(span);
} else {
const select = window.document.createElement("select");
select.id = id ?? "";
select.name = id ?? "";
field.append(select);
choices.forEach((choice) => {
const option = window.document.createElement("option");
option.value = toKey(choice);
option.textContent = toLabel(choice);
if (toKey(choice) === initialKey) {
option.selected = true;
}
select.append(option);
});
select.addEventListener("change", () => {
onChange?.(fromKey(select.value));
});
const remaining = choices.length - 1;
if (remaining > 0) {
const small = window.document.createElement("small");
small.textContent = `+${remaining}`;
field.append(small);
}
select.append(option);
});
}
select.addEventListener("change", () => {
onChange?.(fromKey(select.value));
});
return select;
return field;
}
/**
@@ -361,39 +329,6 @@ export function createOption(arg) {
}
/**
* @param {Object} args
* @param {string} args.title
* @param {string} args.description
* @param {HTMLElement} args.input
*/
export function createFieldElement({ title, description, input }) {
const div = window.document.createElement("div");
const label = window.document.createElement("label");
div.append(label);
const titleElement = window.document.createElement("span");
titleElement.innerHTML = title;
label.append(titleElement);
const descriptionElement = window.document.createElement("small");
descriptionElement.innerHTML = description;
label.append(descriptionElement);
div.append(input);
const forId = input.id || input.firstElementChild?.id;
if (!forId) {
console.log(input);
throw `Input should've an ID`;
}
label.htmlFor = forId;
return div;
}
/**
* @param {'left' | 'bottom' | 'top' | 'right'} position