aboutsummaryrefslogtreecommitdiffstats
path: root/old-apps/web-shared/src/components/dropdown.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'old-apps/web-shared/src/components/dropdown.svelte')
-rw-r--r--old-apps/web-shared/src/components/dropdown.svelte389
1 files changed, 0 insertions, 389 deletions
diff --git a/old-apps/web-shared/src/components/dropdown.svelte b/old-apps/web-shared/src/components/dropdown.svelte
deleted file mode 100644
index a28bcd3..0000000
--- a/old-apps/web-shared/src/components/dropdown.svelte
+++ /dev/null
@@ -1,389 +0,0 @@
-<script lang="ts">
- // @ts-ignore
- import { go, highlight } from "fuzzysort";
- import { element_has_focus, random_string } from "$shared/lib/helpers";
- import Button from "$shared/components/button.svelte";
- import Chip from "$shared/components/chip.svelte";
-
- export let name;
- export let id;
- export let maxlength;
- export let placeholder = "Search";
- export let entries = [];
- export let createable = false;
- export let loading = false;
- export let multiple = false;
- export let noResultsText;
- export let errorText;
- export let label;
- export let on_create_async = ({name: string}) => {
- };
-
- export const reset = () => methods.reset();
- export const select = (id: string) => methods.select_entry(id);
- export const deselect = (id: string) => methods.deselect_entry(id);
-
- const INTERNAL_ID = "__dropdown-" + random_string(5);
-
- let searchInputNode;
- let searchResults = [];
- let searchValue = "";
- let showCreationHint = false;
- let showDropdown = false;
- let lastKeydownCode = "";
- let mouseIsOverDropdown = false;
- let mouseIsOverComponent = false;
-
- $: hasSelection = entries.some((c) => c.selected === true);
- $: if (searchValue.trim()) {
- showCreationHint = createable && entries.every((c) => search.normalise_value(c.name) !== search.normalise_value(searchValue));
- } else {
- showCreationHint = false;
- entries = methods.get_sorted_array(entries);
- }
-
- const search = {
- normalise_value(value: string): string {
- if (!value) {
- return "";
- }
- return value.toString().trim().toLowerCase();
- },
- do() {
- const query = search.normalise_value(searchValue);
- if (!query.trim()) {
- searchResults = [];
- return;
- }
-
- const options = {
- limit: 10,
- allowTypo: true,
- threshold: -10000,
- key: "name",
- };
- searchResults = go(query, entries, options);
- showDropdown = true;
- },
- on_input_focusout() {
- if (lastKeydownCode !== "Tab" && (mouseIsOverDropdown || lastKeydownCode === "ArrowDown")) {
- return;
- }
- const selected = entries.find((c) => c.selected === true);
- if (selected && !multiple) {
- searchValue = selected.name;
- }
- document.querySelector("#" + INTERNAL_ID + " ul li.focus")?.classList.remove("focus");
- showDropdown = false;
- }
- };
-
- const methods = {
- reset(focus_input = false) {
- searchValue = "";
- const copy = entries;
- for (const entry of copy) {
- entry.selected = false;
- }
- entries = methods.get_sorted_array(copy);
- if (focus_input) {
- searchInputNode?.focus();
- showDropdown = true;
- } else {
- showDropdown = false;
- }
- },
- async create_entry(name) {
- if (!name || !createable || loading) {
- console.log("Not sending creation event due to failed preconditions", {name, createable, loading});
- return;
- }
- try {
- await on_create_async({name});
- searchValue = "";
- loading = false;
- } catch (e) {
- console.error(e);
- }
- },
- select_entry(entry_id) {
- if (!entry_id || loading) {
- console.log("Not selecting entry due to failed preconditions", {
- entry_id,
- loading,
- });
- return;
- }
-
- const copy = entries;
- let selected;
- for (const entry of entries) {
- if (entry.id === entry_id) {
- entry.selected = true;
- selected = entry;
- if (multiple) {
- searchValue = "";
- } else {
- searchValue = entry.name;
- }
- } else if (!multiple) {
- entry.selected = false;
- }
- }
- entries = methods.get_sorted_array(copy);
- searchInputNode?.focus();
- searchResults = [];
- },
- deselect_entry(entry_id) {
- if (!entry_id || loading) {
- console.log("Not deselecting entry due to failed preconditions", {
- entry_id,
- loading,
- });
- return;
- }
- console.log("Deselecting entry", entry_id);
-
- const copy = entries;
- let deselected;
-
- for (const entry of copy) {
- if (entry.id === entry_id) {
- entry.selected = false;
- deselected = entry;
- }
- }
-
- entries = methods.get_sorted_array(copy);
- searchInputNode?.focus();
- },
- get_sorted_array(entries: Array<DropdownEntry>): Array<DropdownEntry> {
- if (!entries) {
- return;
- }
- if (entries.length < 1) {
- return [];
- }
- if (searchValue) {
- return entries;
- }
- return (entries as any).sort((a, b) => {
- search.normalise_value(a.name).localeCompare(search.normalise_value(b.name));
- });
- },
- };
-
- const windowEvents = {
- on_mousemove(event) {
- if (!event.target) return;
- mouseIsOverDropdown = (event.target?.closest("#" + INTERNAL_ID + " .autocomplete__results") != null ?? false);
- mouseIsOverComponent = (event.target?.closest("#" + INTERNAL_ID) != null ?? false);
- },
- on_click(event) {
- if (showDropdown && !mouseIsOverDropdown && !mouseIsOverComponent && event.target.id !== id && event.target?.htmlFor !== id) {
- showDropdown = false;
- }
- },
- on_keydown(event) {
- lastKeydownCode = event.code;
- const enterPressed = event.code === "Enter";
- const backspacePressed = event.code === "Backspace";
- const arrowUpPressed = event.code === "ArrowUp";
- const spacePressed = event.code === "Space";
- const arrowDownPressed = event.code === "ArrowDown";
- const searchInputHasFocus = element_has_focus(searchInputNode);
- const focusedEntry = document.querySelector("#" + INTERNAL_ID + " ul .focus");
-
- if (showDropdown && (enterPressed || arrowDownPressed)) {
- event.preventDefault();
- event.stopPropagation();
- }
-
- if (searchInputHasFocus && backspacePressed && !searchValue && entries.length > 0) {
- if (entries.filter(c => c.selected === true).at(-1)?.id ?? false) {
- methods.deselect_entry(entries.filter(c => c.selected === true).at(-1)?.id ?? "");
- }
- return;
- }
- if (searchInputHasFocus && enterPressed && showCreationHint) {
- methods.create_entry(searchValue.trim());
- return;
- }
-
- if (searchInputHasFocus && !focusedEntry && arrowDownPressed) {
- const firstEntry = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type");
- if (firstEntry) {
- firstEntry.classList.add("focus");
- return;
- }
- }
-
- if (focusedEntry && (arrowUpPressed || arrowDownPressed)) {
- if (arrowDownPressed) {
- if (focusedEntry.nextElementSibling) {
- focusedEntry.nextElementSibling.classList.add("focus");
- focusedEntry.nextElementSibling.scrollIntoView();
- } else {
- document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type").classList.add("focus");
- document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type").scrollIntoView();
- }
- } else if (arrowUpPressed) {
- if (focusedEntry.previousElementSibling) {
- focusedEntry.previousElementSibling.classList.add("focus");
- focusedEntry.previousElementSibling.scrollIntoView();
- } else {
- document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type").classList.add("focus");
- document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type").scrollIntoView();
- }
- }
- focusedEntry.classList.remove("focus");
- return;
- }
-
- if (focusedEntry && (spacePressed || enterPressed)) {
- methods.select_entry(focusedEntry.dataset.id);
- return;
- }
-
- if (lastKeydownCode === "Tab" && !searchInputHasFocus) {
- showDropdown = false;
- }
- },
- on_touchend(event) {
- windowEvents.on_mousemove(event);
- }
- };
-
- interface DropdownEntry {
- name: string,
- id: string,
- }
-</script>
-
-<svelte:window
- on:keydown={windowEvents.on_keydown}
- on:mousemove={windowEvents.on_mousemove}
- on:touchend={windowEvents.on_touchend}
- on:click={windowEvents.on_click}
-/>
-
-{#if label}
- <label for="{id}"
- class="form-label margin-bottom-xxs">{label}</label>
-{/if}
-
-<div class="autocomplete position-relative select-auto"
- class:cursor-wait={loading}
- class:autocomplete--results-visible={showDropdown}
- class:select-auto--selection-done={searchValue}
- id={INTERNAL_ID}
->
- <!-- input -->
- <div class="select-auto__input-wrapper form-control"
- style="cursor: text"
- on:click={() => {
- if (!element_has_focus(searchInputNode)) searchInputNode.focus();
- showDropdown = true;
- }}
- class:multiple={multiple === true}
- class:has-selection={hasSelection}>
- {#if multiple === true && hasSelection}
- {#each entries.filter((c) => c.selected === true) as entry}
- <Chip id={entry.id}
- removable={true}
- tabindex="-1"
- on:remove={() => methods.deselect_entry(entry.id)}
- text={entry.name}/>
- {/each}
- {/if}
- <input
- class="reset width-100%"
- style="outline:none;"
- type="text"
- {name}
- {id}
- {maxlength}
- {placeholder}
- bind:value={searchValue}
- bind:this={searchInputNode}
- on:input={() => search.do()}
- on:click={() => (showDropdown = true)}
- on:focus={() => (showDropdown = true)}
- on:blur={search.on_input_focusout}
- autocomplete="off"
- />
- <div class="select-auto__input-icon-wrapper">
- <!-- arrow icon -->
- <svg class="icon"
- viewBox="0 0 16 16">
- <title>Open selection</title>
- <polyline points="1 5 8 12 15 5"
- fill="none"
- stroke="currentColor"
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"/>
- </svg>
-
- <!-- close X icon -->
- <button class="reset select-auto__input-btn"
- type="button"
- on:click={() => reset(true)}>
- <svg class="icon"
- viewBox="0 0 16 16">
- <title>Reset selection</title>
- <path
- d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0Zm3.707,10.293a1,1,0,1,1-1.414,1.414L8,9.414,5.707,11.707a1,1,0,0,1-1.414-1.414L6.586,8,4.293,5.707A1,1,0,0,1,5.707,4.293L8,6.586l2.293-2.293a1,1,0,1,1,1.414,1.414L9.414,8Z"
- />
- </svg>
- </button>
- </div>
- </div>
-
- {#if errorText}
- <small class="color-error">{errorText}</small>
- {/if}
-
- <!-- dropdown -->
- <div class="autocomplete__results select-auto__results shadow-xs inner-glow">
- <ul on:keydown={(event) => event.code.startsWith("Arrow") && event.preventDefault()}
- tabindex="-1"
- class="autocomplete__list">
- {#if searchResults.length > 0}
- {#each searchResults.filter((c) => !c.selected) as result}
- <li class="select-auto__option padding-y-xs padding-x-sm"
- data-id={result.obj.id}
- on:click={(e) => methods.select_entry(e.target.dataset.id)}
- tabindex="-1">
- {@html highlight(result, (open = '<span class="font-semibold">'), (close = "</span>"))}
- </li>
- {/each}
- {:else if entries.length > 0}
- {#each entries.filter((c) => !c.selected) as entry}
- <li class="select-auto__option padding-y-xs padding-x-sm"
- data-id={entry.id}
- on:click={(e) => methods.select_entry(e.target.dataset.id)}
- tabindex="-1">
- {entry.name}
- </li>
- {/each}
- {:else}
- <li class="select-auto__option text-center padding-y-xs padding-x-sm pointer-events-none"
- tabindex="-1">
- {noResultsText}
- </li>
- {/if}
- </ul>
- {#if showCreationHint}
- <div class="width-100% border-top border-bg-lighter padding-xxxs">
- <Button variant="reset"
- type="button"
- class="width-100%"
- text="Press enter or click to create {searchValue.trim()}"
- title="Press enter or click here to create {searchValue.trim()}"
- loading={loading}
- on:click={() => methods.create_entry(searchValue.trim())}/>
- </div>
- {/if}
- </div>
-</div>