Urara-Blog/node_modules/.pnpm-store/v3/files/c2/974659eb76cebbdfabc137f1c9f4ccc58dd5cc353e3184de624d27d9acc728324c53e31af4ede074063ac5ba7c5a9dc49adbeacc3bdb5badfa8f1f0939b0f5
2022-08-14 01:14:53 +08:00

305 lines
6.7 KiB
Text

<script>
/**
* @template TItem = string | number | Record<string, any>
*/
export let id = "typeahead-" + Math.random().toString(36);
export let value = "";
/** @type {TItem[]} */
export let data = [];
/** @type {(item: TItem) => any} */
export let extract = (item) => item;
/** @type {(item: TItem) => boolean} */
export let disable = (item) => false;
/** @type {(item: TItem) => boolean} */
export let filter = (item) => false;
/** Set to `false` to prevent the first result from being selected */
export let autoselect = true;
/**
* Set to `keep` to keep the search field unchanged after select, set to `clear` to auto-clear search field
* @type {"update" | "clear" | "keep"}
*/
export let inputAfterSelect = "update";
/** @type {{ original: TItem; index: number; score: number; string: string; disabled?: boolean; }[]} */
export let results = [];
/** Set to `true` to re-focus the input after selecting a result */
export let focusAfterSelect = false;
/**
* Specify the maximum number of results to return
* @type {number}
*/
export let limit = Infinity;
import fuzzy from "fuzzy";
import Search from "svelte-search";
import { tick, createEventDispatcher, afterUpdate } from "svelte";
const dispatch = createEventDispatcher();
let comboboxRef = null;
let searchRef = null;
let hideDropdown = false;
let selectedIndex = -1;
let prevResults = "";
afterUpdate(() => {
if (prevResults !== resultsId && autoselect) {
selectedIndex = 0;
}
if (prevResults !== resultsId && !$$slots["no-results"]) {
hideDropdown = results.length === 0;
}
prevResults = resultsId;
});
async function select() {
const result = results[selectedIndex];
if (result.disabled) return;
const selectedValue = extract(result.original);
const searchedValue = value;
if (inputAfterSelect == "clear") value = "";
if (inputAfterSelect == "update") value = selectedValue;
dispatch("select", {
selectedIndex,
searched: searchedValue,
selected: selectedValue,
original: result.original,
originalIndex: result.index,
});
await tick();
if (focusAfterSelect) searchRef.focus();
hideDropdown = true;
}
/** @type {(direction: -1 | 1) => void} */
function change(direction) {
let index =
direction === 1 && selectedIndex === results.length - 1
? 0
: selectedIndex + direction;
if (index < 0) index = results.length - 1;
let disabled = results[index].disabled;
while (disabled) {
if (index === results.length) {
index = 0;
} else {
index += direction;
}
disabled = results[index].disabled;
}
selectedIndex = index;
}
$: options = { pre: "<mark>", post: "</mark>", extract };
$: results = fuzzy
.filter(value, data, options)
.filter(({ score }) => score > 0)
.slice(0, limit)
.filter((result) => !filter(result.original))
.map((result) => ({ ...result, disabled: disable(result.original) }));
$: resultsId = results.map((result) => extract(result.original)).join("");
$: showResults = !hideDropdown && results.length > 0;
</script>
<svelte:window
on:click={({ target }) => {
if (!hideDropdown && comboboxRef && !comboboxRef.contains(target)) {
hideDropdown = true;
}
}}
/>
<div
data-svelte-typeahead
bind:this={comboboxRef}
role="combobox"
aria-haspopup="listbox"
aria-owns="{id}-listbox"
class:dropdown={results.length > 0}
aria-expanded={showResults}
id="{id}-typeahead"
>
<Search
{id}
removeFormAriaAttributes={true}
{...$$restProps}
bind:ref={searchRef}
aria-autocomplete="list"
aria-controls="{id}-listbox"
aria-labelledby="{id}-label"
aria-activedescendant={selectedIndex >= 0 &&
!hideDropdown &&
results.length > 0
? `${id}-result-${selectedIndex}`
: null}
bind:value
on:type
on:input
on:change
on:focus
on:focus={() => {
hideDropdown = false;
}}
on:clear
on:clear={() => {
hideDropdown = false;
}}
on:blur
on:keydown
on:keydown={(e) => {
if (results.length === 0) return;
switch (e.key) {
case "Enter":
select();
break;
case "ArrowDown":
e.preventDefault();
change(1);
break;
case "ArrowUp":
e.preventDefault();
change(-1);
break;
case "Escape":
e.preventDefault();
value = "";
searchRef.focus();
hideDropdown = true;
break;
}
}}
/>
<ul
class:svelte-typeahead-list={true}
role="listbox"
aria-labelledby="{id}-label"
id="{id}-listbox"
>
{#if showResults}
{#each results as result, index}
<li
role="option"
id="{id}-result-{index}"
class:selected={selectedIndex === index}
class:disabled={result.disabled}
aria-selected={selectedIndex === index}
on:click={() => {
if (result.disabled) return;
selectedIndex = index;
select();
}}
on:mouseenter={() => {
if (result.disabled) return;
selectedIndex = index;
}}
>
<slot {result} {index} {value}>
{@html result.string}
</slot>
</li>
{/each}
{/if}
{#if $$slots["no-results"] && !hideDropdown && value.length > 0 && results.length === 0}
<div class:no-results={true}>
<slot name="no-results" {value} />
</div>
{/if}
</ul>
</div>
<style>
[data-svelte-typeahead] {
position: relative;
background-color: #fff;
}
ul {
position: absolute;
top: 100%;
left: 0;
width: 100%;
padding: 0;
list-style: none;
background-color: inherit;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
[aria-expanded="true"] ul {
z-index: 1;
}
li,
.no-results {
padding: 0.25rem 1rem;
}
li {
cursor: pointer;
}
li:not(:last-of-type) {
border-bottom: 1px solid #e0e0e0;
}
li:hover {
background-color: #e5e5e5;
}
.selected {
background-color: #e5e5e5;
}
.selected:hover {
background-color: #cacaca;
}
.disabled {
opacity: 0.4;
cursor: not-allowed;
}
:global([data-svelte-search] label) {
margin-bottom: 0.25rem;
display: inline-flex;
font-size: 0.875rem;
}
:global([data-svelte-search] input) {
width: 100%;
padding: 0.5rem 0.75rem;
background: none;
font-size: 1rem;
border: 0;
border-radius: 0;
border: 1px solid #e5e5e5;
}
:global([data-svelte-search] input:focus) {
outline-color: #0f62fe;
outline-offset: 2px;
outline-width: 1px;
}
</style>