diff options
Diffstat (limited to 'code/app/src/lib/components')
| -rw-r--r-- | code/app/src/lib/components/checkbox.svelte | 7 | ||||
| -rw-r--r-- | code/app/src/lib/components/combobox.svelte | 57 | ||||
| -rw-r--r-- | code/app/src/lib/components/input.svelte | 3 | ||||
| -rw-r--r-- | code/app/src/lib/components/textarea.svelte | 21 |
4 files changed, 55 insertions, 33 deletions
diff --git a/code/app/src/lib/components/checkbox.svelte b/code/app/src/lib/components/checkbox.svelte index b2fcddb..12ebedb 100644 --- a/code/app/src/lib/components/checkbox.svelte +++ b/code/app/src/lib/components/checkbox.svelte @@ -7,6 +7,7 @@ export let name: string | undefined = undefined; export let disabled: boolean | null = null; export let checked: boolean; + export let required: boolean | null = null; export let _pwKey: string | undefined = undefined; </script> @@ -16,9 +17,13 @@ use:pwKey={_pwKey} {disabled} {id} + {required} type="checkbox" bind:checked class="h-4 w-4 text-teal-600 focus:ring-teal-500 border-gray-300 rounded" /> - <label for={id} class="ml-2 block text-sm text-gray-900">{label}</label> + <label for={id} class="ml-2 block text-sm text-gray-900"> + {@html required ? "<span class='text-red-500'>*</span>" : ""} + {label} + </label> </div> diff --git a/code/app/src/lib/components/combobox.svelte b/code/app/src/lib/components/combobox.svelte index ee69917..4e7b1cd 100644 --- a/code/app/src/lib/components/combobox.svelte +++ b/code/app/src/lib/components/combobox.svelte @@ -7,7 +7,7 @@ </script> <script lang="ts"> - import { CheckCircleIcon, ChevronUpDownIcon, XIcon } from "$lib/components/icons"; + import { CheckCircleIcon, ChevronUpDownIcon, XIcon } from "./icons"; import { element_has_focus, random_string } from "$lib/helpers"; import { go, highlight } from "fuzzysort"; import Badge from "./badge.svelte"; @@ -20,19 +20,19 @@ export let disabled: boolean | undefined = undefined; export let required: boolean | undefined = undefined; export let maxlength: number | undefined = undefined; - export let placeholder = $LL.combobox.search(); + export let placeholder: string = $LL.combobox.search(); export let options: Array<ComboboxOption> | undefined = []; export let createable = false; export let loading = false; export let multiple = false; - export let noResultsText = $LL.combobox.noRecordsFound(); + export let noResultsText: string = $LL.combobox.noRecordsFound(); export let on_create_async = 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 = "INTERNAL__combobox-" + random_string(3); + const INTERNAL_ID = "INTERNAL__" + id; let optionsListId = id + "--options"; let searchInputNode; @@ -75,7 +75,7 @@ if (!value) { return ""; } - return value.toString().trim().toLowerCase(); + return value.trim().toLowerCase(); }, do() { const query = search.normalise_value(searchValue); @@ -85,9 +85,9 @@ return; } - //@ts-ignore + // @ts-ignore searchResults = go(query, options, { - limit: 10, + limit: 15, allowTypo: true, threshold: -10000, key: "name", @@ -206,11 +206,11 @@ if (searchValue) { return options; } - return (options as any).sort((a, b) => { - search.normalise_value(a.name).localeCompare(search.normalise_value(b.name)); - }); + + return options.sort((a, b) => search.normalise_value(a.name).localeCompare(search.normalise_value(b.name))); }, }; + const windowEvents = { on_mousemove(event: any) { if (!event.target) return; @@ -230,7 +230,7 @@ const spacePressed = event.code === "Space"; const arrowDownPressed = event.code === "ArrowDown"; const searchInputHasFocus = element_has_focus(searchInputNode); - const focusedEntry = document.querySelector("#" + INTERNAL_ID + " ul .focus"); + const focusedEntry = document.querySelector("#" + INTERNAL_ID + " ul li.focus") as HTMLLIElement; if (showDropdown && (enterPressed || arrowDownPressed || arrowUpPressed)) { event.preventDefault(); @@ -262,16 +262,18 @@ focusedEntry.nextElementSibling.classList.add("focus"); focusedEntry.nextElementSibling.scrollIntoView(false); } else { - document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type").classList.add("focus"); - document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type").scrollIntoView(false); + const firstLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type"); + firstLIEl.classList.add("focus"); + firstLIEl.scrollIntoView(false); } } else if (arrowUpPressed) { if (focusedEntry.previousElementSibling) { focusedEntry.previousElementSibling.classList.add("focus"); focusedEntry.previousElementSibling.scrollIntoView(false); } else { - document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type").classList.add("focus"); - document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type").scrollIntoView(false); + const lastLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type"); + lastLIEl.classList.add("focus"); + lastLIEl.scrollIntoView(false); } } focusedEntry.classList.remove("focus"); @@ -279,7 +281,6 @@ } if (focusedEntry && (spacePressed || enterPressed)) { - //@ts-ignore methods.select_entry(focusedEntry.dataset.id); return; } @@ -303,14 +304,17 @@ <div id={INTERNAL_ID} class:cursor-wait={loading}> {#if label} - <label for={id} class="block text-sm font-medium text-gray-700">{label}</label> + <label for={id} class="block text-sm font-medium text-gray-700"> + {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} + </label> {/if} <div class="relative {label ? 'mt-1' : ''}"> <div on:click={search.on_input_wrapper_focus} on:keypress={search.on_input_wrapper_focus} - class="cursor-text w-full rounded-md border bg-white py-2 pl-3 pr-12 sm:text-sm - {inputHasFocus ? `border-${colorName}-500 outline-none ring-1 ring-{colorName}-500` : 'shadow-sm border-gray-300'}" + class="cursor-text w-full flex rounded-md border bg-white py-2 pl-3 pr-12 sm:text-sm + {inputHasFocus ? `border-${colorName}-500 outline-none ring-1 ring-${colorName}-500` : 'shadow-sm border-gray-300'}" > {#if multiple === true && hasSelection} <div class="flex gap-1 flex-wrap"> @@ -325,7 +329,7 @@ {/each} </div> {/if} - <div class={multiple === true && hasSelection ? "mt-2" : ""}> + <div> <input {...attributes} type="text" @@ -346,12 +350,13 @@ type="button" on:click={() => reset()} title={$LL.reset()} + tabindex="-1" class="text-gray-400 absolute cursor-pointer inset-y-0 right-0 flex items-center rounded-r-md px-2" > <XIcon /> </button> {:else} - <span class="text-gray-400 absolute inset-y-0 right-0 flex items-center rounded-r-md px-2"> + <span tabindex="-1" class="text-gray-400 absolute inset-y-0 right-0 flex items-center rounded-r-md px-2"> <ChevronUpDownIcon /> </span> {/if} @@ -406,10 +411,12 @@ </li> {/each} {:else} - <p class="px-2">{noResultsText}</p> - {#if createable && !searchValue} - <p class="px-2 text-gray-500">{$LL.combobox.createRecordHelpText()}</p> - {/if} + <slot name="no-records"> + <p class="px-2">{noResultsText}</p> + {#if createable && !searchValue} + <p class="px-2 text-gray-500">{$LL.combobox.createRecordHelpText()}</p> + {/if} + </slot> {/if} </ul> {#if showCreationHint} diff --git a/code/app/src/lib/components/input.svelte b/code/app/src/lib/components/input.svelte index efd8946..5d38597 100644 --- a/code/app/src/lib/components/input.svelte +++ b/code/app/src/lib/components/input.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import pwKey from "$actions/pwKey"; import { random_string } from "$lib/helpers"; + import { htmlLangAttributeDetector } from "typesafe-i18n/detectors"; import { ExclamationCircleIcon } from "./icons"; export let label: string | undefined = undefined; @@ -47,12 +48,14 @@ {#if label && !cornerHint && !hideLabel} <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}> {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} </label> {:else if cornerHint && !hideLabel} <div class="flex justify-between"> {#if label} <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}> {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} </label> {/if} <span class="text-sm text-gray-500"> diff --git a/code/app/src/lib/components/textarea.svelte b/code/app/src/lib/components/textarea.svelte index 65127af..6629260 100644 --- a/code/app/src/lib/components/textarea.svelte +++ b/code/app/src/lib/components/textarea.svelte @@ -9,27 +9,31 @@ export let placeholder = ""; export let value; export let label = ""; + export let required = false; export let errorText = ""; - $: shared_props = { + $: attributes = { rows: rows || null, cols: cols || null, name: name || null, id: id || null, disabled: disabled || null, + required: required || null, }; - let textarea; + let textareaElement; let scrollHeight = 0; const defaultColorClass = "border-gray-300 focus:border-teal-500 focus:ring-teal-500"; let colorClass = defaultColorClass; + $: if (errorText) { colorClass = "placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500 text-red-900 pr-10 border-red-300"; } else { colorClass = defaultColorClass; } - $: if (textarea) { - scrollHeight = textarea.scrollHeight; + + $: if (textareaElement) { + scrollHeight = textareaElement.scrollHeight; } function on_input(event) { @@ -40,17 +44,20 @@ <div> {#if label} - <label for={id} class="block text-sm font-medium text-gray-700">{label}</label> + <label for={id} class="block text-sm font-medium text-gray-700"> + {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} + </label> {/if} <div class="mt-1"> <textarea {rows} {name} {id} - {...shared_props} + {...attributes} style="overflow-y:hidden;min-height:calc(1.5em + .75rem + 2px);{scrollHeight ? 'height:{scrollHeight}px' : ''};" bind:value - bind:this={textarea} + bind:this={textareaElement} on:input={on_input} {placeholder} class="block w-full rounded-md {colorClass} shadow-sm sm:text-sm" |
