@casinogate/ui 1.11.12 → 1.11.13
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/assets/css/root.css +1 -1
- package/dist/components/combobox/presets/async/combobox.async.svelte +111 -120
- package/dist/components/combobox/presets/async/combobox.async.svelte.d.ts +8 -9
- package/dist/components/combobox/presets/async/use-async-combobox-data.svelte.d.ts +36 -0
- package/dist/components/combobox/presets/async/use-async-combobox-data.svelte.js +113 -0
- package/dist/components/combobox/presets/root/combobox.svelte +19 -6
- package/dist/components/combobox/presets/use-display-value.svelte.d.ts +6 -3
- package/dist/components/combobox/presets/use-display-value.svelte.js +6 -2
- package/dist/components/combobox/presets/use-selected-items.svelte.d.ts +7 -4
- package/dist/components/combobox/presets/use-selected-items.svelte.js +6 -4
- package/dist/components/combobox/types.d.ts +33 -27
- package/package.json +1 -1
package/dist/assets/css/root.css
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
<script lang="ts" generics="TItem extends
|
|
2
|
-
import type { CommandCollection
|
|
3
|
-
import type { ComboboxAsyncProps } from '../../types.js';
|
|
1
|
+
<script lang="ts" generics="TItem extends ComboboxItem">
|
|
2
|
+
import type { CommandCollection } from '../../../command/index.js';
|
|
3
|
+
import type { ComboboxAsyncProps, ComboboxItem } from '../../types.js';
|
|
4
4
|
|
|
5
5
|
import { SELECTION_TYPE } from '../../../../internal/constants/selection-type.js';
|
|
6
6
|
import { boolAttr } from '../../../../internal/utils/attrs.js';
|
|
7
7
|
import { cn, noop } from '../../../../internal/utils/common.js';
|
|
8
|
-
import {
|
|
9
|
-
import { afterTick, boxWith
|
|
8
|
+
import { watch } from 'runed';
|
|
9
|
+
import { afterTick, boxWith } from 'svelte-toolbelt';
|
|
10
10
|
import type { Attachment } from 'svelte/attachments';
|
|
11
11
|
import { cubicInOut } from 'svelte/easing';
|
|
12
12
|
import { fly } from 'svelte/transition';
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { useDisplayValue } from '../use-display-value.svelte.js';
|
|
20
20
|
import { useGroupedItems } from '../use-grouped-items.svelte.js';
|
|
21
21
|
import { useSelectedItems } from '../use-selected-items.svelte.js';
|
|
22
|
+
import { useAsyncComboboxData } from './use-async-combobox-data.svelte.js';
|
|
22
23
|
|
|
23
24
|
let {
|
|
24
25
|
open = $bindable(false),
|
|
@@ -44,48 +45,44 @@
|
|
|
44
45
|
clearable = false,
|
|
45
46
|
|
|
46
47
|
items = $bindable([]),
|
|
48
|
+
selectedItems = $bindable([]),
|
|
47
49
|
|
|
48
50
|
maxContentHeight,
|
|
49
51
|
|
|
50
52
|
loading: loadingSnippet,
|
|
51
53
|
pageSize = 10,
|
|
52
54
|
callback,
|
|
55
|
+
loadItems,
|
|
53
56
|
loadImmediate = true,
|
|
57
|
+
debounceMs,
|
|
54
58
|
searchDebounce = 300,
|
|
55
59
|
dependsOn,
|
|
56
60
|
clearOnDependencyChange = true,
|
|
61
|
+
onDependencyChange,
|
|
57
62
|
onSelect = noop,
|
|
58
63
|
|
|
59
64
|
...restProps
|
|
60
65
|
}: ComboboxAsyncProps<TItem> = $props();
|
|
61
66
|
|
|
62
|
-
const
|
|
67
|
+
const getDefaultValue = () => (type === SELECTION_TYPE.MULTIPLE ? [] : '');
|
|
68
|
+
const isMultiple = $derived.by(() => type === SELECTION_TYPE.MULTIPLE);
|
|
63
69
|
|
|
64
70
|
// Initialize value based on type
|
|
65
71
|
if (value === undefined) {
|
|
66
|
-
|
|
67
|
-
value = defaultValue;
|
|
72
|
+
value = getDefaultValue();
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
watch.pre(
|
|
71
76
|
() => value,
|
|
72
77
|
() => {
|
|
73
78
|
if (value !== undefined) return;
|
|
74
|
-
value =
|
|
79
|
+
value = getDefaultValue();
|
|
75
80
|
}
|
|
76
81
|
);
|
|
77
82
|
|
|
78
|
-
// let items = $state<CommandItem[]>(initialItems);
|
|
79
|
-
let isLoading = $state(false);
|
|
80
|
-
let isSearching = $state(false);
|
|
81
|
-
let hasMore = $state(false);
|
|
82
|
-
let currentPage = $state(1);
|
|
83
|
-
let error = $state<string | null>(null);
|
|
84
|
-
let lastSearch = $state('');
|
|
85
|
-
|
|
86
83
|
let listRef = $state<HTMLElement | null>(null);
|
|
87
84
|
|
|
88
|
-
const createCollection = (itemList:
|
|
85
|
+
const createCollection = (itemList: ComboboxItem[]): CommandCollection => {
|
|
89
86
|
const groupOrderMap = new Map<string, number>();
|
|
90
87
|
groups.forEach((g, index) => {
|
|
91
88
|
groupOrderMap.set(g.value, g.order ?? index);
|
|
@@ -113,11 +110,13 @@
|
|
|
113
110
|
value: boxWith(() => value!),
|
|
114
111
|
collection: boxWith(() => collection),
|
|
115
112
|
placeholder: boxWith(() => placeholder),
|
|
113
|
+
selectedItems: boxWith(() => selectedItems),
|
|
116
114
|
});
|
|
117
115
|
|
|
118
|
-
const
|
|
116
|
+
const resolvedSelectedItems = useSelectedItems({
|
|
119
117
|
value: boxWith(() => value!),
|
|
120
118
|
collection: boxWith(() => collection),
|
|
119
|
+
selectedItems: boxWith(() => selectedItems),
|
|
121
120
|
});
|
|
122
121
|
|
|
123
122
|
const isSelected = (itemValue: string): boolean => {
|
|
@@ -132,109 +131,61 @@
|
|
|
132
131
|
|
|
133
132
|
const hasResults = $derived(collection.size > 0);
|
|
134
133
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
isSearching = !append;
|
|
140
|
-
error = null;
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const result = await callback({ page, pageSize, search });
|
|
144
|
-
|
|
145
|
-
if (append) {
|
|
146
|
-
items = [...items, ...result.items];
|
|
147
|
-
} else {
|
|
148
|
-
items = result.items;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
currentPage = page;
|
|
152
|
-
hasMore = result.hasMore;
|
|
153
|
-
lastSearch = search;
|
|
154
|
-
} catch (err) {
|
|
155
|
-
error = err instanceof Error ? err.message : 'Failed to fetch data';
|
|
156
|
-
} finally {
|
|
157
|
-
isLoading = false;
|
|
158
|
-
isSearching = false;
|
|
134
|
+
const resolvedLoadItems = $derived.by(() => {
|
|
135
|
+
const loader = loadItems ?? callback;
|
|
136
|
+
if (!loader) {
|
|
137
|
+
throw new Error('Combobox.Async requires either loadItems or callback');
|
|
159
138
|
}
|
|
160
|
-
};
|
|
161
139
|
|
|
162
|
-
|
|
163
|
-
|
|
140
|
+
return loader;
|
|
141
|
+
});
|
|
164
142
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
143
|
+
const asyncData = useAsyncComboboxData<TItem>({
|
|
144
|
+
items: boxWith(
|
|
145
|
+
() => items,
|
|
146
|
+
(next) => {
|
|
147
|
+
items = next;
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
loadItems: boxWith(() => resolvedLoadItems),
|
|
151
|
+
search: boxWith(
|
|
152
|
+
() => searchValue,
|
|
153
|
+
(next) => {
|
|
154
|
+
searchValue = next;
|
|
155
|
+
}
|
|
156
|
+
),
|
|
157
|
+
pageSize: boxWith(() => pageSize),
|
|
158
|
+
debounceMs: boxWith(() => debounceMs ?? searchDebounce),
|
|
159
|
+
loadImmediate: boxWith(() => loadImmediate),
|
|
160
|
+
dependency: boxWith(() => dependsOn?.()),
|
|
161
|
+
onDependencyReset: (dependency) => {
|
|
162
|
+
onDependencyChange?.(dependency);
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
search(debouncedSearchValue.current);
|
|
164
|
+
if (clearOnDependencyChange) {
|
|
165
|
+
value = isMultiple ? [] : '';
|
|
166
|
+
selectedItems = [];
|
|
167
|
+
}
|
|
175
168
|
},
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const isLoading = $derived(asyncData.isLoading);
|
|
172
|
+
const isSearching = $derived(asyncData.isSearching);
|
|
173
|
+
const hasMore = $derived(asyncData.hasMore);
|
|
174
|
+
const error = $derived(asyncData.error);
|
|
180
175
|
|
|
181
|
-
// Fetch on first open
|
|
182
176
|
watch(
|
|
183
177
|
() => open,
|
|
184
178
|
() => {
|
|
185
179
|
if (!open) return;
|
|
180
|
+
if (items.length > 0 || isLoading || loadImmediate) return;
|
|
186
181
|
|
|
187
|
-
|
|
188
|
-
if (items.length === 0 && !isLoading && !loadImmediate) {
|
|
189
|
-
fetchData(1, searchValue);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
// Fetch immediate data on mount
|
|
196
|
-
onMountEffect(() => {
|
|
197
|
-
if (loadImmediate) fetchData(1, searchValue);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Reset page on unmount
|
|
201
|
-
$effect(() => {
|
|
202
|
-
return () => {
|
|
203
|
-
currentPage = 1;
|
|
204
|
-
};
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Watch for dependency changes and reset
|
|
208
|
-
let previousDependency = $state<unknown>(dependsOn?.());
|
|
209
|
-
watch(
|
|
210
|
-
() => dependsOn?.(),
|
|
211
|
-
(newValue) => {
|
|
212
|
-
if (previousDependency === undefined && newValue === undefined) return;
|
|
213
|
-
if (previousDependency === newValue) return;
|
|
214
|
-
|
|
215
|
-
previousDependency = newValue;
|
|
216
|
-
|
|
217
|
-
// Reset state
|
|
218
|
-
items = [];
|
|
219
|
-
currentPage = 1;
|
|
220
|
-
hasMore = true;
|
|
221
|
-
error = null;
|
|
222
|
-
searchValue = '';
|
|
223
|
-
lastSearch = '';
|
|
224
|
-
|
|
225
|
-
// Clear selected value if enabled
|
|
226
|
-
if (clearOnDependencyChange) {
|
|
227
|
-
value = isMultiple ? [] : '';
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Reload data
|
|
231
|
-
fetchData(1, '');
|
|
182
|
+
asyncData.reload(searchValue);
|
|
232
183
|
}
|
|
233
184
|
);
|
|
234
185
|
|
|
235
186
|
const loadMore = () => {
|
|
236
187
|
if (!isLoading && hasMore) {
|
|
237
|
-
|
|
188
|
+
asyncData.loadMore();
|
|
238
189
|
}
|
|
239
190
|
};
|
|
240
191
|
|
|
@@ -256,8 +207,7 @@
|
|
|
256
207
|
};
|
|
257
208
|
|
|
258
209
|
const retry = () => {
|
|
259
|
-
|
|
260
|
-
fetchData(currentPage, searchValue);
|
|
210
|
+
asyncData.retry();
|
|
261
211
|
};
|
|
262
212
|
|
|
263
213
|
const scrollToTop = () => {
|
|
@@ -268,24 +218,45 @@
|
|
|
268
218
|
behavior: 'smooth',
|
|
269
219
|
});
|
|
270
220
|
};
|
|
271
|
-
|
|
221
|
+
|
|
222
|
+
const cacheSelectedItem = (item: TItem) => {
|
|
223
|
+
selectedItems = [...selectedItems.filter((selectedItem) => selectedItem.value !== item.value), item];
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const removeSelectedItem = (itemValue: string) => {
|
|
227
|
+
selectedItems = selectedItems.filter((selectedItem) => selectedItem.value !== itemValue);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
function onSelectItem(item: ComboboxItem) {
|
|
231
|
+
const typedItem = item as TItem;
|
|
232
|
+
|
|
272
233
|
if (Array.isArray(value)) {
|
|
273
234
|
let result = value;
|
|
274
235
|
if (value.includes(item.value)) {
|
|
275
|
-
|
|
236
|
+
if (allowDeselect) {
|
|
237
|
+
result = value.filter((v) => v !== item.value);
|
|
238
|
+
removeSelectedItem(item.value);
|
|
239
|
+
}
|
|
276
240
|
} else {
|
|
277
241
|
result = [...value, item.value];
|
|
242
|
+
cacheSelectedItem(typedItem);
|
|
278
243
|
}
|
|
279
|
-
onSelect(result,
|
|
244
|
+
onSelect(result, typedItem);
|
|
280
245
|
value = result;
|
|
281
246
|
} else {
|
|
282
|
-
let result = value;
|
|
247
|
+
let result = typeof value === 'string' ? value : '';
|
|
283
248
|
if (item.value === value) {
|
|
284
|
-
|
|
249
|
+
if (allowDeselect) {
|
|
250
|
+
result = '';
|
|
251
|
+
selectedItems = [];
|
|
252
|
+
} else {
|
|
253
|
+
cacheSelectedItem(typedItem);
|
|
254
|
+
}
|
|
285
255
|
} else {
|
|
286
256
|
result = item.value;
|
|
257
|
+
selectedItems = [typedItem];
|
|
287
258
|
}
|
|
288
|
-
onSelect(result,
|
|
259
|
+
onSelect(result, typedItem);
|
|
289
260
|
value = result;
|
|
290
261
|
}
|
|
291
262
|
|
|
@@ -307,11 +278,15 @@
|
|
|
307
278
|
{#snippet clearBtn()}
|
|
308
279
|
{#if clearable && isNotEmpty}
|
|
309
280
|
<button
|
|
281
|
+
aria-label="Clear selection"
|
|
310
282
|
class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:cursor-pointer cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
|
|
311
283
|
type="button"
|
|
312
284
|
onclick={(event) => {
|
|
313
285
|
event.stopPropagation();
|
|
314
|
-
|
|
286
|
+
const nextValue = isMultiple ? [] : '';
|
|
287
|
+
value = nextValue;
|
|
288
|
+
selectedItems = [];
|
|
289
|
+
onSelect(nextValue, null);
|
|
315
290
|
}}
|
|
316
291
|
>
|
|
317
292
|
<Icon.Cross class="cgui:ms-auto" width={16} height={16} />
|
|
@@ -323,13 +298,15 @@
|
|
|
323
298
|
{@const triggerProps = {
|
|
324
299
|
class: cn(comboboxTriggerVariants({ variant, size, rounded, fullWidth, class: triggerClass })),
|
|
325
300
|
'data-placeholder': boolAttr(isPlaceholder),
|
|
301
|
+
'aria-expanded': open,
|
|
302
|
+
'aria-busy': isLoading,
|
|
326
303
|
}}
|
|
327
304
|
{#if labelSnippet}
|
|
328
305
|
<PopoverPrimitive.Trigger {...triggerProps}>
|
|
329
306
|
{@render labelSnippet({
|
|
330
307
|
placeholder: displayValue.current,
|
|
331
308
|
value: isMultiple ? (value as string[]) : (value as string),
|
|
332
|
-
selectedItems:
|
|
309
|
+
selectedItems: resolvedSelectedItems.current,
|
|
333
310
|
})}
|
|
334
311
|
|
|
335
312
|
{@render clearBtn()}
|
|
@@ -349,7 +326,7 @@
|
|
|
349
326
|
{/if}
|
|
350
327
|
{/snippet}
|
|
351
328
|
|
|
352
|
-
{#snippet renderItem(item:
|
|
329
|
+
{#snippet renderItem(item: ComboboxItem)}
|
|
353
330
|
{@const { value: itemValue, label, icon: Icon, shortcut, onSelect: _, ...restItem } = item}
|
|
354
331
|
{@const itemAttrs = {
|
|
355
332
|
value: itemValue,
|
|
@@ -388,8 +365,22 @@
|
|
|
388
365
|
{/if}
|
|
389
366
|
{/snippet}
|
|
390
367
|
|
|
368
|
+
{#snippet renderEmpty()}
|
|
369
|
+
{#if typeof empty === 'string'}
|
|
370
|
+
<CommandPrimitive.Empty>{empty}</CommandPrimitive.Empty>
|
|
371
|
+
{:else if empty}
|
|
372
|
+
{@render empty({ props: {} })}
|
|
373
|
+
{:else}
|
|
374
|
+
<CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
|
|
375
|
+
{/if}
|
|
376
|
+
{/snippet}
|
|
377
|
+
|
|
391
378
|
{#snippet loadingSpinner()}
|
|
392
|
-
<div
|
|
379
|
+
<div
|
|
380
|
+
role="status"
|
|
381
|
+
aria-live="polite"
|
|
382
|
+
class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-icon-default"
|
|
383
|
+
>
|
|
393
384
|
<Spinner />
|
|
394
385
|
</div>
|
|
395
386
|
{/snippet}
|
|
@@ -401,7 +392,7 @@
|
|
|
401
392
|
{/snippet}
|
|
402
393
|
|
|
403
394
|
{#snippet errorState()}
|
|
404
|
-
<div class="cgui:p-4 cgui:text-center cgui:text-body-2">
|
|
395
|
+
<div role="alert" class="cgui:p-4 cgui:text-center cgui:text-body-2">
|
|
405
396
|
<p class="cgui:text-destructive cgui:mb-2">{error}</p>
|
|
406
397
|
<button type="button" class="cgui:text-sm cgui:text-primary cgui:underline cgui:hover:no-underline" onclick={retry}>
|
|
407
398
|
Try again
|
|
@@ -463,7 +454,7 @@
|
|
|
463
454
|
{@render loadingMore()}
|
|
464
455
|
{/if}
|
|
465
456
|
{:else}
|
|
466
|
-
|
|
457
|
+
{@render renderEmpty()}
|
|
467
458
|
{/if}
|
|
468
459
|
</CommandPrimitive.Viewport>
|
|
469
460
|
|
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
declare function $$render<TItem extends CommandItem>(): {
|
|
1
|
+
import type { ComboboxAsyncProps, ComboboxItem } from '../../types.js';
|
|
2
|
+
declare function $$render<TItem extends ComboboxItem>(): {
|
|
4
3
|
props: ComboboxAsyncProps<TItem>;
|
|
5
4
|
exports: {};
|
|
6
|
-
bindings: "value" | "open" | "items" | "searchValue";
|
|
5
|
+
bindings: "value" | "open" | "items" | "searchValue" | "selectedItems";
|
|
7
6
|
slots: {};
|
|
8
7
|
events: {};
|
|
9
8
|
};
|
|
10
|
-
declare class __sveltets_Render<TItem extends
|
|
9
|
+
declare class __sveltets_Render<TItem extends ComboboxItem> {
|
|
11
10
|
props(): ReturnType<typeof $$render<TItem>>['props'];
|
|
12
11
|
events(): ReturnType<typeof $$render<TItem>>['events'];
|
|
13
12
|
slots(): ReturnType<typeof $$render<TItem>>['slots'];
|
|
14
|
-
bindings(): "value" | "open" | "items" | "searchValue";
|
|
13
|
+
bindings(): "value" | "open" | "items" | "searchValue" | "selectedItems";
|
|
15
14
|
exports(): {};
|
|
16
15
|
}
|
|
17
16
|
interface $$IsomorphicComponent {
|
|
18
|
-
new <TItem extends
|
|
17
|
+
new <TItem extends ComboboxItem>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TItem>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TItem>['props']>, ReturnType<__sveltets_Render<TItem>['events']>, ReturnType<__sveltets_Render<TItem>['slots']>> & {
|
|
19
18
|
$$bindings?: ReturnType<__sveltets_Render<TItem>['bindings']>;
|
|
20
19
|
} & ReturnType<__sveltets_Render<TItem>['exports']>;
|
|
21
|
-
<TItem extends
|
|
20
|
+
<TItem extends ComboboxItem>(internal: unknown, props: ReturnType<__sveltets_Render<TItem>['props']> & {}): ReturnType<__sveltets_Render<TItem>['exports']>;
|
|
22
21
|
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
23
22
|
}
|
|
24
23
|
declare const Combobox: $$IsomorphicComponent;
|
|
25
|
-
type Combobox<TItem extends
|
|
24
|
+
type Combobox<TItem extends ComboboxItem> = InstanceType<typeof Combobox<TItem>>;
|
|
26
25
|
export default Combobox;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ComboboxAsyncLoader, ComboboxItem } from '../../types.js';
|
|
2
|
+
import type { ReadableBoxedValues, WritableBoxedValues } from 'svelte-toolbelt';
|
|
3
|
+
export declare const ASYNC_STATUS: {
|
|
4
|
+
readonly IDLE: "idle";
|
|
5
|
+
readonly LOADING: "loading";
|
|
6
|
+
readonly SEARCHING: "searching";
|
|
7
|
+
readonly LOADING_MORE: "loadingMore";
|
|
8
|
+
readonly ERROR: "error";
|
|
9
|
+
};
|
|
10
|
+
export type AsyncStatus = (typeof ASYNC_STATUS)[keyof typeof ASYNC_STATUS];
|
|
11
|
+
type Options<TItem extends ComboboxItem> = WritableBoxedValues<{
|
|
12
|
+
items: TItem[];
|
|
13
|
+
search: string;
|
|
14
|
+
}> & ReadableBoxedValues<{
|
|
15
|
+
loadItems: ComboboxAsyncLoader<TItem>;
|
|
16
|
+
pageSize: number;
|
|
17
|
+
debounceMs: number;
|
|
18
|
+
loadImmediate: boolean;
|
|
19
|
+
dependency: unknown;
|
|
20
|
+
}> & {
|
|
21
|
+
onDependencyReset?: (value: unknown) => void;
|
|
22
|
+
};
|
|
23
|
+
export declare function useAsyncComboboxData<TItem extends ComboboxItem>(opts: Options<TItem>): {
|
|
24
|
+
readonly status: AsyncStatus;
|
|
25
|
+
readonly error: string | null;
|
|
26
|
+
readonly hasMore: boolean;
|
|
27
|
+
readonly currentPage: number;
|
|
28
|
+
readonly isLoading: boolean;
|
|
29
|
+
readonly isSearching: boolean;
|
|
30
|
+
readonly isLoadingMore: boolean;
|
|
31
|
+
reload: (search?: string) => Promise<void>;
|
|
32
|
+
loadMore: () => Promise<void>;
|
|
33
|
+
retry: () => Promise<void>;
|
|
34
|
+
abort: () => void;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Debounced, watch } from 'runed';
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
export const ASYNC_STATUS = {
|
|
4
|
+
IDLE: 'idle',
|
|
5
|
+
LOADING: 'loading',
|
|
6
|
+
SEARCHING: 'searching',
|
|
7
|
+
LOADING_MORE: 'loadingMore',
|
|
8
|
+
ERROR: 'error',
|
|
9
|
+
};
|
|
10
|
+
export function useAsyncComboboxData(opts) {
|
|
11
|
+
let status = $state(ASYNC_STATUS.IDLE);
|
|
12
|
+
let error = $state(null);
|
|
13
|
+
let hasMore = $state(false);
|
|
14
|
+
let currentPage = $state(1);
|
|
15
|
+
let committedSearch = $state('');
|
|
16
|
+
let requestId = 0;
|
|
17
|
+
let controller = null;
|
|
18
|
+
const abortCurrent = () => {
|
|
19
|
+
requestId += 1;
|
|
20
|
+
controller?.abort();
|
|
21
|
+
controller = null;
|
|
22
|
+
};
|
|
23
|
+
const run = async (page, search, append = false) => {
|
|
24
|
+
if (append && (status === ASYNC_STATUS.LOADING_MORE || !hasMore))
|
|
25
|
+
return;
|
|
26
|
+
abortCurrent();
|
|
27
|
+
const id = requestId;
|
|
28
|
+
const localController = new AbortController();
|
|
29
|
+
controller = localController;
|
|
30
|
+
status = append
|
|
31
|
+
? ASYNC_STATUS.LOADING_MORE
|
|
32
|
+
: opts.items.current.length > 0
|
|
33
|
+
? ASYNC_STATUS.SEARCHING
|
|
34
|
+
: ASYNC_STATUS.LOADING;
|
|
35
|
+
error = null;
|
|
36
|
+
try {
|
|
37
|
+
const result = await opts.loadItems.current({
|
|
38
|
+
page,
|
|
39
|
+
pageSize: opts.pageSize.current,
|
|
40
|
+
search,
|
|
41
|
+
signal: localController.signal,
|
|
42
|
+
dependency: opts.dependency.current,
|
|
43
|
+
});
|
|
44
|
+
if (localController.signal.aborted || id !== requestId)
|
|
45
|
+
return;
|
|
46
|
+
opts.items.current = append ? [...opts.items.current, ...result.items] : result.items;
|
|
47
|
+
currentPage = page;
|
|
48
|
+
committedSearch = search;
|
|
49
|
+
hasMore = result.hasMore;
|
|
50
|
+
status = ASYNC_STATUS.IDLE;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (localController.signal.aborted || id !== requestId)
|
|
54
|
+
return;
|
|
55
|
+
error = err instanceof Error ? err.message : 'Failed to fetch data';
|
|
56
|
+
status = ASYNC_STATUS.ERROR;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const reload = (search = opts.search.current) => run(1, search, false);
|
|
60
|
+
const loadMore = () => run(currentPage + 1, committedSearch, true);
|
|
61
|
+
const retry = () => run(currentPage, committedSearch, currentPage > 1);
|
|
62
|
+
const debouncedSearch = new Debounced(() => opts.search.current, opts.debounceMs.current);
|
|
63
|
+
watch(() => debouncedSearch.current, (search) => {
|
|
64
|
+
reload(search);
|
|
65
|
+
}, { lazy: true });
|
|
66
|
+
let previousDependency = $state(opts.dependency.current);
|
|
67
|
+
watch(() => $state.snapshot(opts.dependency.current), (dependency) => {
|
|
68
|
+
if (Object.is(previousDependency, dependency))
|
|
69
|
+
return;
|
|
70
|
+
previousDependency = dependency;
|
|
71
|
+
abortCurrent();
|
|
72
|
+
opts.items.current = [];
|
|
73
|
+
opts.search.current = '';
|
|
74
|
+
opts.onDependencyReset?.(dependency);
|
|
75
|
+
error = null;
|
|
76
|
+
hasMore = false;
|
|
77
|
+
currentPage = 1;
|
|
78
|
+
committedSearch = '';
|
|
79
|
+
reload('');
|
|
80
|
+
}, { lazy: true });
|
|
81
|
+
onMount(() => {
|
|
82
|
+
if (opts.loadImmediate.current)
|
|
83
|
+
reload(opts.search.current);
|
|
84
|
+
return () => abortCurrent();
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
get status() {
|
|
88
|
+
return status;
|
|
89
|
+
},
|
|
90
|
+
get error() {
|
|
91
|
+
return error;
|
|
92
|
+
},
|
|
93
|
+
get hasMore() {
|
|
94
|
+
return hasMore;
|
|
95
|
+
},
|
|
96
|
+
get currentPage() {
|
|
97
|
+
return currentPage;
|
|
98
|
+
},
|
|
99
|
+
get isLoading() {
|
|
100
|
+
return (status === ASYNC_STATUS.LOADING || status === ASYNC_STATUS.SEARCHING || status === ASYNC_STATUS.LOADING_MORE);
|
|
101
|
+
},
|
|
102
|
+
get isSearching() {
|
|
103
|
+
return status === ASYNC_STATUS.LOADING || status === ASYNC_STATUS.SEARCHING;
|
|
104
|
+
},
|
|
105
|
+
get isLoadingMore() {
|
|
106
|
+
return status === ASYNC_STATUS.LOADING_MORE;
|
|
107
|
+
},
|
|
108
|
+
reload,
|
|
109
|
+
loadMore,
|
|
110
|
+
retry,
|
|
111
|
+
abort: abortCurrent,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
import { afterTick, boxWith } from 'svelte-toolbelt';
|
|
7
7
|
import { cubicInOut } from 'svelte/easing';
|
|
8
8
|
import { fly } from 'svelte/transition';
|
|
9
|
-
import { CommandPrimitive
|
|
9
|
+
import { CommandPrimitive } from '../../../command/index.js';
|
|
10
10
|
import { Icon, Icon as IconComponent } from '../../../icons/index.js';
|
|
11
11
|
import { Kbd } from '../../../kbd/index.js';
|
|
12
12
|
import { PopoverPrimitive } from '../../../popover/index.js';
|
|
13
13
|
import { comboboxTriggerVariants } from '../../styles.js';
|
|
14
|
-
import type { ComboboxProps } from '../../types.js';
|
|
14
|
+
import type { ComboboxItem, ComboboxProps } from '../../types.js';
|
|
15
15
|
import { useDisplayValue } from '../use-display-value.svelte.js';
|
|
16
16
|
import { useGroupedItems } from '../use-grouped-items.svelte.js';
|
|
17
17
|
import { useSelectedItems } from '../use-selected-items.svelte.js';
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
return typeof value === 'string' && value.trim() !== '';
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
function onSelectItem(item:
|
|
87
|
+
function onSelectItem(item: ComboboxItem) {
|
|
88
88
|
if (Array.isArray(value)) {
|
|
89
89
|
let result = value;
|
|
90
90
|
if (value.includes(item.value)) {
|
|
@@ -123,11 +123,14 @@
|
|
|
123
123
|
{#snippet clearBtn()}
|
|
124
124
|
{#if clearable && isNotEmpty}
|
|
125
125
|
<button
|
|
126
|
+
aria-label="Clear selection"
|
|
126
127
|
class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:cursor-pointer cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
|
|
127
128
|
type="button"
|
|
128
129
|
onclick={(event) => {
|
|
129
130
|
event.stopPropagation();
|
|
130
|
-
|
|
131
|
+
const nextValue = isMultiple ? [] : '';
|
|
132
|
+
value = nextValue;
|
|
133
|
+
onSelect(nextValue, null);
|
|
131
134
|
}}
|
|
132
135
|
>
|
|
133
136
|
<Icon.Cross class="cgui:ms-auto" width={16} height={16} />
|
|
@@ -166,7 +169,7 @@
|
|
|
166
169
|
{/if}
|
|
167
170
|
{/snippet}
|
|
168
171
|
|
|
169
|
-
{#snippet renderItem(item:
|
|
172
|
+
{#snippet renderItem(item: ComboboxItem)}
|
|
170
173
|
{@const { value: itemValue, label, icon: Icon, shortcut, onSelect: _, ...restItem } = item}
|
|
171
174
|
{@const itemAttrs = {
|
|
172
175
|
value: itemValue,
|
|
@@ -205,6 +208,16 @@
|
|
|
205
208
|
{/if}
|
|
206
209
|
{/snippet}
|
|
207
210
|
|
|
211
|
+
{#snippet renderEmpty()}
|
|
212
|
+
{#if typeof empty === 'string'}
|
|
213
|
+
<CommandPrimitive.Empty>{empty}</CommandPrimitive.Empty>
|
|
214
|
+
{:else if empty}
|
|
215
|
+
{@render empty({ props: {} })}
|
|
216
|
+
{:else}
|
|
217
|
+
<CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
|
|
218
|
+
{/if}
|
|
219
|
+
{/snippet}
|
|
220
|
+
|
|
208
221
|
<PopoverPrimitive.Root bind:open {...restProps}>
|
|
209
222
|
{@render renderTrigger()}
|
|
210
223
|
|
|
@@ -215,7 +228,7 @@
|
|
|
215
228
|
|
|
216
229
|
<CommandPrimitive.List class="cgui:rounded-inherit" style={{ maxHeight: maxContentHeight }}>
|
|
217
230
|
<CommandPrimitive.Viewport>
|
|
218
|
-
|
|
231
|
+
{@render renderEmpty()}
|
|
219
232
|
|
|
220
233
|
{#each groupedItems.items.current as [groupKey, items] (groupKey || '__ungrouped__')}
|
|
221
234
|
{#if groupKey}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { CommandCollection } from '../../command/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { ComboboxItem } from '../types.js';
|
|
3
|
+
import { type ReadableBox, type ReadableBoxedValues } from 'svelte-toolbelt';
|
|
3
4
|
type Opts = ReadableBoxedValues<{
|
|
4
5
|
value: string | string[];
|
|
5
6
|
collection: CommandCollection;
|
|
6
7
|
placeholder: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
8
|
+
}> & {
|
|
9
|
+
selectedItems?: ReadableBox<ComboboxItem[]>;
|
|
10
|
+
};
|
|
11
|
+
export declare const useDisplayValue: (opts: Opts) => ReadableBox<string>;
|
|
9
12
|
export {};
|
|
@@ -2,14 +2,18 @@ import { boxWith } from 'svelte-toolbelt';
|
|
|
2
2
|
export const useDisplayValue = (opts) => {
|
|
3
3
|
const res = $derived.by(() => {
|
|
4
4
|
const { value, collection, placeholder } = opts;
|
|
5
|
+
const findItem = (itemValue) => {
|
|
6
|
+
return (collection.current.items.find((item) => item.value === itemValue) ??
|
|
7
|
+
opts.selectedItems?.current.find((item) => item.value === itemValue));
|
|
8
|
+
};
|
|
5
9
|
if (typeof value.current === 'string' && value.current.trim() !== '') {
|
|
6
|
-
const item =
|
|
10
|
+
const item = findItem(value.current);
|
|
7
11
|
return item?.label ?? value.current;
|
|
8
12
|
}
|
|
9
13
|
if (Array.isArray(value.current) && value.current.length > 0) {
|
|
10
14
|
return value.current
|
|
11
15
|
.map((v) => {
|
|
12
|
-
const item =
|
|
16
|
+
const item = findItem(v);
|
|
13
17
|
return item?.label ?? v;
|
|
14
18
|
})
|
|
15
19
|
.join(', ');
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { CommandCollection
|
|
2
|
-
import {
|
|
1
|
+
import type { CommandCollection } from '../../command/types.js';
|
|
2
|
+
import type { ComboboxItem } from '../types.js';
|
|
3
|
+
import { type ReadableBox, type ReadableBoxedValues } from 'svelte-toolbelt';
|
|
3
4
|
type Opts = ReadableBoxedValues<{
|
|
4
5
|
value: string | string[];
|
|
5
6
|
collection: CommandCollection;
|
|
6
|
-
}
|
|
7
|
-
|
|
7
|
+
}> & {
|
|
8
|
+
selectedItems?: ReadableBox<ComboboxItem[]>;
|
|
9
|
+
};
|
|
10
|
+
export declare const useSelectedItems: (opts: Opts) => ReadableBox<import("../../command/types.js").CommandItem[]>;
|
|
8
11
|
export {};
|
|
@@ -2,14 +2,16 @@ import { boxWith } from 'svelte-toolbelt';
|
|
|
2
2
|
export const useSelectedItems = (opts) => {
|
|
3
3
|
const { value, collection } = opts;
|
|
4
4
|
const res = $derived.by(() => {
|
|
5
|
+
const findItem = (itemValue) => {
|
|
6
|
+
return (collection.current.items.find((item) => item.value === itemValue) ??
|
|
7
|
+
opts.selectedItems?.current.find((item) => item.value === itemValue));
|
|
8
|
+
};
|
|
5
9
|
if (typeof value.current === 'string' && value.current.trim() !== '') {
|
|
6
|
-
const item =
|
|
10
|
+
const item = findItem(value.current);
|
|
7
11
|
return item ? [item] : [];
|
|
8
12
|
}
|
|
9
13
|
if (Array.isArray(value.current) && value.current.length > 0) {
|
|
10
|
-
return value.current
|
|
11
|
-
.map((v) => collection.current.items.find((i) => i.value === v))
|
|
12
|
-
.filter((i) => i !== undefined);
|
|
14
|
+
return value.current.map((v) => findItem(v)).filter((i) => i !== undefined);
|
|
13
15
|
}
|
|
14
16
|
return [];
|
|
15
17
|
});
|
|
@@ -4,6 +4,7 @@ import type { CommandGroup, CommandItem, CommandProps } from '../command/index.j
|
|
|
4
4
|
import type { PrimitivePopoverRootProps } from '../popover/types.js';
|
|
5
5
|
import type { ComboboxTriggerVariantsProps } from './styles.js';
|
|
6
6
|
export type ComboboxRootProps = PrimitivePopoverRootProps;
|
|
7
|
+
export type ComboboxItem = CommandItem;
|
|
7
8
|
/**
|
|
8
9
|
* Base props shared by both single and multiple selection modes
|
|
9
10
|
*/
|
|
@@ -29,10 +30,10 @@ type ComboboxBaseProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsProp
|
|
|
29
30
|
label?: Snippet<[{
|
|
30
31
|
placeholder: string;
|
|
31
32
|
value: string | string[];
|
|
32
|
-
selectedItems:
|
|
33
|
+
selectedItems: ComboboxItem[];
|
|
33
34
|
}]>;
|
|
34
35
|
/** Callback when selection changes */
|
|
35
|
-
onSelect?: (value: string | string[], item:
|
|
36
|
+
onSelect?: (value: string | string[], item: ComboboxItem | null) => void;
|
|
36
37
|
};
|
|
37
38
|
type ComboboxSingleProps = ComboboxBaseProps & {
|
|
38
39
|
type: typeof SELECTION_TYPE.SINGLE;
|
|
@@ -46,58 +47,63 @@ export type ComboboxProps = ComboboxSingleProps | ComboboxMultipleProps;
|
|
|
46
47
|
/**
|
|
47
48
|
* Async Combobox Types
|
|
48
49
|
*/
|
|
49
|
-
export type
|
|
50
|
+
export type ComboboxAsyncLoadParams = {
|
|
50
51
|
page: number;
|
|
51
52
|
pageSize: number;
|
|
52
53
|
search: string;
|
|
54
|
+
signal: AbortSignal;
|
|
55
|
+
dependency?: unknown;
|
|
53
56
|
};
|
|
54
|
-
export type
|
|
57
|
+
export type ComboboxAsyncLoadResult<TItem extends ComboboxItem> = {
|
|
55
58
|
items: TItem[];
|
|
56
59
|
hasMore: boolean;
|
|
57
60
|
};
|
|
58
|
-
export type
|
|
59
|
-
|
|
61
|
+
export type ComboboxAsyncLoader<TItem extends ComboboxItem = ComboboxItem> = (params: ComboboxAsyncLoadParams) => Promise<ComboboxAsyncLoadResult<TItem>>;
|
|
62
|
+
/** @deprecated Use ComboboxAsyncLoadParams instead. */
|
|
63
|
+
export type ComboboxAsyncCallbackParams = ComboboxAsyncLoadParams;
|
|
64
|
+
/** @deprecated Use ComboboxAsyncLoadResult instead. */
|
|
65
|
+
export type ComboboxAsyncCallbackResult<TItem extends ComboboxItem> = ComboboxAsyncLoadResult<TItem>;
|
|
66
|
+
/** @deprecated Use ComboboxAsyncLoader instead. */
|
|
67
|
+
export type ComboboxAsyncCallback<TItem extends ComboboxItem = ComboboxItem> = ComboboxAsyncLoader<TItem>;
|
|
68
|
+
type ComboboxAsyncLoadProp<TItem extends ComboboxItem> = {
|
|
69
|
+
/** Preferred async loader API */
|
|
70
|
+
loadItems: ComboboxAsyncLoader<TItem>;
|
|
71
|
+
callback?: never;
|
|
72
|
+
} | {
|
|
73
|
+
/** @deprecated Use loadItems instead. Kept for backwards compatibility. */
|
|
74
|
+
callback: ComboboxAsyncLoader<TItem>;
|
|
75
|
+
loadItems?: never;
|
|
76
|
+
};
|
|
77
|
+
type ComboboxAsyncBaseProps<TItem extends ComboboxItem> = Omit<ComboboxBaseProps, 'collection' | 'item' | 'onSelect'> & ComboboxAsyncLoadProp<TItem> & {
|
|
60
78
|
items?: TItem[];
|
|
79
|
+
/** Cache of selected item objects, useful when selected values are not in the currently loaded async page. */
|
|
80
|
+
selectedItems?: TItem[];
|
|
61
81
|
item?: Snippet<[{
|
|
62
82
|
item: TItem;
|
|
63
83
|
props: Record<string, unknown>;
|
|
64
84
|
}]>;
|
|
65
|
-
/** Callback when selection changes */
|
|
66
85
|
onSelect?: (value: string | string[], item: TItem | null) => void;
|
|
67
|
-
/** Loading state renderer */
|
|
68
86
|
loading?: Snippet;
|
|
69
|
-
/** Current page (default: 1) */
|
|
70
|
-
page?: number;
|
|
71
|
-
/** Page size for pagination (default: 20) */
|
|
72
87
|
pageSize?: number;
|
|
73
|
-
/** Async data callback */
|
|
74
|
-
callback: ComboboxAsyncCallback<TItem>;
|
|
75
|
-
/** Load immediate data when combobox is mounted (default: true) */
|
|
76
88
|
loadImmediate?: boolean;
|
|
77
|
-
/** Group definitions (for labels/ordering) */
|
|
78
89
|
groups?: CommandGroup[];
|
|
79
|
-
/**
|
|
90
|
+
/** Preferred name */
|
|
91
|
+
debounceMs?: number;
|
|
92
|
+
/** @deprecated Use debounceMs instead. */
|
|
80
93
|
searchDebounce?: number;
|
|
81
|
-
/**
|
|
82
|
-
* Getter function for dependency tracking. When the returned value changes,
|
|
83
|
-
* the component will reset items, page to 1, clear selected value, and reload data.
|
|
84
|
-
* @example dependsOn={() => parentSelectValue}
|
|
85
|
-
*/
|
|
86
94
|
dependsOn?: () => unknown;
|
|
87
|
-
/**
|
|
88
|
-
* Whether to clear the selected value when dependsOn changes **(default: true)**
|
|
89
|
-
*/
|
|
90
95
|
clearOnDependencyChange?: boolean;
|
|
96
|
+
onDependencyChange?: (value: unknown) => void;
|
|
91
97
|
};
|
|
92
|
-
type ComboboxAsyncSingleProps<TItem extends
|
|
98
|
+
type ComboboxAsyncSingleProps<TItem extends ComboboxItem> = ComboboxAsyncBaseProps<TItem> & {
|
|
93
99
|
/** Selection mode (default: 'single') */
|
|
94
100
|
type?: 'single';
|
|
95
101
|
value?: string;
|
|
96
102
|
};
|
|
97
|
-
type ComboboxAsyncMultipleProps<TItem extends
|
|
103
|
+
type ComboboxAsyncMultipleProps<TItem extends ComboboxItem> = ComboboxAsyncBaseProps<TItem> & {
|
|
98
104
|
/** Selection mode */
|
|
99
105
|
type: 'multiple';
|
|
100
106
|
value?: string[];
|
|
101
107
|
};
|
|
102
|
-
export type ComboboxAsyncProps<TItem extends
|
|
108
|
+
export type ComboboxAsyncProps<TItem extends ComboboxItem> = ComboboxAsyncSingleProps<TItem> | ComboboxAsyncMultipleProps<TItem>;
|
|
103
109
|
export {};
|