diff --git a/package.json b/package.json index af1cd98..f0f17d7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "@prisma/client": "^5.8.1", "svelte-floating-ui": "^1.5.8", "svelte-markdown": "^0.4.1", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zod-form-data": "^2.0.2" }, "devDependencies": { "@faker-js/faker": "^8.4.0", @@ -51,6 +52,7 @@ "prisma": "^5.8.1", "svelte": "^4.2.9", "svelte-check": "^3.6.3", + "sveltekit-superforms": "^1.13.4", "tailwindcss": "^3.4.1", "trpc-sveltekit": "^3.5.22", "tslib": "^2.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61a9fe5..376698e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: zod: specifier: ^3.22.4 version: 3.22.4 + zod-form-data: + specifier: ^2.0.2 + version: 2.0.2(zod@3.22.4) devDependencies: '@faker-js/faker': @@ -103,6 +106,9 @@ devDependencies: svelte-check: specifier: ^3.6.3 version: 3.6.3(postcss@8.4.33)(svelte@4.2.9) + sveltekit-superforms: + specifier: ^1.13.4 + version: 1.13.4(@sveltejs/kit@2.5.0)(svelte@4.2.9)(zod@3.22.4) tailwindcss: specifier: ^3.4.1 version: 3.4.1 @@ -2023,6 +2029,11 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + /klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + dev: true + /known-css-properties@0.29.0: resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} dev: true @@ -2962,6 +2973,20 @@ packages: magic-string: 0.30.5 periscopic: 3.1.0 + /sveltekit-superforms@1.13.4(@sveltejs/kit@2.5.0)(svelte@4.2.9)(zod@3.22.4): + resolution: {integrity: sha512-rM2+Ictaw7OAIorCLmvg82orci/mtO9ZouI4emtx8SyYngx9aED+eNZlHPLufgB6D7geL2a+hMSFtM3zmMQixQ==} + peerDependencies: + '@sveltejs/kit': 1.x || 2.x + svelte: 3.x || 4.x + zod: 3.x + dependencies: + '@sveltejs/kit': 2.5.0(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.9)(vite@5.0.12) + devalue: 4.3.2 + klona: 2.0.6 + svelte: 4.2.9 + zod: 3.22.4 + dev: true + /tailwindcss@3.4.1: resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} engines: {node: '>=14.0.0'} @@ -3333,5 +3358,13 @@ packages: engines: {node: '>=12.20'} dev: true + /zod-form-data@2.0.2(zod@3.22.4): + resolution: {integrity: sha512-sKTi+k0fvkxdakD0V5rq+9WVJA3cuTQUfEmNqvHrTzPLvjfLmkkBLfR0ed3qOi9MScJXTHIDH/jUNnEJ3CBX4g==} + peerDependencies: + zod: '>= 3.11.0' + dependencies: + zod: 3.22.4 + dev: false + /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} diff --git a/src/app.pcss b/src/app.pcss index 4abb4ca..05095e1 100644 --- a/src/app.pcss +++ b/src/app.pcss @@ -16,3 +16,12 @@ button { white-space: nowrap; text-overflow: ellipsis; } + +.v-form-field > input { + @apply input input-bordered w-full max-w-xs; +} + +.input[aria-invalid="true"], +.v-form-field > [aria-invalid="true"] { + @apply input-error; +} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 065efc2..3e27cfd 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -65,6 +65,7 @@ export const handle = sequence( return opt.session; }, }, + trustHost: true, }), authorization, diff --git a/src/index.test.ts b/src/index.test.ts deleted file mode 100644 index dadc7e4..0000000 --- a/src/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from "vitest"; - -describe("sum test", () => { - it("adds 1 + 2 to equal 3", () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/src/lib/components/filter/Autocomplete.svelte b/src/lib/components/filter/Autocomplete.svelte index 2f34a43..64d535f 100644 --- a/src/lib/components/filter/Autocomplete.svelte +++ b/src/lib/components/filter/Autocomplete.svelte @@ -11,26 +11,30 @@ import { shift } from "svelte-floating-ui/dom"; import outclick from "$lib/actions/outclick"; import type { BaseItem, SelectionOrText } from "./types"; - import Icon from "../ui/Icon.svelte"; + import Icon from "$lib/components/ui/Icon.svelte"; type OnSelectResult = { newValue: string; close: boolean }; export let items: BaseItem[] | (() => Promise); - export let defaultSelection: SelectionOrText | null = null; + export let selection: SelectionOrText | null = null; export let hiddenIds: Set = new Set(); export let cache: { [key: string]: BaseItem[] } = {}; export let cacheKey: string | undefined = undefined; export let placeholder: string | undefined = undefined; export let padding = true; export let cls = ""; + export let inputCls = "w-full bg-transparent"; + export let asTextInput = false; + export let idInputName: string | undefined = undefined; /** Selection callback. Returns the new input value after selection */ export let onSelect: (item: SelectionOrText, kb: boolean) => OnSelectResult = ( item, kb ) => { - return { newValue: item.name || "", close: true }; + return { newValue: item.name ?? "", close: true }; }; + export let onUnselect = () => {}; export let onClose = (kb: boolean) => {}; export let onBackspace = () => {}; @@ -40,10 +44,11 @@ let srcItems: BaseItem[]; let filteredItems: BaseItem[] = []; - $: if (hiddenIds) updateSearch(); + $: if (hiddenIds || selection) updateSearch(); // HTML elements let inputElm: HTMLInputElement | undefined; + let listElm: HTMLElement | undefined; function inputValue(): string { if (inputElm) return inputElm.value; @@ -69,7 +74,7 @@ srcItems = items; if (cacheKey) cache[cacheKey] = items; isLoading = false; - onInput(); + updateSearch(); }); return false; } @@ -77,38 +82,50 @@ return true; } - function selectDefault() { - if (defaultSelection) { - if (defaultSelection.id) { - const i = srcItems.findIndex((itm) => itm.id === defaultSelection?.id); + function markSelection() { + if (selection) { + if (selection.id) { + const i = srcItems.findIndex((itm) => itm.id === selection?.id); if (i !== -1) { highlightIndex = i; } + if (asTextInput) setInputValue(selection.name || ""); } else { - setInputValue(defaultSelection.name || ""); highlightIndex = 0; + setInputValue(selection.name || ""); } } else { highlightIndex = 0; } + highlight(); + } + + function clearSelection() { + selection = null; + onUnselect(); + setInputValue(""); + highlightIndex = 0; + updateSearch(); } function onInput() { - updateSearch(); + selection = null; + onUnselect(); opened = true; + updateSearch(); } function updateSearch() { if (loadSrcItems()) { let searchWord = inputValue().toLowerCase().trim(); filteredItems = - searchWord.length > 0 + !selection && searchWord.length > 0 ? srcItems.filter( (it) => !hiddenIds.has(it.id) && it.name.toLowerCase().includes(searchWord) ) : srcItems.filter((it) => !hiddenIds.has(it.id)); - selectDefault(); + markSelection(); } } @@ -128,6 +145,7 @@ } function selectListItem(item: BaseItem | undefined, kb: boolean) { + selection = { id: item?.id, name: item?.name }; const selRes = onSelect(item ?? { name: inputValue() }, kb); setInputValue(selRes.newValue); if (selRes.close) { @@ -147,12 +165,14 @@ open(); if (highlightIndex < filteredItems.length - 1) { highlightIndex++; + highlight(); } }, ArrowUp: () => { open(); if (highlightIndex > 0) { highlightIndex--; + highlight(); } }, Escape: () => { @@ -165,6 +185,8 @@ Backspace: () => { if (inputValue().length === 0) { onBackspace(); + } else if (selection) { + clearSelection(); } }, }; @@ -184,6 +206,35 @@ } } + function onBlur() { + if (!selection) { + if (filteredItems.length === 1) { + selectListItem(filteredItems[0], true); + } else { + setInputValue(""); + } + } + } + + function highlight() { + if (browser && opened) { + window.setTimeout(() => { + const query = ".selected"; + + const el = listElm && listElm.querySelector(query); + if (el) { + // @ts-expect-error scrollIntoViewIfNeeded is unspecified (currently only on Chrome) + if (typeof el.scrollIntoViewIfNeeded === "function") { + // @ts-expect-error scrollIntoViewIfNeeded is unspecified + el.scrollIntoViewIfNeeded(); + } else { + el.scrollIntoView({ block: "nearest" }); + } + } + }, 1); + } + } + function selectItem() { const listItem = filteredItems[highlightIndex]; selectListItem(listItem, true); @@ -198,21 +249,21 @@
{#if opened && filteredItems.length > 0} -
+
{#each filteredItems as item, i}
selectListItem(item, false)} + on:click|preventDefault={() => { + selectListItem(item, false); + }} on:keypress={(e) => { e.key == "Enter" && selectListItem(item, true); }} @@ -236,6 +289,11 @@ {/each}
{/if} + + {#if idInputName} + + + {/if}