fuma 2.1.5 → 2.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_doc/data.d.ts +3 -1
- package/dist/_doc/data.js +6 -2
- package/dist/command/index.d.ts +1 -1
- package/dist/command/index.js +1 -1
- package/dist/command/{command.svelte.js → useCommand.svelte.js} +1 -1
- package/dist/input/InputBoolean.svelte +25 -7
- package/dist/input/InputBoolean.svelte.d.ts +4 -0
- package/dist/input/InputCheckboxes.svelte +116 -0
- package/dist/input/InputCheckboxes.svelte.d.ts +16 -0
- package/dist/input/InputDateTime.svelte +194 -0
- package/dist/input/InputDateTime.svelte.d.ts +23 -0
- package/dist/input/InputMultiSelect.svelte +286 -0
- package/dist/input/{InputRelation.svelte.d.ts → InputMultiSelect.svelte.d.ts} +27 -13
- package/dist/input/InputNumber.svelte +36 -6
- package/dist/input/InputNumber.svelte.d.ts +5 -0
- package/dist/input/InputRadio.svelte +54 -23
- package/dist/input/InputRange.svelte +19 -4
- package/dist/input/InputRange.svelte.d.ts +6 -0
- package/dist/input/InputSelect.svelte +204 -62
- package/dist/input/InputSelect.svelte.d.ts +87 -6
- package/dist/input/InputSelectNative.svelte +44 -19
- package/dist/input/InputSelectNative.svelte.d.ts +5 -0
- package/dist/input/InputString.svelte +36 -6
- package/dist/input/InputString.svelte.d.ts +5 -0
- package/dist/input/InputTextarea.svelte +41 -12
- package/dist/input/InputTextarea.svelte.d.ts +5 -0
- package/dist/input/index.d.ts +4 -1
- package/dist/input/index.js +4 -1
- package/dist/input/useItems.svelte.d.ts +38 -0
- package/dist/input/useItems.svelte.js +142 -0
- package/dist/loading/Loading.svelte +3 -2
- package/dist/loading/Loading.svelte.d.ts +3 -2
- package/dist/popover/Popover.svelte +1 -2
- package/dist/popover/Popover.svelte.d.ts +13 -0
- package/dist/popover/index.d.ts +1 -1
- package/dist/popover/index.js +1 -1
- package/dist/popover/{popover.svelte.d.ts → usePopover.svelte.d.ts} +2 -1
- package/dist/popover/usePopover.svelte.js +145 -0
- package/dist/remote/useForm.d.ts +4 -5
- package/dist/remote/useForm.js +9 -4
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/ui/button/ButtonDelete.svelte +4 -2
- package/dist/ui/dialog/Dialog.svelte +5 -5
- package/dist/ui/drawer/Drawer.svelte +74 -26
- package/dist/ui/drawer/Drawer.svelte.d.ts +10 -4
- package/dist/ui/drawer/index.d.ts +1 -0
- package/dist/ui/drawer/index.js +1 -0
- package/dist/ui/drawer/inertBackground.d.ts +13 -0
- package/dist/ui/drawer/inertBackground.js +59 -0
- package/dist/ui/drawer/useLayer.svelte.d.ts +1 -1
- package/dist/ui/drawer/useLayer.svelte.js +11 -7
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/menu/ContextMenu.svelte +30 -17
- package/dist/ui/menu/ContextMenu.svelte.d.ts +2 -3
- package/dist/ui/menu/DropDown.svelte +1 -1
- package/dist/ui/menu/DropDown.svelte.d.ts +1 -1
- package/dist/ui/menu/DropDownMenu.svelte +12 -10
- package/dist/ui/menu/DropDownMenu.svelte.d.ts +2 -3
- package/dist/ui/menu/index.d.ts +0 -1
- package/dist/ui/menu/index.js +0 -2
- package/dist/ui/range/RangePicker.svelte +25 -13
- package/dist/ui/range/RangePicker.svelte.d.ts +9 -25
- package/dist/ui/range/RangePickerButton.svelte +49 -32
- package/dist/ui/range/RangePickerButton.svelte.d.ts +1 -1
- package/dist/ui/table/Table.svelte +7 -10
- package/dist/ui/table/Table.svelte.d.ts +1 -1
- package/dist/ui/table/TableFieldsEdition.svelte +24 -22
- package/dist/ui/table/TableHead.svelte +5 -1
- package/dist/ui/table/TableViewSelect.svelte +52 -47
- package/dist/ui/table/cell/TableCellBoolean.svelte +2 -2
- package/dist/ui/table/head/TableHeadDate.svelte +62 -55
- package/dist/ui/table/head/TableHeadNumber.svelte +55 -19
- package/dist/ui/table/head/TableHeadSelect.svelte +30 -27
- package/dist/ui/table/head/TableHeadString.svelte +11 -7
- package/dist/ui/table/index.d.ts +1 -0
- package/dist/ui/table/index.js +1 -0
- package/package.json +7 -4
- package/dist/input/InputRelation.svelte +0 -200
- package/dist/popover/popover.svelte.js +0 -106
- /package/dist/command/{command.svelte.d.ts → useCommand.svelte.d.ts} +0 -0
- /package/dist/state/{param.svelte.d.ts → param.d.ts} +0 -0
- /package/dist/state/{param.svelte.js → param.js} +0 -0
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
import z from 'zod'
|
|
5
5
|
import { goto } from '$app/navigation'
|
|
6
6
|
import { page } from '$app/state'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { Popover } from '../../../popover/index.js'
|
|
8
|
+
import { urlParam } from '../../../state/param.js'
|
|
9
9
|
import { zodCoerceJsonRecord } from '../../../validation/zod.js'
|
|
10
10
|
import type { ItemBase, TableField } from '../field.js'
|
|
11
11
|
import OrderButtons from './OrderButtons.svelte'
|
|
12
12
|
|
|
13
13
|
let { field }: { field: TableField<Item> } = $props()
|
|
14
14
|
|
|
15
|
+
// `.catch`: le paramètre vient de l'URL, donc de l'utilisateur. Un contenu illisible retombe
|
|
16
|
+
// sur un filtre vide au lieu de faire échouer le rendu de toute la table.
|
|
15
17
|
const paramModel = zodCoerceJsonRecord
|
|
16
18
|
.pipe(
|
|
17
19
|
z.object({
|
|
@@ -21,14 +23,24 @@
|
|
|
21
23
|
})
|
|
22
24
|
)
|
|
23
25
|
.default({})
|
|
26
|
+
.catch({})
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
// `?? undefined`: `searchParams.get` rend `null` quand le filtre est absent, et un
|
|
29
|
+
// `.default()` de zod ne couvre que `undefined`.
|
|
30
|
+
let { min, max, order } = $derived(
|
|
31
|
+
paramModel.parse(page.url.searchParams.get(field.key) ?? undefined)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/** Un `<input type="number">` vidé lie `null`: une borne effacée, pas une borne à zéro. */
|
|
35
|
+
function isSet(bound: number | null | undefined): bound is number {
|
|
36
|
+
return bound !== null && bound !== undefined
|
|
37
|
+
}
|
|
26
38
|
|
|
27
39
|
const updateUrl = debounce(() => {
|
|
28
40
|
const query: Record<string, string | number> = {}
|
|
29
41
|
if (order) query.order = order
|
|
30
|
-
if (min
|
|
31
|
-
if (max
|
|
42
|
+
if (isSet(min)) query.min = min
|
|
43
|
+
if (isSet(max)) query.max = max
|
|
32
44
|
if (!Object.keys(query).length) return resetFilter()
|
|
33
45
|
goto(urlParam.with({ [field.key]: JSON.stringify(query) }, 'skip', 'take'), {
|
|
34
46
|
noScroll: true,
|
|
@@ -37,6 +49,8 @@
|
|
|
37
49
|
}, 250)
|
|
38
50
|
|
|
39
51
|
function resetFilter() {
|
|
52
|
+
// Une frappe encore en attente réécrirait le filtre qu'on vient d'effacer.
|
|
53
|
+
updateUrl.clear()
|
|
40
54
|
return goto(urlParam.without(field.key, 'skip', 'take'), {
|
|
41
55
|
replaceState: true,
|
|
42
56
|
noScroll: true,
|
|
@@ -46,23 +60,23 @@
|
|
|
46
60
|
</script>
|
|
47
61
|
|
|
48
62
|
<th class="p-1">
|
|
49
|
-
<
|
|
50
|
-
{#snippet
|
|
51
|
-
<button class="menu-item min-h-8 w-full flex-wrap gap-y-1">
|
|
63
|
+
<Popover class="p-1">
|
|
64
|
+
{#snippet trigger(popover)}
|
|
65
|
+
<button class="menu-item min-h-8 w-full flex-wrap gap-y-1" {...popover.trigger}>
|
|
52
66
|
<div class="flex gap-2">
|
|
53
67
|
<span>{field.label}</span>
|
|
54
|
-
{#if min
|
|
68
|
+
{#if !isSet(min) && !isSet(max)}
|
|
55
69
|
<FunnelIcon size={15} class="opacity-50" />
|
|
56
70
|
{/if}
|
|
57
71
|
</div>
|
|
58
72
|
|
|
59
|
-
{#if min
|
|
73
|
+
{#if isSet(min) || isSet(max)}
|
|
60
74
|
<span class="badge badge-xs text-[0.7rem] font-normal text-white badge-primary">
|
|
61
|
-
{#if min
|
|
75
|
+
{#if isSet(min)}
|
|
62
76
|
{min} ≤
|
|
63
77
|
{/if}
|
|
64
78
|
x
|
|
65
|
-
{#if max
|
|
79
|
+
{#if isSet(max)}
|
|
66
80
|
≤ {max}
|
|
67
81
|
{/if}
|
|
68
82
|
</span>
|
|
@@ -75,13 +89,13 @@
|
|
|
75
89
|
</button>
|
|
76
90
|
{/snippet}
|
|
77
91
|
|
|
78
|
-
{#snippet children(
|
|
92
|
+
{#snippet children(popover)}
|
|
79
93
|
{#if field.sortable !== false}
|
|
80
94
|
<OrderButtons
|
|
81
95
|
bind:order
|
|
82
96
|
onChange={() => {
|
|
83
97
|
updateUrl()
|
|
84
|
-
|
|
98
|
+
popover.hide()
|
|
85
99
|
}}
|
|
86
100
|
/>
|
|
87
101
|
<div class="divider"></div>
|
|
@@ -91,11 +105,33 @@
|
|
|
91
105
|
class="grid grid-cols-2 gap-2 p-1"
|
|
92
106
|
onsubmit={(e) => {
|
|
93
107
|
e.preventDefault()
|
|
94
|
-
|
|
108
|
+
// Fermer sans purger la temporisation perdrait la dernière frappe.
|
|
109
|
+
updateUrl.flush()
|
|
110
|
+
popover.hide()
|
|
95
111
|
}}
|
|
96
112
|
>
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
<!-- `type="number"`: sans lui `bind:value` lie une chaîne, que ni le paramètre
|
|
114
|
+
d'URL ni le filtre serveur ne savent relire. -->
|
|
115
|
+
<!-- `autofocus`: le popover natif donne le focus à cet élément à l'ouverture. -->
|
|
116
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
117
|
+
<input
|
|
118
|
+
bind:value={min}
|
|
119
|
+
oninput={updateUrl}
|
|
120
|
+
type="number"
|
|
121
|
+
class="input w-full"
|
|
122
|
+
placeholder="Min"
|
|
123
|
+
aria-label="Minimum"
|
|
124
|
+
autofocus
|
|
125
|
+
onfocus={(e) => e.currentTarget.select()}
|
|
126
|
+
/>
|
|
127
|
+
<input
|
|
128
|
+
bind:value={max}
|
|
129
|
+
oninput={updateUrl}
|
|
130
|
+
type="number"
|
|
131
|
+
class="input w-full"
|
|
132
|
+
placeholder="Max"
|
|
133
|
+
aria-label="Maximum"
|
|
134
|
+
/>
|
|
99
135
|
|
|
100
136
|
<div class="col-span-full flex justify-end gap-2">
|
|
101
137
|
<button
|
|
@@ -104,7 +140,7 @@
|
|
|
104
140
|
onclick={() => {
|
|
105
141
|
min = undefined
|
|
106
142
|
max = undefined
|
|
107
|
-
|
|
143
|
+
popover.hide()
|
|
108
144
|
resetFilter()
|
|
109
145
|
}}
|
|
110
146
|
>
|
|
@@ -114,5 +150,5 @@
|
|
|
114
150
|
</div>
|
|
115
151
|
</form>
|
|
116
152
|
{/snippet}
|
|
117
|
-
</
|
|
153
|
+
</Popover>
|
|
118
154
|
</th>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts" generics="Item extends ItemBase">
|
|
2
2
|
import { ListFilterIcon } from '@lucide/svelte'
|
|
3
3
|
import { page } from '$app/state'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { Popover } from '../../../popover/index.js'
|
|
5
|
+
import { param, urlParam } from '../../../state/param.js'
|
|
6
6
|
import type { ItemBase, TableField } from '../index.js'
|
|
7
7
|
import { jsonParse } from '../../../utils/jsonParse.js'
|
|
8
8
|
import { type Options, parseOptions } from '../../../utils/options.js'
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
</script>
|
|
50
50
|
|
|
51
51
|
<th class="p-1">
|
|
52
|
-
<
|
|
53
|
-
{#snippet
|
|
54
|
-
<button class="menu-item min-h-8 w-full flex-wrap gap-y-1">
|
|
55
|
-
<div class="flex gap-2">
|
|
52
|
+
<Popover class="p-1">
|
|
53
|
+
{#snippet trigger(popover)}
|
|
54
|
+
<button class="menu-item min-h-8 w-full flex-wrap gap-y-1" {...popover.trigger}>
|
|
55
|
+
<div class="flex items-center gap-2">
|
|
56
56
|
{field.label}
|
|
57
57
|
{#if !optionsActive.length}
|
|
58
58
|
<ListFilterIcon size={15} class="opacity-50" />
|
|
@@ -74,25 +74,28 @@
|
|
|
74
74
|
</button>
|
|
75
75
|
{/snippet}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
77
|
+
{#snippet children(popover)}
|
|
78
|
+
<div class="flex flex-col gap-1">
|
|
79
|
+
{#each options as { isActive, icon: Icon, label, value } (value)}
|
|
80
|
+
<a
|
|
81
|
+
href={getHref(value)}
|
|
82
|
+
class="menu-item px-3 py-2"
|
|
83
|
+
class:active={isActive}
|
|
84
|
+
data-sveltekit-noscroll
|
|
85
|
+
data-sveltekit-replacestate
|
|
86
|
+
onclick={multiSelect ? undefined : popover.hide}
|
|
87
|
+
>
|
|
88
|
+
{#if Icon}
|
|
89
|
+
<Icon size={18} class="opacity-60" />
|
|
90
|
+
{/if}
|
|
91
|
+
<span class="font-normal whitespace-nowrap">{label}</span>
|
|
92
|
+
</a>
|
|
93
|
+
{:else}
|
|
94
|
+
<div class="menu-item disabled px-10">
|
|
95
|
+
<span class="opacity-40">{placeholder}</span>
|
|
96
|
+
</div>
|
|
97
|
+
{/each}
|
|
98
|
+
</div>
|
|
99
|
+
{/snippet}
|
|
100
|
+
</Popover>
|
|
98
101
|
</th>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" generics="Item extends ItemBase">
|
|
2
2
|
import { SearchIcon } from '@lucide/svelte'
|
|
3
3
|
import { page } from '$app/state'
|
|
4
|
-
import {
|
|
4
|
+
import { Popover } from '../../../popover/index.js'
|
|
5
5
|
import type { ItemBase, TableField } from '../index.js'
|
|
6
6
|
|
|
7
7
|
let { field }: { field: TableField<Item> } = $props()
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<th class="p-1">
|
|
13
|
-
<
|
|
14
|
-
{#snippet
|
|
15
|
-
<button class="menu-item min-h-8 w-full flex-wrap gap-y-1">
|
|
13
|
+
<Popover class="p-1">
|
|
14
|
+
{#snippet trigger(popover)}
|
|
15
|
+
<button class="menu-item min-h-8 w-full flex-wrap gap-y-1" {...popover.trigger}>
|
|
16
16
|
<div class="flex items-center gap-2">
|
|
17
17
|
<span>{field.label}</span>
|
|
18
18
|
{#if !searchValue}
|
|
@@ -29,14 +29,18 @@
|
|
|
29
29
|
</button>
|
|
30
30
|
{/snippet}
|
|
31
31
|
|
|
32
|
-
{#snippet children(
|
|
32
|
+
{#snippet children(popover)}
|
|
33
|
+
<!-- `autofocus`: le popover natif donne le focus à cet élément à l'ouverture. -->
|
|
34
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
33
35
|
<input
|
|
34
36
|
name={field.key}
|
|
35
37
|
type="search"
|
|
36
38
|
class="m-1"
|
|
37
39
|
value={searchValue}
|
|
38
|
-
|
|
40
|
+
autofocus
|
|
41
|
+
onfocus={(e) => e.currentTarget.select()}
|
|
42
|
+
onkeydown={(e) => e.key === 'Enter' && popover.hide()}
|
|
39
43
|
/>
|
|
40
44
|
{/snippet}
|
|
41
|
-
</
|
|
45
|
+
</Popover>
|
|
42
46
|
</th>
|
package/dist/ui/table/index.d.ts
CHANGED
package/dist/ui/table/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fuma",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.7",
|
|
4
4
|
"description": "My fullstack material build with sveltekit, daisyui, zod, prisma, lucia",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonas Voisard",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build": "vite build && bun run package",
|
|
12
12
|
"preview": "vite preview --host",
|
|
13
13
|
"package": "svelte-kit sync && svelte-package && publint",
|
|
14
|
+
"package:watch": "svelte-package --watch",
|
|
14
15
|
"prepublishOnly": "bun run package",
|
|
15
16
|
"sync": "svelte-kit sync",
|
|
16
17
|
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
|
|
@@ -44,7 +45,9 @@
|
|
|
44
45
|
"!dist/**/*.spec.*"
|
|
45
46
|
],
|
|
46
47
|
"peerDependencies": {
|
|
48
|
+
"@sveltejs/kit": "^2.70.2",
|
|
47
49
|
"daisyui": "^5.5.17",
|
|
50
|
+
"svelte": "^5.55.5",
|
|
48
51
|
"tailwindcss": "^4.1.18",
|
|
49
52
|
"zod": "^4.3.6"
|
|
50
53
|
},
|
|
@@ -52,12 +55,12 @@
|
|
|
52
55
|
"@biomejs/biome": "2.4.2",
|
|
53
56
|
"@eslint/compat": "^2.0.5",
|
|
54
57
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
55
|
-
"@sveltejs/kit": "^2.
|
|
58
|
+
"@sveltejs/kit": "^2.70.2",
|
|
56
59
|
"@sveltejs/package": "^2.5.7",
|
|
57
60
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
58
61
|
"@tailwindcss/vite": "^4.2.4",
|
|
59
62
|
"@types/node": "^25.6.0",
|
|
60
|
-
"daisyui": "^5.
|
|
63
|
+
"daisyui": "^5.7.4",
|
|
61
64
|
"eslint": "^9.39.4",
|
|
62
65
|
"eslint-config-prettier": "^10.1.8",
|
|
63
66
|
"eslint-plugin-svelte": "^3.17.1",
|
|
@@ -67,6 +70,7 @@
|
|
|
67
70
|
"prettier-plugin-tailwindcss": "^0.7.4",
|
|
68
71
|
"publint": "^0.1.16",
|
|
69
72
|
"shiki": "^4.0.2",
|
|
73
|
+
"svelte": "^5.55.5",
|
|
70
74
|
"svelte-check": "^4.4.6",
|
|
71
75
|
"tailwindcss": "^4.2.4",
|
|
72
76
|
"typescript": "^5.9.3",
|
|
@@ -86,7 +90,6 @@
|
|
|
86
90
|
"litepicker": "^2.0.12",
|
|
87
91
|
"perod": "^1.4.1",
|
|
88
92
|
"runed": "^0.37.1",
|
|
89
|
-
"svelte": "^5.55.5",
|
|
90
93
|
"svelte-easy-crop": "^5.0.1",
|
|
91
94
|
"svelte-sonner": "^1.1.1",
|
|
92
95
|
"tippy.js": "^6.3.7",
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
<script lang="ts" generics="Item">
|
|
2
|
-
import { ChevronsUpDownIcon, SearchIcon, XIcon } from '@lucide/svelte'
|
|
3
|
-
import type { RemoteFormField, RemoteQueryFunction } from '@sveltejs/kit'
|
|
4
|
-
import type { Snippet } from 'svelte'
|
|
5
|
-
import type { ClassValue } from 'svelte/elements'
|
|
6
|
-
import { tip } from '../action/tip.js'
|
|
7
|
-
import { useCommand } from '../command/command.svelte.js'
|
|
8
|
-
import Loading from '../loading/Loading.svelte'
|
|
9
|
-
import { type PopoverType, usePopover } from '../popover/popover.svelte.js'
|
|
10
|
-
import Issues from './Issues.svelte'
|
|
11
|
-
|
|
12
|
-
let {
|
|
13
|
-
label,
|
|
14
|
-
searchItems,
|
|
15
|
-
getValue = defaultGetValue,
|
|
16
|
-
selected = defaultSnippet,
|
|
17
|
-
proposal = defaultSnippet,
|
|
18
|
-
field,
|
|
19
|
-
value = $bindable(),
|
|
20
|
-
placeholder = 'Selectionner une valeur',
|
|
21
|
-
class: klass,
|
|
22
|
-
nullable,
|
|
23
|
-
hint,
|
|
24
|
-
onSelect,
|
|
25
|
-
hotKey
|
|
26
|
-
}: {
|
|
27
|
-
searchItems: RemoteQueryFunction<{ search: string }, Item[]>
|
|
28
|
-
label?: string
|
|
29
|
-
getValue?: (item: NoInfer<Item>) => string
|
|
30
|
-
selected?: Snippet<[Item]>
|
|
31
|
-
proposal?: Snippet<[Item, { isSelected: boolean; isFocus: boolean }]>
|
|
32
|
-
field?: RemoteFormField<string>
|
|
33
|
-
value?: string
|
|
34
|
-
placeholder?: string
|
|
35
|
-
class?: ClassValue
|
|
36
|
-
nullable?: boolean
|
|
37
|
-
hint?: Snippet<[Item | undefined]>
|
|
38
|
-
onSelect?: (item: NoInfer<Item> | undefined, popover: PopoverType) => void
|
|
39
|
-
hotKey?: string
|
|
40
|
-
} = $props()
|
|
41
|
-
|
|
42
|
-
let search = $state('')
|
|
43
|
-
const query = $derived.by(() => searchItems({ search }))
|
|
44
|
-
const items = $derived(query.current || [])
|
|
45
|
-
|
|
46
|
-
let selectedItem = $state<Item | undefined>(undefined)
|
|
47
|
-
$effect(() => {
|
|
48
|
-
const targetValue = (field?.value() ?? value)?.toString()
|
|
49
|
-
if (!targetValue) {
|
|
50
|
-
selectedItem = undefined
|
|
51
|
-
command.selectedIndex = -1
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
// TODO: Quelle est la meilleur facon de récupérer l'item initial ?
|
|
55
|
-
command.selectedIndex = items.findIndex((item) => getValue(item) === targetValue)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
export const popover = usePopover({
|
|
59
|
-
listenFocus: false,
|
|
60
|
-
onShow: () => command.focusTrigger(),
|
|
61
|
-
hotKey: (() => hotKey)()
|
|
62
|
-
})
|
|
63
|
-
export const command = useCommand({
|
|
64
|
-
isEnable: () => popover.isOpen,
|
|
65
|
-
onSelect: (index) => {
|
|
66
|
-
popover.hide()
|
|
67
|
-
const item = items[index]
|
|
68
|
-
onSelect?.(item, popover)
|
|
69
|
-
if (!item) return
|
|
70
|
-
selectedItem = item
|
|
71
|
-
setValue(item)
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
function setValue(item: Item | undefined) {
|
|
76
|
-
const newValue = item && getValue(item)
|
|
77
|
-
if (field) {
|
|
78
|
-
field.set(newValue)
|
|
79
|
-
} else {
|
|
80
|
-
value = newValue
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function defaultGetValue(item: Item): string {
|
|
85
|
-
if (item && typeof item === 'object') {
|
|
86
|
-
if ('value' in item && typeof item.value === 'string') return item.value
|
|
87
|
-
if ('rowid' in item && typeof item.rowid === 'string') return item.rowid
|
|
88
|
-
}
|
|
89
|
-
return JSON.stringify(item)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getDefaultLabel(item: Item) {
|
|
93
|
-
if (item && typeof item === 'object' && 'label' in item) {
|
|
94
|
-
if (typeof item.label === 'string') return item.label
|
|
95
|
-
}
|
|
96
|
-
return getValue(item)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const isButtonSetNullVisible = $derived(nullable && (field?.value() || value))
|
|
100
|
-
</script>
|
|
101
|
-
|
|
102
|
-
{#snippet defaultSnippet(item: Item)}
|
|
103
|
-
<span>{getDefaultLabel(item)}</span>
|
|
104
|
-
{/snippet}
|
|
105
|
-
|
|
106
|
-
{#snippet triggerButton()}
|
|
107
|
-
<button
|
|
108
|
-
type="button"
|
|
109
|
-
class={['input', field?.issues.length && 'input-error', klass]}
|
|
110
|
-
{...popover.trigger}
|
|
111
|
-
>
|
|
112
|
-
<div class="grow text-left">
|
|
113
|
-
{#if !selectedItem}
|
|
114
|
-
<span class="opacity-60">{placeholder}</span>
|
|
115
|
-
{:else}
|
|
116
|
-
<!-- item.icon not rerender if not wrapped in #key -->
|
|
117
|
-
{#key selectedItem}{@render selected(selectedItem)}{/key}
|
|
118
|
-
{/if}
|
|
119
|
-
</div>
|
|
120
|
-
<ChevronsUpDownIcon size={14} opacity={0.7} />
|
|
121
|
-
</button>
|
|
122
|
-
{/snippet}
|
|
123
|
-
|
|
124
|
-
{#if field}
|
|
125
|
-
<input {...field.as('hidden', field.value() || '')} />
|
|
126
|
-
{/if}
|
|
127
|
-
|
|
128
|
-
<div>
|
|
129
|
-
{#if label}
|
|
130
|
-
<label class="floating-label">
|
|
131
|
-
<span>{label}</span>
|
|
132
|
-
{@render triggerButton()}
|
|
133
|
-
</label>
|
|
134
|
-
{:else}
|
|
135
|
-
{@render triggerButton()}
|
|
136
|
-
{/if}
|
|
137
|
-
|
|
138
|
-
<div
|
|
139
|
-
{...popover.content}
|
|
140
|
-
class={['my-2 rounded-box border bg-base-100 shadow-xl']}
|
|
141
|
-
style="min-width: anchor-size(width);"
|
|
142
|
-
tabindex="-1"
|
|
143
|
-
>
|
|
144
|
-
<div class="sticky top-0 z-10 flex gap-2 bg-base-100/10 p-2 backdrop-blur-md">
|
|
145
|
-
<label class="input input-sm grow input-ghost">
|
|
146
|
-
<SearchIcon opacity={0.6} size={20} />
|
|
147
|
-
<input
|
|
148
|
-
type="search"
|
|
149
|
-
placeholder="Recherche"
|
|
150
|
-
autocomplete="off"
|
|
151
|
-
bind:value={search}
|
|
152
|
-
{...command.trigger}
|
|
153
|
-
/>
|
|
154
|
-
</label>
|
|
155
|
-
{#if isButtonSetNullVisible}
|
|
156
|
-
<button
|
|
157
|
-
class="btn btn-square btn-soft btn-sm"
|
|
158
|
-
type="button"
|
|
159
|
-
onclick={() => {
|
|
160
|
-
selectedItem = undefined
|
|
161
|
-
setValue(undefined)
|
|
162
|
-
onSelect?.(undefined, popover)
|
|
163
|
-
popover.hide()
|
|
164
|
-
}}
|
|
165
|
-
use:tip={{ content: 'Pas de sélection' }}
|
|
166
|
-
>
|
|
167
|
-
<XIcon />
|
|
168
|
-
</button>
|
|
169
|
-
{/if}
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
<ul class="menu max-h-80 w-full flex-nowrap pt-0">
|
|
173
|
-
{#each items as item, index (item)}
|
|
174
|
-
{@const isSelected = index === command.selectedIndex}
|
|
175
|
-
{@const isFocus = index === command.focusIndex}
|
|
176
|
-
<li>
|
|
177
|
-
<button
|
|
178
|
-
{...command.item(index, '')}
|
|
179
|
-
class={[isSelected && 'border', isFocus && 'menu-focus', 'scroll-mt-13 scroll-mb-2']}
|
|
180
|
-
type="button"
|
|
181
|
-
tabindex="-1"
|
|
182
|
-
role="option"
|
|
183
|
-
>
|
|
184
|
-
{@render proposal(item, { isSelected, isFocus })}
|
|
185
|
-
</button>
|
|
186
|
-
</li>
|
|
187
|
-
{/each}
|
|
188
|
-
</ul>
|
|
189
|
-
|
|
190
|
-
<Loading {query} />
|
|
191
|
-
</div>
|
|
192
|
-
<Issues {field} />
|
|
193
|
-
{@render hint?.(selectedItem)}
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
<style>
|
|
197
|
-
div[popover] {
|
|
198
|
-
border-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
|
199
|
-
}
|
|
200
|
-
</style>
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import debounce from 'debounce';
|
|
2
|
-
import { createAttachmentKey } from 'svelte/attachments';
|
|
3
|
-
import { on } from 'svelte/events';
|
|
4
|
-
const placements = {
|
|
5
|
-
top: 'top',
|
|
6
|
-
'top-end': 'top span-left',
|
|
7
|
-
'top-start': 'top span-right',
|
|
8
|
-
bottom: 'bottom',
|
|
9
|
-
'bottom-end': 'bottom span-left',
|
|
10
|
-
'bottom-start': 'bottom span-right',
|
|
11
|
-
left: 'left',
|
|
12
|
-
'left-end': 'left span-top',
|
|
13
|
-
'left-start': 'left span-bottom',
|
|
14
|
-
right: 'right',
|
|
15
|
-
'right-end': 'right span-top',
|
|
16
|
-
'right-start': 'right span-bottom'
|
|
17
|
-
};
|
|
18
|
-
let popoverInstanceCount = 0;
|
|
19
|
-
export function usePopover({ mode = 'auto', placement = 'bottom-start', listenClick = true, listenFocus = true, listenHover = false, hideDelay = 400, onShow, onHide, hotKey } = {}) {
|
|
20
|
-
const uid = popoverInstanceCount++;
|
|
21
|
-
const anchorName = `--anchor-${uid}`;
|
|
22
|
-
let popover = $state();
|
|
23
|
-
let isOpen = $state(!!popover?.matches(':popover-open'));
|
|
24
|
-
const hideDebounced = debounce(hide, hideDelay);
|
|
25
|
-
function show() {
|
|
26
|
-
popover?.showPopover();
|
|
27
|
-
onShow?.();
|
|
28
|
-
}
|
|
29
|
-
function hide() {
|
|
30
|
-
popover?.hidePopover();
|
|
31
|
-
onHide?.();
|
|
32
|
-
}
|
|
33
|
-
function onMouseEnter() {
|
|
34
|
-
hideDebounced.clear();
|
|
35
|
-
show();
|
|
36
|
-
}
|
|
37
|
-
function onToggle(event) {
|
|
38
|
-
isOpen = event.newState === 'open';
|
|
39
|
-
}
|
|
40
|
-
function attachTriggerListeners(activator) {
|
|
41
|
-
const cleanups = [];
|
|
42
|
-
if (listenClick) {
|
|
43
|
-
cleanups.push(on(activator, 'click', show));
|
|
44
|
-
}
|
|
45
|
-
if (listenFocus) {
|
|
46
|
-
cleanups.push(on(activator, 'focusin', show));
|
|
47
|
-
// TODO: handle focusout ???
|
|
48
|
-
}
|
|
49
|
-
if (listenHover) {
|
|
50
|
-
cleanups.push(on(activator, 'mouseenter', onMouseEnter));
|
|
51
|
-
cleanups.push(on(activator, 'mouseleave', hideDebounced));
|
|
52
|
-
}
|
|
53
|
-
if (hotKey) {
|
|
54
|
-
cleanups.push(on(window, 'keydown', handleHotKey));
|
|
55
|
-
}
|
|
56
|
-
return () => {
|
|
57
|
-
for (const cleanup of cleanups)
|
|
58
|
-
cleanup();
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
function attachPopoverListeners(node) {
|
|
62
|
-
const cleanups = [];
|
|
63
|
-
cleanups.push(on(node, 'toggle', onToggle));
|
|
64
|
-
if (listenHover) {
|
|
65
|
-
cleanups.push(on(node, 'mouseenter', onMouseEnter));
|
|
66
|
-
cleanups.push(on(node, 'mouseleave', hideDebounced));
|
|
67
|
-
}
|
|
68
|
-
return () => {
|
|
69
|
-
for (const cleanup of cleanups)
|
|
70
|
-
cleanup();
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
async function handleHotKey(event) {
|
|
74
|
-
const { metaKey, ctrlKey, key } = event;
|
|
75
|
-
if ((metaKey || ctrlKey) && key === hotKey) {
|
|
76
|
-
event.preventDefault();
|
|
77
|
-
show();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
show,
|
|
83
|
-
hide,
|
|
84
|
-
get isOpen() {
|
|
85
|
-
return isOpen;
|
|
86
|
-
},
|
|
87
|
-
content: {
|
|
88
|
-
popover: mode,
|
|
89
|
-
[createAttachmentKey()]: (node) => {
|
|
90
|
-
popover = node;
|
|
91
|
-
popover.style.positionAnchor = anchorName;
|
|
92
|
-
popover.style.positionArea = placements[placement];
|
|
93
|
-
popover.style.inset = 'auto';
|
|
94
|
-
popover.style.positionTry = 'flip-x, flip-y';
|
|
95
|
-
popover.style.position = 'relative';
|
|
96
|
-
return attachPopoverListeners(node);
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
trigger: {
|
|
100
|
-
[createAttachmentKey()]: (node) => {
|
|
101
|
-
node.style.anchorName = anchorName;
|
|
102
|
-
return attachTriggerListeners(node);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|