@ibobbyts/svelte-ui-utils 0.1.2 → 0.2.1

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.
Files changed (59) hide show
  1. package/README.md +112 -15
  2. package/dist/data-table/BaseDataTable.svelte +70 -41
  3. package/dist/data-table/BaseDataTable.svelte.d.ts +1 -0
  4. package/dist/data-table/BaseDataTable.svelte.d.ts.map +1 -1
  5. package/dist/data-table/DataTable.svelte +4 -0
  6. package/dist/data-table/DataTable.svelte.d.ts +1 -0
  7. package/dist/data-table/DataTable.svelte.d.ts.map +1 -1
  8. package/dist/data-table/DateRangeFilter.svelte +27 -0
  9. package/dist/data-table/DateRangeFilter.svelte.d.ts +1 -0
  10. package/dist/data-table/DateRangeFilter.svelte.d.ts.map +1 -1
  11. package/dist/data-table/FilterControl.svelte +3 -0
  12. package/dist/data-table/FilterControl.svelte.d.ts.map +1 -1
  13. package/dist/data-table/FilterTable.svelte +2 -0
  14. package/dist/data-table/FilterTable.svelte.d.ts.map +1 -1
  15. package/dist/data-table/NumberRangeFilter.svelte +2 -0
  16. package/dist/data-table/NumberRangeFilter.svelte.d.ts.map +1 -1
  17. package/dist/data-table/index.d.ts +1 -1
  18. package/dist/data-table/index.d.ts.map +1 -1
  19. package/dist/data-table/types.d.ts +8 -2
  20. package/dist/data-table/types.d.ts.map +1 -1
  21. package/dist/dropdown/Dropdown.svelte +2 -0
  22. package/dist/dropdown/Dropdown.svelte.d.ts.map +1 -1
  23. package/dist/dropdown-search/DropdownSearch.svelte +358 -38
  24. package/dist/dropdown-search/DropdownSearch.svelte.d.ts +8 -1
  25. package/dist/dropdown-search/DropdownSearch.svelte.d.ts.map +1 -1
  26. package/dist/dropdown-search/DropdownSearchMultiSelect.svelte +92 -0
  27. package/dist/dropdown-search/DropdownSearchMultiSelect.svelte.d.ts +53 -0
  28. package/dist/dropdown-search/DropdownSearchMultiSelect.svelte.d.ts.map +1 -0
  29. package/dist/dropdown-search/index.d.ts +3 -2
  30. package/dist/dropdown-search/index.d.ts.map +1 -1
  31. package/dist/dropdown-search/index.js +2 -1
  32. package/dist/dropdown-search/state.d.ts +2 -0
  33. package/dist/dropdown-search/state.d.ts.map +1 -1
  34. package/dist/dropdown-search/state.js +7 -0
  35. package/dist/dropdown-search/types.d.ts +8 -0
  36. package/dist/dropdown-search/types.d.ts.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/modal/ConfirmDialog.svelte +69 -0
  41. package/dist/modal/ConfirmDialog.svelte.d.ts +44 -0
  42. package/dist/modal/ConfirmDialog.svelte.d.ts.map +1 -0
  43. package/dist/modal/Modal.svelte +131 -0
  44. package/dist/modal/Modal.svelte.d.ts +43 -0
  45. package/dist/modal/Modal.svelte.d.ts.map +1 -0
  46. package/dist/modal/index.d.ts +4 -0
  47. package/dist/modal/index.d.ts.map +1 -0
  48. package/dist/modal/index.js +2 -0
  49. package/dist/modal/types.d.ts +3 -0
  50. package/dist/modal/types.d.ts.map +1 -0
  51. package/dist/modal/types.js +1 -0
  52. package/dist/pagination/Pagination.svelte +2 -0
  53. package/dist/pagination/Pagination.svelte.d.ts.map +1 -1
  54. package/dist/style.css +295 -4
  55. package/dist/toast/Toast.svelte +2 -0
  56. package/dist/toast/Toast.svelte.d.ts.map +1 -1
  57. package/dist/toast/ToastManager.svelte +2 -0
  58. package/dist/toast/ToastManager.svelte.d.ts.map +1 -1
  59. package/package.json +6 -1
@@ -1,3 +1,5 @@
1
+ <svelte:options runes={false} />
2
+
1
3
  <script lang="ts">
2
4
  import type { DropdownChangeHandler, DropdownOption, DropdownPlacement, DropdownValue } from './types.js';
3
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAuI1G,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAV8I,MAAM,GAAG,SAAS;YAAU,aAAa;cAAY,cAAc,EAAE;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;;eAAyC,qBAAqB,GAAG,SAAS;;;kBAU7Q,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAyI1G,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAV8I,MAAM,GAAG,SAAS;YAAU,aAAa;cAAY,cAAc,EAAE;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;;eAAyC,qBAAqB,GAAG,SAAS;;;kBAU7Q,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
@@ -1,18 +1,29 @@
1
+ <svelte:options runes={false} />
2
+
1
3
  <script lang="ts">
2
4
  import { onDestroy, onMount } from 'svelte';
3
5
  import { getUiMessages, type UiLanguage } from '../i18n.js';
4
- import { formatParamDict, normalizeDropdownSearchValue, resolveDropdownSearchStatus } from './state.js';
6
+ import {
7
+ formatParamDict,
8
+ normalizeDropdownSearchValue,
9
+ resolveDropdownSearchStatus,
10
+ } from './state.js';
5
11
  import type {
6
12
  DropdownSearchChangeDetail,
13
+ DropdownSearchEnterDetail,
7
14
  DropdownSearchItem,
8
15
  DropdownSearchItemValueGetter,
9
16
  DropdownSearchLoadOptions,
10
- DropdownSearchStatus
17
+ DropdownSearchSelectedItemLabelGetter,
18
+ DropdownSearchSelectedItemsChangeHandler,
19
+ DropdownSearchStatus,
11
20
  } from './types.js';
12
21
 
13
22
  export let value = '';
14
23
  export let selectedItem: DropdownSearchItem | null = null;
24
+ export let selectedItems: DropdownSearchItem[] = [];
15
25
  export let status: DropdownSearchStatus = 'empty';
26
+ export let multiselect = false;
16
27
  export let placeholder = '';
17
28
  export let debounceMs = 500;
18
29
  export let limit = 10;
@@ -32,37 +43,67 @@
32
43
  export let width: string | undefined = undefined;
33
44
  export let minWidth: string | undefined = undefined;
34
45
  export let maxWidth: string | undefined = undefined;
46
+ export let selectedItemsLabel = 'Selected items';
47
+ export let removeSelectedItemLabel:
48
+ | DropdownSearchSelectedItemLabelGetter
49
+ | undefined = undefined;
35
50
  export let getItemValue: DropdownSearchItemValueGetter = (item) => item.title;
36
- export let onChange: ((detail: DropdownSearchChangeDetail) => void) | undefined = undefined;
37
- export let onSelect: ((item: DropdownSearchItem) => void) | undefined = undefined;
38
- export let onStatusChange: ((status: DropdownSearchStatus) => void) | undefined = undefined;
51
+ export let onChange:
52
+ | ((detail: DropdownSearchChangeDetail) => void)
53
+ | undefined = undefined;
54
+ export let onSelect: ((item: DropdownSearchItem) => void) | undefined =
55
+ undefined;
56
+ export let onDeselect: ((item: DropdownSearchItem) => void) | undefined =
57
+ undefined;
58
+ export let onSelectedItemsChange:
59
+ | DropdownSearchSelectedItemsChangeHandler
60
+ | undefined = undefined;
61
+ export let onStatusChange:
62
+ | ((status: DropdownSearchStatus) => void)
63
+ | undefined = undefined;
39
64
  export let onInputBlur: ((event: FocusEvent) => void) | undefined = undefined;
65
+ export let onEnter: ((detail: DropdownSearchEnterDetail) => void) | undefined =
66
+ undefined;
40
67
 
41
68
  let options: DropdownSearchItem[] = [];
42
69
  let exactMatch: DropdownSearchItem | null = null;
43
70
  let focused = false;
71
+ let inputValue = value;
44
72
  let inputElement: HTMLInputElement | undefined;
45
73
  let searchTimer: ReturnType<typeof setTimeout> | undefined;
74
+ let blurTimer: ReturnType<typeof setTimeout> | undefined;
46
75
  let activeController: AbortController | undefined;
47
76
  let requestId = 0;
48
77
  let lastHandledValue = value;
49
78
 
50
79
  $: messages = getUiMessages(language);
51
- $: resolvedNoResultsText = noResultsText?.trim() ? noResultsText : messages.dropdownSearch.noResultsText;
52
- $: resolvedLoadingText = loadingText?.trim() ? loadingText : messages.dropdownSearch.loadingText;
53
- $: resolvedClearLabel = clearLabel?.trim() ? clearLabel : messages.dropdownSearch.clearLabel;
80
+ $: resolvedNoResultsText = noResultsText?.trim()
81
+ ? noResultsText
82
+ : messages.dropdownSearch.noResultsText;
83
+ $: resolvedLoadingText = loadingText?.trim()
84
+ ? loadingText
85
+ : messages.dropdownSearch.loadingText;
86
+ $: resolvedClearLabel = clearLabel?.trim()
87
+ ? clearLabel
88
+ : messages.dropdownSearch.clearLabel;
54
89
  $: resolvedListboxId = listboxId ?? (id ? `${id}-options` : undefined);
55
90
  $: hasQuery = normalizeDropdownSearchValue(value).length > 0;
56
- $: showOptions = focused && !disabled && (options.length > 0 || status === 'loading' || (hasQuery && status === 'invalid'));
91
+ $: showOptions =
92
+ focused &&
93
+ !disabled &&
94
+ (hasQuery || (multiselect && options.length > 0)) &&
95
+ (options.length > 0 ||
96
+ status === 'loading' ||
97
+ (validate && hasQuery && options.length === 0));
57
98
  $: if (!validate && status !== 'empty' && status !== 'loading') {
58
99
  setStatus('empty');
59
100
  }
60
- $: if (searchOnExternalValueChange && value !== lastHandledValue) {
61
- handleExternalValue(value);
101
+ $: if (value !== lastHandledValue && value !== inputValue) {
102
+ handleIncomingValue(value);
62
103
  }
63
104
 
64
105
  function emitChange() {
65
- const detail = { value, selectedItem, status };
106
+ const detail = { value, selectedItem, selectedItems, status };
66
107
  onChange?.(detail);
67
108
  }
68
109
 
@@ -76,7 +117,9 @@
76
117
  function resetSearchState(nextValue: string) {
77
118
  lastHandledValue = nextValue;
78
119
  value = nextValue;
79
- selectedItem = null;
120
+ if (!multiselect) {
121
+ selectedItem = null;
122
+ }
80
123
  exactMatch = null;
81
124
  options = [];
82
125
  const trimmed = normalizeDropdownSearchValue(nextValue);
@@ -85,7 +128,17 @@
85
128
  setStatus('loading');
86
129
  } else {
87
130
  abortActiveSearch();
88
- setStatus(resolveDropdownSearchStatus({ value, selectedItem, exactMatch, minLength, validate }));
131
+ setStatus(
132
+ resolveDropdownSearchStatus({
133
+ value,
134
+ selectedItem,
135
+ selectedItems,
136
+ exactMatch,
137
+ minLength,
138
+ validate,
139
+ multiselect,
140
+ }),
141
+ );
89
142
  }
90
143
  emitChange();
91
144
  }
@@ -97,6 +150,13 @@
97
150
  }
98
151
  }
99
152
 
153
+ function clearBlurTimer() {
154
+ if (blurTimer) {
155
+ clearTimeout(blurTimer);
156
+ blurTimer = undefined;
157
+ }
158
+ }
159
+
100
160
  function abortActiveSearch() {
101
161
  if (activeController) {
102
162
  activeController.abort();
@@ -111,7 +171,15 @@
111
171
  abortActiveSearch();
112
172
  options = [];
113
173
  exactMatch = null;
114
- setStatus(resolveDropdownSearchStatus({ value: query, minLength, validate }));
174
+ setStatus(
175
+ resolveDropdownSearchStatus({
176
+ value: query,
177
+ selectedItems,
178
+ minLength,
179
+ validate,
180
+ multiselect,
181
+ }),
182
+ );
115
183
  emitChange();
116
184
  return;
117
185
  }
@@ -124,7 +192,10 @@
124
192
  emitChange();
125
193
 
126
194
  try {
127
- const result = await loadOptions(trimmed, { limit, signal: controller.signal });
195
+ const result = await loadOptions(trimmed, {
196
+ limit,
197
+ signal: controller.signal,
198
+ });
128
199
 
129
200
  if (controller.signal.aborted || currentRequestId !== requestId) {
130
201
  return;
@@ -132,8 +203,21 @@
132
203
 
133
204
  options = result.options ?? [];
134
205
  exactMatch = validate ? (result.exactMatch ?? null) : null;
135
- selectedItem = validate && exactMatch && !exactMatch.disabled ? exactMatch : null;
136
- setStatus(resolveDropdownSearchStatus({ value, selectedItem, exactMatch, minLength, validate }));
206
+ selectedItem =
207
+ !multiselect && validate && exactMatch && !exactMatch.disabled
208
+ ? exactMatch
209
+ : null;
210
+ setStatus(
211
+ resolveDropdownSearchStatus({
212
+ value,
213
+ selectedItem,
214
+ selectedItems,
215
+ exactMatch: multiselect ? null : exactMatch,
216
+ minLength,
217
+ validate,
218
+ multiselect,
219
+ }),
220
+ );
137
221
  emitChange();
138
222
  } catch (error) {
139
223
  if (controller.signal.aborted || currentRequestId !== requestId) {
@@ -143,7 +227,16 @@
143
227
  options = [];
144
228
  exactMatch = null;
145
229
  selectedItem = null;
146
- setStatus(resolveDropdownSearchStatus({ value: query, errored: true, minLength, validate }));
230
+ setStatus(
231
+ resolveDropdownSearchStatus({
232
+ value: query,
233
+ selectedItems,
234
+ errored: true,
235
+ minLength,
236
+ validate,
237
+ multiselect,
238
+ }),
239
+ );
147
240
  emitChange();
148
241
  } finally {
149
242
  if (activeController === controller) {
@@ -159,10 +252,57 @@
159
252
  }, debounceMs);
160
253
  }
161
254
 
255
+ function handleInputValue(nextValue: string) {
256
+ clearBlurTimer();
257
+ focused = true;
258
+ inputValue = nextValue;
259
+ resetSearchState(nextValue);
260
+ scheduleSearch(nextValue);
261
+ }
262
+
162
263
  function handleInput(event: Event) {
163
264
  const input = event.currentTarget as HTMLInputElement;
164
- resetSearchState(input.value);
165
- scheduleSearch(input.value);
265
+ handleInputValue(input.value);
266
+ }
267
+
268
+ function handleBeforeInput() {
269
+ setTimeout(() => {
270
+ if (!inputElement || inputElement.value === lastHandledValue) {
271
+ return;
272
+ }
273
+
274
+ handleInputValue(inputElement.value);
275
+ }, 0);
276
+ }
277
+
278
+ function handleFocus() {
279
+ clearBlurTimer();
280
+ focused = true;
281
+ }
282
+
283
+ function handleBlur(event: FocusEvent) {
284
+ onInputBlur?.(event);
285
+ clearBlurTimer();
286
+ blurTimer = setTimeout(() => {
287
+ focused = document.activeElement === inputElement;
288
+ blurTimer = undefined;
289
+ }, 120);
290
+ }
291
+
292
+ function handleKeydown(event: KeyboardEvent) {
293
+ if (event.key !== 'Enter' || multiselect) {
294
+ return;
295
+ }
296
+
297
+ onEnter?.({
298
+ event,
299
+ value,
300
+ selectedItem,
301
+ selectedItems,
302
+ status,
303
+ exactMatch,
304
+ options,
305
+ });
166
306
  }
167
307
 
168
308
  function selectItem(item: DropdownSearchItem) {
@@ -170,11 +310,17 @@
170
310
  return;
171
311
  }
172
312
 
313
+ if (multiselect) {
314
+ toggleSelectedItem(item);
315
+ return;
316
+ }
317
+
173
318
  clearSearchTimer();
174
319
  abortActiveSearch();
175
320
  selectedItem = item;
176
321
  exactMatch = validate ? item : null;
177
322
  value = getItemValue(item);
323
+ inputValue = value;
178
324
  lastHandledValue = value;
179
325
  setStatus(validate ? 'valid' : 'empty');
180
326
  focused = false;
@@ -182,6 +328,85 @@
182
328
  onSelect?.(item);
183
329
  }
184
330
 
331
+ function selectedItemKey(item: DropdownSearchItem): string {
332
+ return String(item.id);
333
+ }
334
+
335
+ function isSelectedItem(item: DropdownSearchItem): boolean {
336
+ const key = selectedItemKey(item);
337
+ return selectedItems.some((selected) => selectedItemKey(selected) === key);
338
+ }
339
+
340
+ function resolveRemoveSelectedItemLabel(item: DropdownSearchItem): string {
341
+ return removeSelectedItemLabel?.(item) ?? `Remove ${item.title}`;
342
+ }
343
+
344
+ function setSelectedItems(nextItems: DropdownSearchItem[]) {
345
+ selectedItems = nextItems;
346
+ void onSelectedItemsChange?.(selectedItems);
347
+ }
348
+
349
+ function toggleSelectedItem(item: DropdownSearchItem) {
350
+ const selected = isSelectedItem(item);
351
+ const nextItems = selected
352
+ ? selectedItems.filter(
353
+ (selectedItem) =>
354
+ selectedItemKey(selectedItem) !== selectedItemKey(item),
355
+ )
356
+ : [...selectedItems, item];
357
+
358
+ clearSearchTimer();
359
+ abortActiveSearch();
360
+ setSelectedItems(nextItems);
361
+ selectedItem = null;
362
+ exactMatch = null;
363
+ value = '';
364
+ inputValue = value;
365
+ lastHandledValue = value;
366
+ setStatus(
367
+ resolveDropdownSearchStatus({
368
+ value,
369
+ selectedItems,
370
+ minLength,
371
+ validate,
372
+ multiselect,
373
+ }),
374
+ );
375
+ focused = true;
376
+ emitChange();
377
+
378
+ if (selected) {
379
+ onDeselect?.(item);
380
+ } else {
381
+ onSelect?.(item);
382
+ }
383
+
384
+ inputElement?.focus();
385
+ }
386
+
387
+ function removeSelectedItem(item: DropdownSearchItem) {
388
+ if (disabled || !multiselect) {
389
+ return;
390
+ }
391
+
392
+ const nextItems = selectedItems.filter(
393
+ (selectedItem) => selectedItemKey(selectedItem) !== selectedItemKey(item),
394
+ );
395
+ setSelectedItems(nextItems);
396
+ setStatus(
397
+ resolveDropdownSearchStatus({
398
+ value,
399
+ selectedItems,
400
+ minLength,
401
+ validate,
402
+ multiselect,
403
+ }),
404
+ );
405
+ emitChange();
406
+ onDeselect?.(item);
407
+ inputElement?.focus();
408
+ }
409
+
185
410
  function clearSearch() {
186
411
  if (!hasQuery || disabled) {
187
412
  return;
@@ -190,25 +415,65 @@
190
415
  clearSearchTimer();
191
416
  abortActiveSearch();
192
417
  value = '';
418
+ inputValue = value;
193
419
  lastHandledValue = value;
194
- selectedItem = null;
420
+ if (!multiselect) {
421
+ selectedItem = null;
422
+ }
195
423
  exactMatch = null;
196
424
  options = [];
197
- setStatus(resolveDropdownSearchStatus({ value, selectedItem, exactMatch, minLength, validate }));
425
+ setStatus(
426
+ resolveDropdownSearchStatus({
427
+ value,
428
+ selectedItem,
429
+ selectedItems,
430
+ exactMatch,
431
+ minLength,
432
+ validate,
433
+ multiselect,
434
+ }),
435
+ );
198
436
  focused = true;
199
437
  emitChange();
200
438
  inputElement?.focus();
201
439
  }
202
440
 
441
+ function handleIncomingValue(nextValue: string) {
442
+ inputValue = nextValue;
443
+
444
+ if (searchOnExternalValueChange) {
445
+ handleExternalValue(nextValue);
446
+ return;
447
+ }
448
+
449
+ lastHandledValue = nextValue;
450
+ }
451
+
203
452
  function handleExternalValue(nextValue: string) {
204
453
  lastHandledValue = nextValue;
205
- selectedItem = null;
454
+ inputValue = nextValue;
455
+ if (!multiselect) {
456
+ selectedItem = null;
457
+ }
206
458
  exactMatch = null;
207
459
  options = [];
208
460
 
209
461
  clearSearchTimer();
210
- if (!normalizeDropdownSearchValue(nextValue) || normalizeDropdownSearchValue(nextValue).length < minLength) {
211
- setStatus(resolveDropdownSearchStatus({ value: nextValue, selectedItem, exactMatch, minLength, validate }));
462
+ if (
463
+ !normalizeDropdownSearchValue(nextValue) ||
464
+ normalizeDropdownSearchValue(nextValue).length < minLength
465
+ ) {
466
+ setStatus(
467
+ resolveDropdownSearchStatus({
468
+ value: nextValue,
469
+ selectedItem,
470
+ selectedItems,
471
+ exactMatch,
472
+ minLength,
473
+ validate,
474
+ multiselect,
475
+ }),
476
+ );
212
477
  emitChange();
213
478
  return;
214
479
  }
@@ -218,6 +483,7 @@
218
483
 
219
484
  onDestroy(() => {
220
485
  clearSearchTimer();
486
+ clearBlurTimer();
221
487
  abortActiveSearch();
222
488
  });
223
489
 
@@ -230,14 +496,47 @@
230
496
 
231
497
  <div
232
498
  class={`suu-dropdown-search suu-dropdown-search--${status}`}
499
+ class:suu-dropdown-search--multi={multiselect}
233
500
  class:suu-dropdown-search--has-clear={hasQuery && !disabled}
234
501
  data-status={status}
502
+ data-multiselect={multiselect}
235
503
  style:width
236
504
  style:min-width={minWidth}
237
505
  style:max-width={maxWidth}
238
506
  >
507
+ {#if multiselect && selectedItems.length > 0}
508
+ <div
509
+ class="suu-dropdown-search__selected-items"
510
+ aria-label={selectedItemsLabel}
511
+ >
512
+ {#each selectedItems as item (item.id)}
513
+ <span class="suu-dropdown-search__selected-item">
514
+ <span class="suu-dropdown-search__selected-title">{item.title}</span>
515
+ {#if !disabled}
516
+ <button
517
+ type="button"
518
+ class="suu-dropdown-search__selected-remove"
519
+ aria-label={resolveRemoveSelectedItemLabel(item)}
520
+ title={resolveRemoveSelectedItemLabel(item)}
521
+ on:mousedown|preventDefault
522
+ on:click={() => removeSelectedItem(item)}
523
+ >
524
+ <svg viewBox="0 0 20 20" aria-hidden="true">
525
+ <path d="m6 6 8 8M14 6l-8 8"></path>
526
+ </svg>
527
+ </button>
528
+ {/if}
529
+ </span>
530
+ {/each}
531
+ </div>
532
+ {/if}
533
+
239
534
  <div class="suu-dropdown-search__field">
240
- <svg class="suu-dropdown-search__icon" viewBox="0 0 24 24" aria-hidden="true">
535
+ <svg
536
+ class="suu-dropdown-search__icon"
537
+ viewBox="0 0 24 24"
538
+ aria-hidden="true"
539
+ >
241
540
  <circle cx="11" cy="11" r="7"></circle>
242
541
  <path d="m16 16 4 4"></path>
243
542
  </svg>
@@ -246,7 +545,7 @@
246
545
  class="suu-dropdown-search__input"
247
546
  {id}
248
547
  {name}
249
- {value}
548
+ bind:value={inputValue}
250
549
  {placeholder}
251
550
  aria-label={ariaLabel}
252
551
  {disabled}
@@ -255,12 +554,11 @@
255
554
  aria-controls={resolvedListboxId}
256
555
  aria-expanded={showOptions}
257
556
  aria-invalid={status === 'invalid' || status === 'error'}
557
+ on:beforeinput={handleBeforeInput}
258
558
  on:input={handleInput}
259
- on:focus={() => (focused = true)}
260
- on:blur={(event) => {
261
- onInputBlur?.(event);
262
- setTimeout(() => (focused = false), 120);
263
- }}
559
+ on:keydown={handleKeydown}
560
+ on:focus={handleFocus}
561
+ on:blur={handleBlur}
264
562
  />
265
563
  {#if hasQuery && !disabled}
266
564
  <button
@@ -280,10 +578,19 @@
280
578
 
281
579
  {#if showOptions}
282
580
  <div class="suu-dropdown-search__menu">
283
- <div class="suu-dropdown-search__menu-panel" id={resolvedListboxId} role="listbox">
581
+ <div
582
+ class="suu-dropdown-search__menu-panel"
583
+ id={resolvedListboxId}
584
+ role="listbox"
585
+ aria-multiselectable={multiselect}
586
+ >
284
587
  {#if status === 'loading'}
285
- <div class="suu-dropdown-search__empty suu-dropdown-search__empty--loading" aria-live="polite">
286
- <span class="suu-dropdown-search__spinner" aria-hidden="true"></span>
588
+ <div
589
+ class="suu-dropdown-search__empty suu-dropdown-search__empty--loading"
590
+ aria-live="polite"
591
+ >
592
+ <span class="suu-dropdown-search__spinner" aria-hidden="true"
593
+ ></span>
287
594
  <span>{resolvedLoadingText}</span>
288
595
  </div>
289
596
  {:else if options.length > 0}
@@ -292,14 +599,27 @@
292
599
  type="button"
293
600
  class="suu-dropdown-search__option"
294
601
  class:suu-dropdown-search__option--disabled={option.disabled}
602
+ class:suu-dropdown-search__option--selected={multiselect &&
603
+ isSelectedItem(option)}
295
604
  role="option"
296
- aria-selected={selectedItem?.id === option.id}
605
+ aria-selected={multiselect
606
+ ? isSelectedItem(option)
607
+ : selectedItem?.id === option.id}
297
608
  disabled={option.disabled}
298
609
  on:mousedown|preventDefault={() => selectItem(option)}
299
610
  >
611
+ {#if multiselect}
612
+ <span class="suu-dropdown-search__check" aria-hidden="true">
613
+ <svg viewBox="0 0 20 20">
614
+ <path d="m4.5 10.5 3.5 3.5 7.5-8"></path>
615
+ </svg>
616
+ </span>
617
+ {/if}
300
618
  <span class="suu-dropdown-search__title">{option.title}</span>
301
619
  {#if formatParamDict(option.param_dict).length > 0}
302
- <span class="suu-dropdown-search__meta">{formatParamDict(option.param_dict).join(' · ')}</span>
620
+ <span class="suu-dropdown-search__meta"
621
+ >{formatParamDict(option.param_dict).join(' · ')}</span
622
+ >
303
623
  {/if}
304
624
  </button>
305
625
  {/each}
@@ -1,5 +1,5 @@
1
1
  import { type UiLanguage } from '../i18n.js';
2
- import type { DropdownSearchChangeDetail, DropdownSearchItem, DropdownSearchItemValueGetter, DropdownSearchLoadOptions, DropdownSearchStatus } from './types.js';
2
+ import type { DropdownSearchChangeDetail, DropdownSearchEnterDetail, DropdownSearchItem, DropdownSearchItemValueGetter, DropdownSearchLoadOptions, DropdownSearchSelectedItemLabelGetter, DropdownSearchSelectedItemsChangeHandler, DropdownSearchStatus } from './types.js';
3
3
  interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
4
4
  new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
5
5
  $$bindings?: Bindings;
@@ -16,7 +16,9 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
16
16
  declare const DropdownSearch: $$__sveltets_2_IsomorphicComponent<{
17
17
  value?: string;
18
18
  selectedItem?: DropdownSearchItem | null;
19
+ selectedItems?: DropdownSearchItem[];
19
20
  status?: DropdownSearchStatus;
21
+ multiselect?: boolean;
20
22
  placeholder?: string;
21
23
  debounceMs?: number;
22
24
  limit?: number;
@@ -36,11 +38,16 @@ declare const DropdownSearch: $$__sveltets_2_IsomorphicComponent<{
36
38
  width?: string | undefined;
37
39
  minWidth?: string | undefined;
38
40
  maxWidth?: string | undefined;
41
+ selectedItemsLabel?: string;
42
+ removeSelectedItemLabel?: DropdownSearchSelectedItemLabelGetter | undefined;
39
43
  getItemValue?: DropdownSearchItemValueGetter;
40
44
  onChange?: ((detail: DropdownSearchChangeDetail) => void) | undefined;
41
45
  onSelect?: ((item: DropdownSearchItem) => void) | undefined;
46
+ onDeselect?: ((item: DropdownSearchItem) => void) | undefined;
47
+ onSelectedItemsChange?: DropdownSearchSelectedItemsChangeHandler | undefined;
42
48
  onStatusChange?: ((status: DropdownSearchStatus) => void) | undefined;
43
49
  onInputBlur?: ((event: FocusEvent) => void) | undefined;
50
+ onEnter?: ((detail: DropdownSearchEnterDetail) => void) | undefined;
44
51
  }, {
45
52
  mousedown: MouseEvent;
46
53
  } & {
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownSearch.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown-search/DropdownSearch.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,KAAK,EACR,0BAA0B,EAC1B,kBAAkB,EAClB,6BAA6B,EAC7B,yBAAyB,EACzB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAgRtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,cAAc;;mBAVoqB,kBAAkB,GAAG,IAAI;aAAW,oBAAoB;;;;;;iBAAiK,yBAAyB;SAAO,MAAM,GAAG,SAAS;WAAS,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAyC,UAAU;oBAAkB,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;iBAAe,MAAM,GAAG,SAAS;;YAA4E,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;mBAAiB,6BAA6B;eAAa,CAAC,CAAC,MAAM,EAAE,0BAA0B,KAAK,IAAI,CAAC,GAAG,SAAS;eAAa,CAAC,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC,GAAG,SAAS;qBAAmB,CAAC,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC,GAAG,SAAS;kBAAgB,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS;;;;;kBAUniD,CAAC;AAClF,KAAK,cAAc,GAAG,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC;AAC5D,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"DropdownSearch.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown-search/DropdownSearch.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAM5D,OAAO,KAAK,EACR,0BAA0B,EAC1B,yBAAyB,EACzB,kBAAkB,EAClB,6BAA6B,EAC7B,yBAAyB,EACzB,qCAAqC,EACrC,wCAAwC,EACxC,oBAAoB,EACrB,MAAM,YAAY,CAAC;AA+iBtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,cAAc;;mBAdq5B,kBAAkB,GAAG,IAAI;oBAAkB,kBAAkB,EAAE;aAAW,oBAAoB;;;;;;;iBAAmM,yBAAyB;SAAO,MAAM,GAAG,SAAS;WAAS,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAyC,UAAU;oBAAkB,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;iBAAe,MAAM,GAAG,SAAS;;YAA4E,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;;8BAA8E,qCAAqC,GAC1vD,SAAS;mBAAiB,6BAA6B;eAAe,CAAC,CAAC,MAAM,EAAE,0BAA0B,KAAK,IAAI,CAAC,GACpH,SAAS;eAAa,CAAC,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC,GAAG,SAAS;iBAAe,CAAC,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC,GAAG,SAAS;4BAA4B,wCAAwC,GACzM,SAAS;qBAAqB,CAAC,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC,GACtE,SAAS;kBAAgB,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS;cAAY,CAAC,CAAC,MAAM,EAAE,yBAAyB,KAAK,IAAI,CAAC,GAAG,SAAS;;;;;kBAU1C,CAAC;AAClF,KAAK,cAAc,GAAG,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC;AAC5D,eAAe,cAAc,CAAC"}