@keenmate/web-multiselect 2.0.0-rc12 → 2.0.0
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/README.md +34 -12
- package/component-variables.manifest.json +23 -1
- package/custom-elements.json +470 -9
- package/dist/index.d.ts +187 -5
- package/dist/multiselect.js +961 -730
- package/dist/multiselect.umd.js +11 -11
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/css/animations.css +6 -1
- package/src/css/badges.css +14 -0
- package/src/css/options.css +81 -17
- package/src/css/variables.css +66 -21
- package/vscode.css-custom-data.json +15 -0
- package/vscode.html-custom-data.json +11 -1
- package/web-types.json +58 -4
package/dist/index.d.ts
CHANGED
|
@@ -332,8 +332,32 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
332
332
|
fullscreenAutofocus?: boolean;
|
|
333
333
|
/** Lock dropdown placement after first open (internal: isPlacementLocked) */
|
|
334
334
|
isPlacementLocked?: boolean;
|
|
335
|
-
/**
|
|
335
|
+
/**
|
|
336
|
+
* Allow adding new options not in the list (internal: isAddNewAllowed).
|
|
337
|
+
* When on and a search yields no matches, the empty dropdown shows a clickable
|
|
338
|
+
* "add new" prompt (text from `addNewText` / `getAddNewTextCallback`) instead of
|
|
339
|
+
* the `emptyMessage`; choosing it (click or Enter) fires the `add` event and, if
|
|
340
|
+
* `addNewCallback` is set, materializes + selects the created option.
|
|
341
|
+
*/
|
|
336
342
|
isAddNewAllowed?: boolean;
|
|
343
|
+
/**
|
|
344
|
+
* Template for the clickable "add new" prompt (see `isAddNewAllowed`). The substring
|
|
345
|
+
* `{value}` is replaced with the (HTML-escaped) typed text. Default: `Add "{value}"`.
|
|
346
|
+
* `getAddNewTextCallback` takes precedence. (internal: addNewText)
|
|
347
|
+
*/
|
|
348
|
+
addNewText?: string;
|
|
349
|
+
/**
|
|
350
|
+
* Dynamically compute the "add new" prompt label from the typed text. Takes precedence
|
|
351
|
+
* over `addNewText`. Returns plain text (inserted as text, not HTML). Use it for i18n or
|
|
352
|
+
* context-aware wording, e.g. `(v) => \`Add new member: ${v}\``.
|
|
353
|
+
*/
|
|
354
|
+
getAddNewTextCallback?: ((value: string) => string) | null;
|
|
355
|
+
/**
|
|
356
|
+
* Template for the pending prompt shown (spinner + this text) while an async `addNewCallback`
|
|
357
|
+
* is in flight. `{value}` is replaced with the (HTML-escaped) typed text. Default:
|
|
358
|
+
* `Adding "{value}"…`. (internal: addNewPendingText)
|
|
359
|
+
*/
|
|
360
|
+
addNewPendingText?: string;
|
|
337
361
|
/** Show count badge next to toggle icon (internal: isCounterShown) */
|
|
338
362
|
isCounterShown?: boolean;
|
|
339
363
|
/**
|
|
@@ -530,8 +554,32 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
530
554
|
* to cancel the in-flight request; ignoring it is fine — stale results are discarded.
|
|
531
555
|
*/
|
|
532
556
|
searchCallback?: ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | null;
|
|
533
|
-
/**
|
|
534
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Callback to create the new option object from the typed text when `isAddNewAllowed` is on.
|
|
559
|
+
* Return (or resolve to) the new option — it is appended to the list and auto-selected. The
|
|
560
|
+
* returned `T` can be a rich option object (icon/subtitle/custom-render fields and all): it flows
|
|
561
|
+
* through the same `get*` / `render*` callbacks as any other option, so the created row and its
|
|
562
|
+
* badge render exactly like the rest.
|
|
563
|
+
*
|
|
564
|
+
* **Cancelable (async):** return `null` or `undefined` (or a Promise of either) to abort — nothing
|
|
565
|
+
* is added or selected, the search is left intact, and the `add` event does NOT fire. Use it for
|
|
566
|
+
* async validation, a confirm dialog, or a server round-trip that may say no. The cancel sentinel
|
|
567
|
+
* is strictly `null`/`undefined` (checked with `== null`), so a falsy-but-valid option in
|
|
568
|
+
* primitive mode (`0`, `false`, `""`) still creates normally.
|
|
569
|
+
*
|
|
570
|
+
* Omit the callback entirely to handle creation yourself via the `add` event / `onAddNew` (e.g.
|
|
571
|
+
* open a modal, POST to a server, then add the option imperatively).
|
|
572
|
+
*/
|
|
573
|
+
addNewCallback?: ((value: string) => T | null | undefined | Promise<T | null | undefined>) | null;
|
|
574
|
+
/**
|
|
575
|
+
* Event handler: the user chose to create a new option from the typed text (via the "add new"
|
|
576
|
+
* row or Enter). `value` is the typed text; `option` is the created item when `addNewCallback`
|
|
577
|
+
* produced one (absent otherwise). Mirrors the bubbling `add` CustomEvent on the element.
|
|
578
|
+
*/
|
|
579
|
+
onAddNew?: ((detail: {
|
|
580
|
+
value: string;
|
|
581
|
+
option?: T;
|
|
582
|
+
}) => void) | null;
|
|
535
583
|
/**
|
|
536
584
|
* Intercept keyboard input before the built-in handling. Runs on every keydown (open or
|
|
537
585
|
* closed) with a {@link MultiSelectKeydownContext} carrying the event, current state, and a
|
|
@@ -606,10 +654,14 @@ export declare class MultiSelectElement<T = any> extends BlissElement<MultiSelec
|
|
|
606
654
|
}, {
|
|
607
655
|
readonly name: "change";
|
|
608
656
|
readonly description: "The selection changed. `detail.selectedOptions`/`detail.selectedValues` are the full selection.";
|
|
657
|
+
}, {
|
|
658
|
+
readonly name: "add";
|
|
659
|
+
readonly description: "The user chose to create a new option from the typed text (via the \"add new\" prompt or Enter) — requires `allow-add-new`. `detail.value` is the typed text; `detail.option` is the created item when `addNewCallback` produced one.";
|
|
609
660
|
}];
|
|
610
661
|
onSelect: ((e: CustomEvent<MultiSelectEventDetail<T>>) => void) | null;
|
|
611
662
|
onDeselect: ((e: CustomEvent<MultiSelectEventDetail<T>>) => void) | null;
|
|
612
663
|
onChange: ((e: CustomEvent<MultiSelectEventDetail<T>>) => void) | null;
|
|
664
|
+
onAdd: ((e: CustomEvent<MultiSelectEventDetail<T>>) => void) | null;
|
|
613
665
|
constructor();
|
|
614
666
|
/**
|
|
615
667
|
* Called by the browser when the surrounding <form> is reset. Clears the
|
|
@@ -667,6 +719,43 @@ export declare class MultiSelectElement<T = any> extends BlissElement<MultiSelec
|
|
|
667
719
|
showMessage(content: string | HTMLElement, opts?: MessageOptions): void;
|
|
668
720
|
/** Dismiss the transient message shown by {@link showMessage}, if any. */
|
|
669
721
|
hideMessage(): void;
|
|
722
|
+
/**
|
|
723
|
+
* Clear the search box and restore the full option list (does not touch the selection — use
|
|
724
|
+
* {@link clearAll} for that). Pair with {@link scrollToValue} to reveal then scroll to an option
|
|
725
|
+
* a search had filtered out: `el.clearSearch(); el.scrollToValue(v)`.
|
|
726
|
+
*/
|
|
727
|
+
clearSearch(): void;
|
|
728
|
+
/** The current search box text (empty string when nothing is typed). Read via this getter, write with {@link search}. */
|
|
729
|
+
get searchText(): string;
|
|
730
|
+
/**
|
|
731
|
+
* Programmatically set the search text and filter, as if the user typed it (runs
|
|
732
|
+
* `beforeSearchCallback` / `minSearchLength` / async `searchCallback`). Does not open the dropdown
|
|
733
|
+
* — call {@link open} if you want it visible. Pass `''` to clear (same as {@link clearSearch}).
|
|
734
|
+
*/
|
|
735
|
+
search(term: string): void;
|
|
736
|
+
/**
|
|
737
|
+
* Scroll the open dropdown to the option at `index` (into the current filtered list). Returns
|
|
738
|
+
* false if closed or out of range. Deferred internally so `el.open(); el.scrollToIndex(i)` works.
|
|
739
|
+
*/
|
|
740
|
+
scrollToIndex(index: number, opts?: {
|
|
741
|
+
block?: ScrollLogicalPosition;
|
|
742
|
+
}): boolean;
|
|
743
|
+
/**
|
|
744
|
+
* Scroll the open dropdown to the option with this `value`. Returns false if it isn't in the
|
|
745
|
+
* currently visible list (filtered out by search, or under a collapsed tree branch) — call
|
|
746
|
+
* {@link clearSearch} / expand first.
|
|
747
|
+
*/
|
|
748
|
+
scrollToValue(value: string | number, opts?: {
|
|
749
|
+
block?: ScrollLogicalPosition;
|
|
750
|
+
}): boolean;
|
|
751
|
+
/**
|
|
752
|
+
* Scroll to a group: its header in standard rendering, or the group's first option in
|
|
753
|
+
* virtual-scroll mode (no headers there). Returns false in tree mode or if the group is empty
|
|
754
|
+
* in the current filtered list.
|
|
755
|
+
*/
|
|
756
|
+
scrollToGroup(name: string, opts?: {
|
|
757
|
+
block?: ScrollLogicalPosition;
|
|
758
|
+
}): boolean;
|
|
670
759
|
/** Open the dropdown. */
|
|
671
760
|
open(): void;
|
|
672
761
|
/** Close the dropdown. */
|
|
@@ -688,14 +777,17 @@ export declare interface MultiSelectEventDetail<T = any> {
|
|
|
688
777
|
selectedOptions: T[];
|
|
689
778
|
/** Selected values array */
|
|
690
779
|
selectedValues: (string | number)[];
|
|
691
|
-
/** The option that triggered the event (for select/deselect) */
|
|
780
|
+
/** The option that triggered the event (for select/deselect/add) */
|
|
692
781
|
option?: T;
|
|
782
|
+
/** The typed text that triggered the `add` event (add only) */
|
|
783
|
+
value?: string;
|
|
693
784
|
}
|
|
694
785
|
|
|
695
786
|
declare type MultiSelectEvents = {
|
|
696
787
|
select: MultiSelectEventDetail;
|
|
697
788
|
deselect: MultiSelectEventDetail;
|
|
698
789
|
change: MultiSelectEventDetail;
|
|
790
|
+
add: MultiSelectEventDetail;
|
|
699
791
|
};
|
|
700
792
|
|
|
701
793
|
/**
|
|
@@ -943,6 +1035,10 @@ export declare class WebMultiSelect<T = any> {
|
|
|
943
1035
|
private keyboardController;
|
|
944
1036
|
private matchingIndices;
|
|
945
1037
|
private searchTerm;
|
|
1038
|
+
/** Keyboard focus sits on the empty-state "add new" prompt (arrow-navigated). */
|
|
1039
|
+
private addNewFocused;
|
|
1040
|
+
/** An async addNewCallback is in flight — the prompt shows a spinner + pending text. */
|
|
1041
|
+
private addNewPending;
|
|
946
1042
|
private isLoading;
|
|
947
1043
|
private searchDebounceTimer?;
|
|
948
1044
|
private searchAbortController?;
|
|
@@ -1192,6 +1288,25 @@ export declare class WebMultiSelect<T = any> {
|
|
|
1192
1288
|
* chevron/toggle — every node is just a normal, selectable option.
|
|
1193
1289
|
*/
|
|
1194
1290
|
private renderTreeNode;
|
|
1291
|
+
/**
|
|
1292
|
+
* Empty-dropdown content. When "add new" is enabled (isAddNewAllowed) AND the user has typed
|
|
1293
|
+
* a non-empty search term, show a clickable "add new" prompt instead of the plain emptyMessage —
|
|
1294
|
+
* choosing it (click via handleDropdownClick, or Enter via the keydown handler) runs handleAddNew.
|
|
1295
|
+
* Otherwise fall back to the emptyMessage.
|
|
1296
|
+
*/
|
|
1297
|
+
private renderEmptyStateHTML;
|
|
1298
|
+
/** True when the empty dropdown is currently showing the clickable "add new" prompt. */
|
|
1299
|
+
private isAddNewPromptShown;
|
|
1300
|
+
/**
|
|
1301
|
+
* Resolve the "add new" prompt label for the typed text. Priority: getAddNewTextCallback
|
|
1302
|
+
* (returns plain text — fully escaped here) → addNewText template (trusted config string;
|
|
1303
|
+
* only the `{value}` substitution is escaped) → the default `Add "{value}"`.
|
|
1304
|
+
*/
|
|
1305
|
+
private getAddNewText;
|
|
1306
|
+
/** Pending-prompt label shown (with a spinner) while an async addNewCallback runs. */
|
|
1307
|
+
private getAddNewPendingText;
|
|
1308
|
+
/** Minimal HTML entity escape for untrusted text spliced into an innerHTML string. */
|
|
1309
|
+
private escapeHtml;
|
|
1195
1310
|
private highlightMatch;
|
|
1196
1311
|
private groupOptions;
|
|
1197
1312
|
/** Whether the input currently functions as a usable search field (drives placeholder wording). */
|
|
@@ -1251,6 +1366,8 @@ export declare class WebMultiSelect<T = any> {
|
|
|
1251
1366
|
private focusLast;
|
|
1252
1367
|
private focusPageUp;
|
|
1253
1368
|
private focusPageDown;
|
|
1369
|
+
/** Move keyboard focus onto the empty-state "add new" prompt (the only actionable row). */
|
|
1370
|
+
private focusAddNewPrompt;
|
|
1254
1371
|
private focusNextMatch;
|
|
1255
1372
|
private focusPreviousMatch;
|
|
1256
1373
|
/** Lazily build (and cache) the imperative facade passed to `keydownCallback`. Bound to the
|
|
@@ -1258,8 +1375,64 @@ export declare class WebMultiSelect<T = any> {
|
|
|
1258
1375
|
private getKeyboardController;
|
|
1259
1376
|
/** Clear the search box (both the main input and the fullscreen search) and reset the visible
|
|
1260
1377
|
* list. Shared by Escape and the keyboard controller. */
|
|
1261
|
-
|
|
1378
|
+
/**
|
|
1379
|
+
* Clear the search box and restore the full option list (resets the visible/matched sets and
|
|
1380
|
+
* drops keyboard focus). Public building block: pair it with `scrollToValue()` to reveal then
|
|
1381
|
+
* scroll to an option the current search had filtered out — `el.clearSearch(); el.scrollToValue(v)`.
|
|
1382
|
+
* Does not touch the selection (use `clearAll()` for that).
|
|
1383
|
+
*/
|
|
1384
|
+
clearSearch(): void;
|
|
1385
|
+
/**
|
|
1386
|
+
* Programmatically set the search text and filter — exactly as if the user typed it, so
|
|
1387
|
+
* `beforeSearchCallback`, `minSearchLength` and async `searchCallback` all apply the same way.
|
|
1388
|
+
* Reflects into the search box (and the fullscreen sheet's field). Does NOT open the dropdown —
|
|
1389
|
+
* call `open()` if you want it visible. Passing `''` clears (equivalent to `clearSearch()`).
|
|
1390
|
+
*/
|
|
1391
|
+
search(term: string): void;
|
|
1262
1392
|
private scrollToFocused;
|
|
1393
|
+
/**
|
|
1394
|
+
* scrollIntoView with the fullscreen-safe default. In the fullscreen sheet the soft keyboard
|
|
1395
|
+
* covers the lower viewport, so `block:'nearest'` can bottom-align a match BEHIND the keyboard;
|
|
1396
|
+
* centre it instead and scroll INSTANTLY (a smooth animation kicked off per-keystroke is torn
|
|
1397
|
+
* down by the next re-render and never settles — the "list jumps every letter" bug). Floating
|
|
1398
|
+
* scrolls the nearest edge smoothly. The caller may override `block`.
|
|
1399
|
+
*/
|
|
1400
|
+
private applyScrollIntoView;
|
|
1401
|
+
/**
|
|
1402
|
+
* Scroll the open dropdown so the option at `index` (into the current `filteredOptions`) is
|
|
1403
|
+
* visible. Works in floating, fullscreen (mobile), virtual-scroll and tree modes. Returns
|
|
1404
|
+
* false if the dropdown is closed or the index is out of range. The scroll is deferred a frame
|
|
1405
|
+
* when the list isn't rendered yet (e.g. right after `open()` in virtual mode / the fullscreen
|
|
1406
|
+
* sheet build), so `el.open(); el.scrollToIndex(i)` works.
|
|
1407
|
+
*/
|
|
1408
|
+
scrollToIndex(index: number, opts?: {
|
|
1409
|
+
block?: ScrollLogicalPosition;
|
|
1410
|
+
}): boolean;
|
|
1411
|
+
/**
|
|
1412
|
+
* Scroll to the option whose value matches `value` (resolved within the current
|
|
1413
|
+
* `filteredOptions`). Returns false if it isn't in the currently visible list — e.g. filtered
|
|
1414
|
+
* out by a search, or (tree) under a collapsed ancestor. Call `clearSearch()` (or expand the
|
|
1415
|
+
* branch) first to reveal it, then scroll.
|
|
1416
|
+
*/
|
|
1417
|
+
scrollToValue(value: string | number, opts?: {
|
|
1418
|
+
block?: ScrollLogicalPosition;
|
|
1419
|
+
}): boolean;
|
|
1420
|
+
/**
|
|
1421
|
+
* Scroll to a group. In standard rendering the group's header (`.ms__group-label`) is brought
|
|
1422
|
+
* into view; in virtual-scroll mode (which renders no headers) it scrolls to the group's FIRST
|
|
1423
|
+
* option instead. Returns false in tree mode (groups don't apply) or if the group has no
|
|
1424
|
+
* options in the current filtered list.
|
|
1425
|
+
*/
|
|
1426
|
+
scrollToGroup(name: string, opts?: {
|
|
1427
|
+
block?: ScrollLogicalPosition;
|
|
1428
|
+
}): boolean;
|
|
1429
|
+
/**
|
|
1430
|
+
* Shared scroll worker for the public scrollTo* methods. Virtual mode uses the fixed-height
|
|
1431
|
+
* math (works even if the row isn't currently rendered); otherwise scrolls the
|
|
1432
|
+
* `.ms__option[data-index]` element into view. Defers one frame if the list isn't ready yet
|
|
1433
|
+
* (post-open virtual init / fullscreen sheet build), then retries once.
|
|
1434
|
+
*/
|
|
1435
|
+
private scrollToRenderedIndex;
|
|
1263
1436
|
private toggleOption;
|
|
1264
1437
|
/**
|
|
1265
1438
|
* The single funnel for an interactive (user-initiated) selection. Consults
|
|
@@ -1278,6 +1451,13 @@ export declare class WebMultiSelect<T = any> {
|
|
|
1278
1451
|
* Returns true if the option was deselected, false if the veto blocked it.
|
|
1279
1452
|
*/
|
|
1280
1453
|
private interactiveDeselect;
|
|
1454
|
+
/**
|
|
1455
|
+
* Commit the "add new" affordance for the typed text. Two modes:
|
|
1456
|
+
* - `addNewCallback` set → create the option, append it, select it, clear the search.
|
|
1457
|
+
* - no callback → the consumer owns creation; we only notify (via the `add` event) so they
|
|
1458
|
+
* can open a modal / POST / add the option imperatively.
|
|
1459
|
+
* The `add` event fires in BOTH modes (with `option` present only when one was created).
|
|
1460
|
+
*/
|
|
1281
1461
|
private handleAddNew;
|
|
1282
1462
|
private selectOption;
|
|
1283
1463
|
private deselectOption;
|
|
@@ -1563,6 +1743,8 @@ export declare class WebMultiSelect<T = any> {
|
|
|
1563
1743
|
*/
|
|
1564
1744
|
updateOptions(partial: Partial<MultiSelectConfig<T>>): boolean;
|
|
1565
1745
|
get selectedItem(): T | null;
|
|
1746
|
+
/** The current search box text (empty string when nothing is typed). Read-only; clear it with `clearSearch()`. */
|
|
1747
|
+
get searchText(): string;
|
|
1566
1748
|
get selectedValue(): string | number | (string | number)[] | null;
|
|
1567
1749
|
getValue(): string | number | (string | number)[] | null;
|
|
1568
1750
|
/**
|