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
|
@@ -3,8 +3,13 @@ import type { TextareaProps } from './type.js';
|
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
label: string;
|
|
5
5
|
field?: RemoteFormField<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
|
|
8
|
+
* ensuite le relais et devient la source de vérité.
|
|
9
|
+
*/
|
|
6
10
|
value?: string;
|
|
7
11
|
maxHeight?: number;
|
|
12
|
+
variant?: 'floating' | 'block';
|
|
8
13
|
} & TextareaProps;
|
|
9
14
|
declare const InputTextarea: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
10
15
|
type InputTextarea = ReturnType<typeof InputTextarea>;
|
package/dist/input/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export { default as InputBoolean } from './InputBoolean.svelte';
|
|
2
|
+
export { default as InputCheckboxes } from './InputCheckboxes.svelte';
|
|
3
|
+
export { default as InputDateTime } from './InputDateTime.svelte';
|
|
4
|
+
export { default as InputMultiSelect } from './InputMultiSelect.svelte';
|
|
2
5
|
export { default as InputNumber } from './InputNumber.svelte';
|
|
3
6
|
export { default as InputRadio } from './InputRadio.svelte';
|
|
4
7
|
export { default as InputRange } from './InputRange.svelte';
|
|
5
|
-
export { default as InputRelation } from './InputRelation.svelte';
|
|
6
8
|
export { default as InputSelect } from './InputSelect.svelte';
|
|
7
9
|
export { default as InputSelectNative } from './InputSelectNative.svelte';
|
|
8
10
|
export { default as InputString } from './InputString.svelte';
|
|
9
11
|
export { default as InputTextarea } from './InputTextarea.svelte';
|
|
10
12
|
export { default as Issues } from './Issues.svelte';
|
|
11
13
|
export type * from './type.js';
|
|
14
|
+
export { type ItemsSource, type QueryLike, useItems } from './useItems.svelte.js';
|
package/dist/input/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { default as InputBoolean } from './InputBoolean.svelte';
|
|
2
|
+
export { default as InputCheckboxes } from './InputCheckboxes.svelte';
|
|
3
|
+
export { default as InputDateTime } from './InputDateTime.svelte';
|
|
4
|
+
export { default as InputMultiSelect } from './InputMultiSelect.svelte';
|
|
2
5
|
export { default as InputNumber } from './InputNumber.svelte';
|
|
3
6
|
export { default as InputRadio } from './InputRadio.svelte';
|
|
4
7
|
export { default as InputRange } from './InputRange.svelte';
|
|
5
|
-
export { default as InputRelation } from './InputRelation.svelte';
|
|
6
8
|
export { default as InputSelect } from './InputSelect.svelte';
|
|
7
9
|
export { default as InputSelectNative } from './InputSelectNative.svelte';
|
|
8
10
|
export { default as InputString } from './InputString.svelte';
|
|
9
11
|
export { default as InputTextarea } from './InputTextarea.svelte';
|
|
10
12
|
export { default as Issues } from './Issues.svelte';
|
|
13
|
+
export { useItems } from './useItems.svelte.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RemoteQueryFunction } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Ce que la liste d'un select et `Loading` lisent d'une source. `RemoteQuery` le satisfait
|
|
4
|
+
* structurellement, ce qui évite d'imposer une remote query à qui n'en a pas.
|
|
5
|
+
*/
|
|
6
|
+
export type QueryLike<T> = {
|
|
7
|
+
readonly current: T | undefined;
|
|
8
|
+
readonly loading: boolean;
|
|
9
|
+
readonly error: unknown;
|
|
10
|
+
readonly ready: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Les trois façons d'alimenter un select:
|
|
14
|
+
* - un tableau déjà chargé — le filtre reste local;
|
|
15
|
+
* - une fonction, synchrone ou non — elle porte son propre filtre;
|
|
16
|
+
* - une remote query — le serveur filtre.
|
|
17
|
+
*/
|
|
18
|
+
export type ItemsSource<Item> = Item[] | ((arg: {
|
|
19
|
+
search: string;
|
|
20
|
+
}) => Item[] | Promise<Item[]>) | RemoteQueryFunction<{
|
|
21
|
+
search: string;
|
|
22
|
+
}, Item[]>;
|
|
23
|
+
export type UseItemsOptions<Item> = {
|
|
24
|
+
source: () => ItemsSource<Item>;
|
|
25
|
+
search: () => string;
|
|
26
|
+
/** Base du filtre par défaut, et libellé affiché à défaut de snippet. */
|
|
27
|
+
getLabel: (item: Item) => string;
|
|
28
|
+
filter?: () => ((item: Item, search: string) => boolean) | undefined;
|
|
29
|
+
debounceMs?: number;
|
|
30
|
+
};
|
|
31
|
+
/** `item.id`, puis `item.value`: les deux formes que prennent les items en pratique. */
|
|
32
|
+
export declare function defaultGetValue<Item>(item: Item): string;
|
|
33
|
+
/** Un item porteur d'un `label` s'affiche par celui-ci, ce qui rend les options lisibles sans snippet. */
|
|
34
|
+
export declare function defaultGetLabel<Item>(item: Item, getValue: (item: Item) => string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Ramène les trois formes de `ItemsSource` à la seule que lisent les selects et `Loading`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useItems<Item>({ source, search, getLabel, filter, debounceMs }: UseItemsOptions<Item>): QueryLike<Item[]>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import debounce from 'debounce';
|
|
2
|
+
/** `item.id`, puis `item.value`: les deux formes que prennent les items en pratique. */
|
|
3
|
+
export function defaultGetValue(item) {
|
|
4
|
+
if (item && typeof item === 'object') {
|
|
5
|
+
if ('id' in item && typeof item.id === 'string')
|
|
6
|
+
return item.id;
|
|
7
|
+
if ('value' in item && typeof item.value === 'string')
|
|
8
|
+
return item.value;
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(item);
|
|
11
|
+
}
|
|
12
|
+
/** Un item porteur d'un `label` s'affiche par celui-ci, ce qui rend les options lisibles sans snippet. */
|
|
13
|
+
export function defaultGetLabel(item, getValue) {
|
|
14
|
+
if (item && typeof item === 'object' && 'label' in item) {
|
|
15
|
+
if (typeof item.label === 'string')
|
|
16
|
+
return item.label;
|
|
17
|
+
}
|
|
18
|
+
return getValue(item);
|
|
19
|
+
}
|
|
20
|
+
/** Casse et diacritiques ignorées: « Genève » se trouve en tapant « geneve ». */
|
|
21
|
+
function normalize(value) {
|
|
22
|
+
return value
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.normalize('NFD')
|
|
25
|
+
.replace(/\p{Diacritic}/gu, '');
|
|
26
|
+
}
|
|
27
|
+
/** `RemoteQuery` est une `Promise` augmentée: `ready` est ce qui l'en distingue. */
|
|
28
|
+
function isQueryLike(value) {
|
|
29
|
+
return !!value && typeof value === 'object' && 'ready' in value;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Ramène les trois formes de `ItemsSource` à la seule que lisent les selects et `Loading`.
|
|
33
|
+
*/
|
|
34
|
+
export function useItems({ source, search, getLabel, filter, debounceMs = 150 }) {
|
|
35
|
+
// On temporise la valeur cherchée plutôt que l'appel: un seul mécanisme couvre les trois
|
|
36
|
+
// formes, et les remote queries cessent de partir à chaque frappe.
|
|
37
|
+
let debounced = $state(search());
|
|
38
|
+
const apply = debounce((value) => {
|
|
39
|
+
debounced = value;
|
|
40
|
+
}, debounceMs);
|
|
41
|
+
$effect(() => {
|
|
42
|
+
const value = search();
|
|
43
|
+
// Une recherche vide s'applique tout de suite: ouvrir le popover ne doit rien attendre.
|
|
44
|
+
if (!value) {
|
|
45
|
+
apply.clear();
|
|
46
|
+
debounced = value;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
apply(value);
|
|
50
|
+
});
|
|
51
|
+
const resolved = $derived.by(() => {
|
|
52
|
+
const items = source();
|
|
53
|
+
if (Array.isArray(items))
|
|
54
|
+
return { kind: 'static', value: items };
|
|
55
|
+
// Appel unique: c'est son retour, et lui seul, qui distingue les trois formes.
|
|
56
|
+
const result = items({ search: debounced });
|
|
57
|
+
if (Array.isArray(result))
|
|
58
|
+
return { kind: 'resolved', value: result };
|
|
59
|
+
if (isQueryLike(result))
|
|
60
|
+
return { kind: 'query', value: result };
|
|
61
|
+
return { kind: 'promise', value: result };
|
|
62
|
+
});
|
|
63
|
+
const pending = $state({
|
|
64
|
+
current: [],
|
|
65
|
+
loading: false,
|
|
66
|
+
error: undefined,
|
|
67
|
+
ready: false
|
|
68
|
+
});
|
|
69
|
+
let sequence = 0;
|
|
70
|
+
$effect(() => {
|
|
71
|
+
if (resolved.kind !== 'promise')
|
|
72
|
+
return;
|
|
73
|
+
// Une réponse en retard ne doit pas écraser une plus récente.
|
|
74
|
+
const current = ++sequence;
|
|
75
|
+
pending.loading = true;
|
|
76
|
+
resolved.value
|
|
77
|
+
.then((items) => {
|
|
78
|
+
if (current !== sequence)
|
|
79
|
+
return;
|
|
80
|
+
pending.current = items;
|
|
81
|
+
pending.error = undefined;
|
|
82
|
+
pending.ready = true;
|
|
83
|
+
})
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
if (current !== sequence)
|
|
86
|
+
return;
|
|
87
|
+
pending.error = error;
|
|
88
|
+
})
|
|
89
|
+
.finally(() => {
|
|
90
|
+
if (current !== sequence)
|
|
91
|
+
return;
|
|
92
|
+
pending.loading = false;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
function filterStatic(items) {
|
|
96
|
+
if (!debounced)
|
|
97
|
+
return items;
|
|
98
|
+
const match = filter?.();
|
|
99
|
+
if (match)
|
|
100
|
+
return items.filter((item) => match(item, debounced));
|
|
101
|
+
const needle = normalize(debounced);
|
|
102
|
+
return items.filter((item) => normalize(getLabel(item)).includes(needle));
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
get current() {
|
|
106
|
+
switch (resolved.kind) {
|
|
107
|
+
case 'static':
|
|
108
|
+
return filterStatic(resolved.value);
|
|
109
|
+
case 'resolved':
|
|
110
|
+
return resolved.value;
|
|
111
|
+
case 'query':
|
|
112
|
+
return resolved.value.current ?? [];
|
|
113
|
+
case 'promise':
|
|
114
|
+
return pending.current;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
get loading() {
|
|
118
|
+
// Sans quoi la fenêtre de temporisation afficherait un résultat périmé sans indicateur.
|
|
119
|
+
if (search() !== debounced)
|
|
120
|
+
return true;
|
|
121
|
+
if (resolved.kind === 'query')
|
|
122
|
+
return resolved.value.loading;
|
|
123
|
+
if (resolved.kind === 'promise')
|
|
124
|
+
return pending.loading;
|
|
125
|
+
return false;
|
|
126
|
+
},
|
|
127
|
+
get error() {
|
|
128
|
+
if (resolved.kind === 'query')
|
|
129
|
+
return resolved.value.error;
|
|
130
|
+
if (resolved.kind === 'promise')
|
|
131
|
+
return pending.error;
|
|
132
|
+
return undefined;
|
|
133
|
+
},
|
|
134
|
+
get ready() {
|
|
135
|
+
if (resolved.kind === 'query')
|
|
136
|
+
return resolved.value.ready;
|
|
137
|
+
if (resolved.kind === 'promise')
|
|
138
|
+
return pending.ready;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { GhostIcon, LoaderCircleIcon, TriangleAlertIcon } from '@lucide/svelte'
|
|
3
|
-
import type { RemoteQuery } from '@sveltejs/kit'
|
|
4
3
|
import type { ClassValue } from 'svelte/elements'
|
|
5
4
|
import { fade } from 'svelte/transition'
|
|
5
|
+
import type { QueryLike } from '../input/useItems.svelte.js'
|
|
6
6
|
|
|
7
7
|
let {
|
|
8
8
|
query,
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
error = 'Une erreur est survenue',
|
|
11
11
|
class: klass
|
|
12
12
|
}: {
|
|
13
|
-
|
|
13
|
+
/** Structurel plutôt que `RemoteQuery`: une source locale s'affiche de la même façon. */
|
|
14
|
+
query: QueryLike<unknown>
|
|
14
15
|
empty?: string
|
|
15
16
|
error?: string
|
|
16
17
|
class?: ClassValue
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { RemoteQuery } from '@sveltejs/kit';
|
|
2
1
|
import type { ClassValue } from 'svelte/elements';
|
|
2
|
+
import type { QueryLike } from '../input/useItems.svelte.js';
|
|
3
3
|
type $$ComponentProps = {
|
|
4
|
-
|
|
4
|
+
/** Structurel plutôt que `RemoteQuery`: une source locale s'affiche de la même façon. */
|
|
5
|
+
query: QueryLike<unknown>;
|
|
5
6
|
empty?: string;
|
|
6
7
|
error?: string;
|
|
7
8
|
class?: ClassValue;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
type PopoverOptions,
|
|
7
7
|
type PopoverType,
|
|
8
8
|
usePopover
|
|
9
|
-
} from './
|
|
9
|
+
} from './usePopover.svelte.js'
|
|
10
10
|
|
|
11
11
|
let {
|
|
12
12
|
children,
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
div[popover]:popover-open {
|
|
40
|
-
inset: 5px;
|
|
41
40
|
transition-property: opacity, scale;
|
|
42
41
|
transition-duration: 150ms;
|
|
43
42
|
opacity: 1;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ClassValue } from 'svelte/elements';
|
|
3
|
+
import { type Placement, type PopoverOptions, type PopoverType } from './usePopover.svelte.js';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
children: Snippet<[PopoverType]>;
|
|
6
|
+
trigger: Snippet<[PopoverType]>;
|
|
7
|
+
placement?: Placement;
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
style?: string;
|
|
10
|
+
} & PopoverOptions;
|
|
11
|
+
declare const Popover: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type Popover = ReturnType<typeof Popover>;
|
|
13
|
+
export default Popover;
|
package/dist/popover/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Popover } from './Popover.svelte';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './usePopover.svelte.js';
|
package/dist/popover/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Popover } from './Popover.svelte';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './usePopover.svelte.js';
|
|
@@ -7,6 +7,7 @@ export type PopoverOptions = {
|
|
|
7
7
|
placement?: Placement;
|
|
8
8
|
listenClick?: boolean;
|
|
9
9
|
listenFocus?: boolean;
|
|
10
|
+
listenFocusout?: boolean;
|
|
10
11
|
listenHover?: boolean;
|
|
11
12
|
/** Only when listenHover={true} */
|
|
12
13
|
hideDelay?: number;
|
|
@@ -14,7 +15,7 @@ export type PopoverOptions = {
|
|
|
14
15
|
onHide?: () => void;
|
|
15
16
|
hotKey?: string;
|
|
16
17
|
};
|
|
17
|
-
export declare function usePopover({ mode, placement, listenClick, listenFocus, listenHover, hideDelay, onShow, onHide, hotKey }?: PopoverOptions): {
|
|
18
|
+
export declare function usePopover({ mode, placement, listenClick, listenFocus, listenFocusout, listenHover, hideDelay, onShow, onHide, hotKey }?: PopoverOptions): {
|
|
18
19
|
show: () => void;
|
|
19
20
|
hide: () => void;
|
|
20
21
|
readonly isOpen: boolean;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import debounce from 'debounce';
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { createAttachmentKey } from 'svelte/attachments';
|
|
4
|
+
import { on } from 'svelte/events';
|
|
5
|
+
const placements = {
|
|
6
|
+
top: 'top',
|
|
7
|
+
'top-end': 'top span-left',
|
|
8
|
+
'top-start': 'top span-right',
|
|
9
|
+
bottom: 'bottom',
|
|
10
|
+
'bottom-end': 'bottom span-left',
|
|
11
|
+
'bottom-start': 'bottom span-right',
|
|
12
|
+
left: 'left',
|
|
13
|
+
'left-end': 'left span-top',
|
|
14
|
+
'left-start': 'left span-bottom',
|
|
15
|
+
right: 'right',
|
|
16
|
+
'right-end': 'right span-top',
|
|
17
|
+
'right-start': 'right span-bottom'
|
|
18
|
+
};
|
|
19
|
+
let popoverInstanceCount = 0;
|
|
20
|
+
export function usePopover({ mode = 'auto', placement = 'bottom-start', listenClick = true, listenFocus = true, listenFocusout = false, listenHover = false, hideDelay = 400, onShow, onHide, hotKey } = {}) {
|
|
21
|
+
const uid = popoverInstanceCount++;
|
|
22
|
+
const anchorName = `--anchor-${uid}`;
|
|
23
|
+
let content = $state();
|
|
24
|
+
let activator = $state();
|
|
25
|
+
let isOpen = $state(false);
|
|
26
|
+
const hideDebounced = debounce(hide, hideDelay);
|
|
27
|
+
let isHiding = false;
|
|
28
|
+
function show() {
|
|
29
|
+
// En refermant, le navigateur rend le focus au déclencheur si le focus était dans le
|
|
30
|
+
// popover — un bouton « Ok » ou « Effacer », typiquement. Sans ce garde, le `focusin`
|
|
31
|
+
// qui s'ensuit rouvre aussitôt ce qu'on vient de fermer, et `hide()` ne fait rien.
|
|
32
|
+
if (isHiding)
|
|
33
|
+
return;
|
|
34
|
+
content?.showPopover();
|
|
35
|
+
onShow?.();
|
|
36
|
+
}
|
|
37
|
+
function hide() {
|
|
38
|
+
isHiding = true;
|
|
39
|
+
content?.hidePopover();
|
|
40
|
+
isHiding = false;
|
|
41
|
+
onHide?.();
|
|
42
|
+
}
|
|
43
|
+
function onMouseEnter() {
|
|
44
|
+
hideDebounced.clear();
|
|
45
|
+
show();
|
|
46
|
+
}
|
|
47
|
+
function onToggle(event) {
|
|
48
|
+
isOpen = event.newState === 'open';
|
|
49
|
+
}
|
|
50
|
+
function onFocusOut({ relatedTarget }) {
|
|
51
|
+
// Le focus ne va nulle part — `null`, ou `body` selon le navigateur: typiquement un
|
|
52
|
+
// `mousedown` sur un élément non focusable, un jour du calendrier par exemple. Fermer ici
|
|
53
|
+
// escamoterait la cible avant que le `click` ne l'atteigne; hors du popover, le
|
|
54
|
+
// light-dismiss natif s'en charge de toute façon.
|
|
55
|
+
if (!(relatedTarget instanceof Node) || relatedTarget === document.body)
|
|
56
|
+
return;
|
|
57
|
+
if (!content?.contains(relatedTarget) && relatedTarget !== activator)
|
|
58
|
+
return hide();
|
|
59
|
+
}
|
|
60
|
+
function attachTriggerListeners(node) {
|
|
61
|
+
const cleanups = [];
|
|
62
|
+
if (listenClick) {
|
|
63
|
+
cleanups.push(on(node, 'click', show));
|
|
64
|
+
}
|
|
65
|
+
if (listenFocus) {
|
|
66
|
+
cleanups.push(on(node, 'focusin', show));
|
|
67
|
+
}
|
|
68
|
+
if (listenFocus || listenFocusout) {
|
|
69
|
+
cleanups.push(on(node, 'focusout', onFocusOut));
|
|
70
|
+
}
|
|
71
|
+
if (listenHover) {
|
|
72
|
+
cleanups.push(on(node, 'mouseenter', onMouseEnter));
|
|
73
|
+
cleanups.push(on(node, 'mouseleave', hideDebounced));
|
|
74
|
+
}
|
|
75
|
+
if (hotKey) {
|
|
76
|
+
cleanups.push(on(window, 'keydown', handleHotKey));
|
|
77
|
+
}
|
|
78
|
+
return () => {
|
|
79
|
+
for (const cleanup of cleanups)
|
|
80
|
+
cleanup();
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function attachPopoverListeners(node) {
|
|
84
|
+
const cleanups = [];
|
|
85
|
+
cleanups.push(on(node, 'toggle', onToggle));
|
|
86
|
+
if (listenHover) {
|
|
87
|
+
cleanups.push(on(node, 'mouseenter', onMouseEnter));
|
|
88
|
+
cleanups.push(on(node, 'mouseleave', hideDebounced));
|
|
89
|
+
}
|
|
90
|
+
if (listenFocus || listenFocusout) {
|
|
91
|
+
cleanups.push(on(node, 'focusout', onFocusOut));
|
|
92
|
+
}
|
|
93
|
+
return () => {
|
|
94
|
+
for (const cleanup of cleanups)
|
|
95
|
+
cleanup();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function handleHotKey(event) {
|
|
99
|
+
const { metaKey, ctrlKey, key } = event;
|
|
100
|
+
if ((metaKey || ctrlKey) && key === hotKey) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
show();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
show,
|
|
108
|
+
hide,
|
|
109
|
+
get isOpen() {
|
|
110
|
+
return isOpen;
|
|
111
|
+
},
|
|
112
|
+
content: {
|
|
113
|
+
popover: mode,
|
|
114
|
+
[createAttachmentKey()]: (node) => {
|
|
115
|
+
untrack(() => (content = node));
|
|
116
|
+
node.style.positionAnchor = anchorName;
|
|
117
|
+
node.style.positionArea = placements[placement];
|
|
118
|
+
node.style.inset = 'auto';
|
|
119
|
+
node.style.positionTry = 'flip-x, flip-y';
|
|
120
|
+
// `fixed` et non `relative`: dans le top layer `relative` est absolutisé, le bloc
|
|
121
|
+
// conteneur devient alors le document et Chrome compense le défilement de page même
|
|
122
|
+
// quand l'ancre vit dans un sous-arbre `position: fixed` (tiroir, en-tête collant) qui,
|
|
123
|
+
// lui, ne défile pas — le popover dérivait d'exactement `window.scrollY`. `fixed` cale
|
|
124
|
+
// le bloc conteneur sur le viewport, ce qui rend aussi `position-try` viewport-relatif.
|
|
125
|
+
node.style.position = 'fixed';
|
|
126
|
+
const cleanup = attachPopoverListeners(node);
|
|
127
|
+
return () => {
|
|
128
|
+
untrack(() => (content = undefined));
|
|
129
|
+
cleanup();
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
trigger: {
|
|
134
|
+
[createAttachmentKey()]: (node) => {
|
|
135
|
+
untrack(() => (activator = node));
|
|
136
|
+
node.style.anchorName = anchorName;
|
|
137
|
+
const cleanup = attachTriggerListeners(node);
|
|
138
|
+
return () => {
|
|
139
|
+
untrack(() => (activator = undefined));
|
|
140
|
+
cleanup();
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
package/dist/remote/useForm.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RemoteForm, RemoteFormInput } from '@sveltejs/kit';
|
|
1
|
+
import type { RemoteForm, RemoteFormEnhanceCallback, RemoteFormInput } from '@sveltejs/kit';
|
|
2
2
|
import type { StandardSchemaV1 } from 'runed/kit';
|
|
3
3
|
import { type Attachment } from 'svelte/attachments';
|
|
4
4
|
/**
|
|
@@ -7,10 +7,9 @@ import { type Attachment } from 'svelte/attachments';
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function useForm<Input extends RemoteFormInput | undefined, Output>(remoteForm: RemoteForm<Input, Output>, schema: StandardSchemaV1<Input>, debounceMs?: number): RemoteForm<Input, Output>;
|
|
9
9
|
export declare function validationOnInput<Input extends RemoteFormInput | undefined, Output>(remoteForm: RemoteForm<Input, Output>, debounceMs?: number): Attachment<HTMLFormElement>;
|
|
10
|
-
type UseEnhanceCallbackOptions<
|
|
11
|
-
onSuccess?: (
|
|
10
|
+
type UseEnhanceCallbackOptions<Output> = {
|
|
11
|
+
onSuccess?: (result: Output | undefined, element: HTMLFormElement) => void;
|
|
12
12
|
onError?: (err: unknown) => void;
|
|
13
13
|
};
|
|
14
|
-
|
|
15
|
-
export declare function useEnhanceCallback<Input extends RemoteFormInput | void>({ onSuccess, onError }: UseEnhanceCallbackOptions<Input>): EnhanceCallback<Input>;
|
|
14
|
+
export declare function useEnhanceCallback<Input extends RemoteFormInput | void, Output>({ onSuccess, onError }: UseEnhanceCallbackOptions<Output>): RemoteFormEnhanceCallback<Input, Output>;
|
|
16
15
|
export {};
|
package/dist/remote/useForm.js
CHANGED
|
@@ -29,10 +29,15 @@ export function validationOnInput(remoteForm, debounceMs = 1000) {
|
|
|
29
29
|
return validation;
|
|
30
30
|
}
|
|
31
31
|
export function useEnhanceCallback({ onSuccess, onError }) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
// L'instance passée au callback ne porte plus `data` ni `form`: la donnée soumise n'est plus
|
|
33
|
+
// exposée telle quelle, et l'élément s'appelle désormais `element`.
|
|
34
|
+
return (form) => {
|
|
35
|
+
form
|
|
36
|
+
.submit()
|
|
37
|
+
.then((succeeded) => {
|
|
38
|
+
// `submit()` résout `false` quand le préflight ou la validation serveur a échoué.
|
|
39
|
+
if (succeeded)
|
|
40
|
+
onSuccess?.(form.result, form.element);
|
|
36
41
|
})
|
|
37
42
|
.catch((err) => {
|
|
38
43
|
console.error(err);
|
package/dist/state/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './param.
|
|
1
|
+
export * from './param.js';
|
package/dist/state/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './param.
|
|
1
|
+
export * from './param.js';
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
formaction = '',
|
|
9
9
|
onclick: onclickProp,
|
|
10
10
|
onmouseleave: onmouseleaveProp,
|
|
11
|
+
class: klass,
|
|
11
12
|
...props
|
|
12
13
|
}: {
|
|
13
14
|
/** Form action URL triggered on confirmation */
|
|
@@ -47,8 +48,9 @@
|
|
|
47
48
|
class={[
|
|
48
49
|
btn && [
|
|
49
50
|
'btn',
|
|
50
|
-
waitConfirmation ? 'btn-outline btn-error' : 'text-error
|
|
51
|
-
props.disabled && 'btn-disabled'
|
|
51
|
+
waitConfirmation ? 'btn-outline btn-error' : 'btn-ghost text-error',
|
|
52
|
+
props.disabled && 'btn-disabled',
|
|
53
|
+
klass
|
|
52
54
|
]
|
|
53
55
|
]}
|
|
54
56
|
formaction={waitConfirmation ? formaction : undefined}
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
{@render activator?.({ showModal: () => dialog?.showModal() })}
|
|
70
70
|
|
|
71
71
|
<dialog bind:this={dialog} class="modal" tabindex="-1">
|
|
72
|
-
<div class="
|
|
73
|
-
<div class="relative flex items-center border-b p-4">
|
|
72
|
+
<div class="border-hard modal-box flex flex-col rounded-lg border p-0">
|
|
73
|
+
<div class="border-hard relative flex items-center border-b p-4">
|
|
74
74
|
<div class="grow">
|
|
75
75
|
{@render header?.()}
|
|
76
76
|
</div>
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
{@render footer?.()}
|
|
93
93
|
</div>
|
|
94
|
-
<
|
|
95
|
-
<button>close</button>
|
|
96
|
-
</
|
|
94
|
+
<div class="modal-backdrop">
|
|
95
|
+
<button type="button" onclick={() => dialog?.close()}>close</button>
|
|
96
|
+
</div>
|
|
97
97
|
</dialog>
|