diff --git a/zenno/src/lib/CharaPick.svelte b/zenno/src/lib/CharaPick.svelte index e4a757a..7017e3e 100644 --- a/zenno/src/lib/CharaPick.svelte +++ b/zenno/src/lib/CharaPick.svelte @@ -1,24 +1,141 @@ - +
+ popover?.togglePopover()} + {onkeydown} + tabindex="0" + >{value?.name ?? ''} +
+ +
+ {#if !required} +
{value = undefined; search = ''; popover!.hidePopover()}} + onfocus={() => value = undefined} + {onkeydown} + > + Reset +
+ {/if} + {#each searchedCharas as c (c.chara_id)} +
{value = c; popover!.hidePopover()}} + onfocus={() => value = c} + {onkeydown} + > + {#if option != null} + {@render option(c)} + {:else} + {c.name} + {/if} +
+ {:else} +
No matches.
+ {/each} +
+
+
diff --git a/zenno/src/lib/stringsearch.ts b/zenno/src/lib/stringsearch.ts new file mode 100644 index 0000000..f390ff3 --- /dev/null +++ b/zenno/src/lib/stringsearch.ts @@ -0,0 +1,56 @@ +const WORD_BOUNDARY = " ,!?/-+();#○☆♡'=♪∀゚∴"; + +function score(s: string, tt: string): number { + let k: number | undefined; + let r = 0; + let run = 0; + for (const c of s) { + const j = tt.indexOf(c, k); + // If the character isn't in the string, there's a major penalty. + if (j < 0) { + // The penalty scales with run length, on the assumption that we're + // typing something else. + // Really this should scale with the longest current run among all + // search terms, but that's infeasible to implement. + r -= 6 + run*run; + run = 0; + continue; + } + run++; + // Characters at word boundaries get extra score. + if (j == 0 || WORD_BOUNDARY.includes(tt[j-1])) { + r += 2; + } + // As do characters that *are* word boundaries. + if (WORD_BOUNDARY.includes(c)) { + r += 2; + } + // And runs of matches scale with run length. + if (j === k) { + r += (run+1) * (run+1); + } else { + run = 0; + } + k = j + 1; + } + return r; +} + +/** + * Fuzzy string search. + * @param sub Substring to search for + * @param terms Iterable of values to search among + * @param map Mapping from term to string to search + * @returns Matching terms in decreasing match quality order + */ +export function stringsearch(sub: string, terms: Iterable, map: (t: T) => string): T[] { + const s = sub.toLocaleLowerCase(); + const scored: [T, number][] = []; + for (const t of terms) { + const sc = score(s, map(t).toLocaleLowerCase()); + if (sc >= 0) { + scored.push([t, sc]); + } + } + return scored.sort(([, a], [, b]) => b - a).map(([t,]) => t); +} diff --git a/zenno/src/routes/chara/affinity/+page.svelte b/zenno/src/routes/chara/affinity/+page.svelte index c6f7618..07c1208 100644 --- a/zenno/src/routes/chara/affinity/+page.svelte +++ b/zenno/src/routes/chara/affinity/+page.svelte @@ -1,7 +1,7 @@