@keenmate/web-multiselect 1.12.0-rc03 → 1.12.0-rc04
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 +14 -3
- package/dist/index.d.ts +71 -6
- package/dist/multiselect.js +774 -683
- package/dist/multiselect.umd.js +7 -7
- package/docs/examples.md +8 -2
- package/docs/usage.md +5 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -21,11 +21,17 @@ Reads `--base-*` variables from the page if [`@keenmate/theme-designer`](https:/
|
|
|
21
21
|
- Custom rendering callbacks for options, badges, and group headers.
|
|
22
22
|
- Form integration via standard hidden inputs (FormData-compatible).
|
|
23
23
|
|
|
24
|
-
## What's
|
|
24
|
+
## What's New in v1.12.0-rc04
|
|
25
25
|
|
|
26
|
-
**
|
|
26
|
+
- **Placeholders — context-aware wording for pickers and empty cascades** — The input used to show `"Search..."` unconditionally, even when search was off or there was nothing to pick. Two new attributes fix that. `select-placeholder` (default `"Pick an option..."`) is shown when the input isn't a usable search box — `enable-search="false"`, or `search-input-mode="readonly"`/`"hidden"` — so a non-searchable picker stops mislabeling itself. `no-data-placeholder` is an opt-in string shown when the option list is empty, letting users see there's nothing to choose without opening the dropdown; it targets cascade multiselects whose parent isn't resolved yet, and stays unset by default so async-loaded selects don't flash an empty-state before their data lands. Placeholder resolution is now recomputed on live updates too, so changing these attributes, the search mode, or the option list updates the visible text on the fly. **Behavior change:** search-disabled instances without a custom placeholder now read `"Pick an option..."` — set `select-placeholder="Search..."` to keep the old text.
|
|
27
|
+
- **setAttributes() — batch attribute updates in a single render** — Setting attributes one at a time triggered a full re-render per call. The new `setAttributes(attrs)` method on `<web-multiselect>` applies a whole map of attributes in one in-place update (or a single reinit at most, if any change is structural). Keys are kebab-case attribute names, exactly like `setAttribute`; a value of `null`/`undefined`/`false` removes the attribute and `true` sets it to `""`. It's particularly handy for i18n language switches that swap several placeholder strings at once without flicker.
|
|
28
|
+
- **search-debounce — coalesce async search into one request** — A new `search-debounce` attribute (milliseconds, default `0`) debounces the async `searchCallback` so a burst of keystrokes collapses into a single request instead of one per character. Each keystroke resets the timer, and it applies to the async `searchCallback` path only — local in-memory filtering stays instant. The existing stale-result guard (results applied only if the term still matches) remains as a second line of defense against out-of-order responses. The examples page §8 now ships a "Debounced Search" demo with a live keystrokes-vs-API-calls counter that makes the coalescing obvious.
|
|
29
|
+
- **searchCallback AbortSignal — cancel superseded in-flight requests** — `searchCallback` now receives an `AbortSignal` as its second argument: `(searchTerm, signal) => Promise<options[]>`. When a newer search supersedes an in-flight one — or the term drops below `min-search-length`, or the component is destroyed — the previous request's signal is aborted; forward it into `fetch(url, { signal })` to actually cancel the network call. It's backward compatible: the argument is optional, and existing callbacks that ignore it keep working with their stale results discarded as before. Aborted or superseded responses never overwrite the live filtered options.
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
## What's New in v1.12.0-rc03
|
|
32
|
+
|
|
33
|
+
- **Declarative options — `<option>` children render without member attributes** — Declarative `<option>` / `<optgroup>` markup previously required `value-member` / `display-value-member` / `group-member` to be set, or every row rendered as `[N/A]`. The element now auto-wires those member properties (plus `icon`/`subtitle`/`disabled`) when declarative children are detected — only when the consumer hasn't already configured them — so markup-only usage works out of the box while every override path still wins.
|
|
34
|
+
- **Framework events — bubbling + composed dispatch** — `select`, `deselect`, and `change` are now dispatched with `bubbles: true, composed: true`, so Svelte 5 `onchange={fn}`, React `onChange`, Vue `@change`, and any framework using delegated event routing work without the legacy `on:` directive. **Behavior change:** delegated form-level `change` listeners now also receive the component's event — filter by `e.target.tagName === 'WEB-MULTISELECT'` if your form library reacts to every `change`.
|
|
29
35
|
|
|
30
36
|
> ⚠️ **Security notice:** This component intentionally allows raw HTML in rendering callbacks to give developers full control over content display. If you display user-generated content, you must sanitize it yourself. See [docs/examples.md → HTML Injection (XSS) notice](./docs/examples.md#html-injection-xss-notice) for the complete list of affected callbacks.
|
|
31
37
|
|
|
@@ -106,6 +112,11 @@ npm run build
|
|
|
106
112
|
|
|
107
113
|
# Create package tarball
|
|
108
114
|
npm run package
|
|
115
|
+
|
|
116
|
+
# Run tests
|
|
117
|
+
npm run test:unit # Vitest (happy-dom) — fast logic checks
|
|
118
|
+
npm run test:e2e # Playwright (browser) — interaction/visual
|
|
119
|
+
npm test # both
|
|
109
120
|
```
|
|
110
121
|
|
|
111
122
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -196,8 +196,20 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
196
196
|
checkboxAlign?: 'top' | 'center' | 'bottom';
|
|
197
197
|
/** Hint text shown above the input while the dropdown is open. */
|
|
198
198
|
searchHint?: string;
|
|
199
|
-
/** Placeholder text for the search input */
|
|
199
|
+
/** Placeholder text for the search input (shown while search is usable) */
|
|
200
200
|
searchPlaceholder?: string;
|
|
201
|
+
/**
|
|
202
|
+
* Placeholder shown when search is disabled (input acts as a picker rather than a search box).
|
|
203
|
+
* Applies when `isSearchEnabled` is false or `searchInputMode` is 'readonly'/'hidden'.
|
|
204
|
+
* Default: "Pick an option..."
|
|
205
|
+
*/
|
|
206
|
+
selectPlaceholder?: string;
|
|
207
|
+
/**
|
|
208
|
+
* Placeholder shown when there are no options to choose from (e.g. an unresolved cascade parent).
|
|
209
|
+
* Opt-in: when unset, the normal search/select placeholder is used even with an empty list.
|
|
210
|
+
* Lets users see there is no data without opening the dropdown.
|
|
211
|
+
*/
|
|
212
|
+
noDataPlaceholder?: string;
|
|
201
213
|
/** Minimum width for the dropdown (e.g., '20rem', '300px') */
|
|
202
214
|
dropdownMinWidth?: string | null;
|
|
203
215
|
/** Maximum width for the dropdown (e.g., '40rem', '500px') */
|
|
@@ -226,6 +238,13 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
226
238
|
badgesMaxVisible?: number | null;
|
|
227
239
|
/** Minimum search length before loading data */
|
|
228
240
|
minSearchLength?: number;
|
|
241
|
+
/**
|
|
242
|
+
* Debounce delay in milliseconds before the async `searchCallback` is invoked.
|
|
243
|
+
* Each keystroke resets the timer, so only the last input in a burst fires a request.
|
|
244
|
+
* Applies to the async `searchCallback` path only — local in-memory filtering stays instant.
|
|
245
|
+
* Default: 0 (no debounce — callback runs on every keystroke).
|
|
246
|
+
*/
|
|
247
|
+
searchDebounce?: number;
|
|
229
248
|
/** Minimum items before virtual scroll activates (default: 100) */
|
|
230
249
|
virtualScrollThreshold?: number;
|
|
231
250
|
/** Fixed height for each option in pixels (required for virtual scroll, default: 50) */
|
|
@@ -236,8 +255,13 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
236
255
|
virtualScrollBuffer?: number;
|
|
237
256
|
/** Pre-process search term before calling searchCallback. Return null to prevent search. Use for accent removal, validation, etc. */
|
|
238
257
|
beforeSearchCallback?: ((searchTerm: string) => string | null) | null;
|
|
239
|
-
/**
|
|
240
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Async function to load data: `(searchTerm, signal) => Promise<options[]>`.
|
|
260
|
+
* The optional second argument is an `AbortSignal` that fires when a newer search
|
|
261
|
+
* supersedes this one (or the component is destroyed). Wire it into your `fetch`
|
|
262
|
+
* to cancel the in-flight request; ignoring it is fine — stale results are discarded.
|
|
263
|
+
*/
|
|
264
|
+
searchCallback?: ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | null;
|
|
241
265
|
/** Callback to add a new option when isAddNewAllowed is true */
|
|
242
266
|
addNewCallback?: ((value: string) => T | Promise<T>) | null;
|
|
243
267
|
/** Callback when an option is selected */
|
|
@@ -304,6 +328,9 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
304
328
|
private _renderSelectedContentCallback?;
|
|
305
329
|
private _getCounterCallback?;
|
|
306
330
|
private _actionButtons?;
|
|
331
|
+
private _batchDepth;
|
|
332
|
+
private _batchPartial;
|
|
333
|
+
private _batchNeedsReinit;
|
|
307
334
|
private _beforeSearchCallback?;
|
|
308
335
|
private _searchCallback?;
|
|
309
336
|
private _addNewCallback?;
|
|
@@ -323,6 +350,20 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
323
350
|
connectedCallback(): void;
|
|
324
351
|
disconnectedCallback(): void;
|
|
325
352
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
353
|
+
/**
|
|
354
|
+
* Set several attributes in one in-place update — a single re-render instead of one per
|
|
355
|
+
* attribute (and a single reinit at most if any change is structural). Keys are attribute
|
|
356
|
+
* names in kebab-case, exactly as `setAttribute`. A value of `null`/`undefined`/`false`
|
|
357
|
+
* removes the attribute; `true` sets it to an empty string; anything else is stringified.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* el.setAttributes({
|
|
361
|
+
* 'search-placeholder': t('search'),
|
|
362
|
+
* 'select-placeholder': t('pick'),
|
|
363
|
+
* 'no-data-placeholder': t('noData'),
|
|
364
|
+
* });
|
|
365
|
+
*/
|
|
366
|
+
setAttributes(attrs: Record<string, string | number | boolean | null | undefined>): void;
|
|
326
367
|
private render;
|
|
327
368
|
private renderDebugInfo;
|
|
328
369
|
private updateDebugInfo;
|
|
@@ -419,8 +460,8 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
419
460
|
get getCounterCallback(): ((count: number, moreCount?: number) => string) | undefined;
|
|
420
461
|
get beforeSearchCallback(): ((searchTerm: string) => string | null) | undefined;
|
|
421
462
|
set beforeSearchCallback(callback: ((searchTerm: string) => string | null) | undefined);
|
|
422
|
-
get searchCallback(): ((searchTerm: string) => Promise<T[]>) | undefined;
|
|
423
|
-
set searchCallback(callback: ((searchTerm: string) => Promise<T[]>) | undefined);
|
|
463
|
+
get searchCallback(): ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | undefined;
|
|
464
|
+
set searchCallback(callback: ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | undefined);
|
|
424
465
|
get addNewCallback(): ((value: string) => T | Promise<T>) | undefined;
|
|
425
466
|
set addNewCallback(callback: ((value: string) => T | Promise<T>) | undefined);
|
|
426
467
|
get selectCallback(): ((option: T) => void) | undefined;
|
|
@@ -478,7 +519,7 @@ export declare interface MultiSelectOption {
|
|
|
478
519
|
*/
|
|
479
520
|
export declare interface MultiSelectOptions extends MultiSelectConfig<MultiSelectOption> {
|
|
480
521
|
options?: MultiSelectOption[];
|
|
481
|
-
searchCallback?: ((searchTerm: string) => Promise<MultiSelectOption[]>) | null;
|
|
522
|
+
searchCallback?: ((searchTerm: string, signal?: AbortSignal) => Promise<MultiSelectOption[]>) | null;
|
|
482
523
|
addNewCallback?: ((value: string) => MultiSelectOption | Promise<MultiSelectOption>) | null;
|
|
483
524
|
selectCallback?: ((option: MultiSelectOption) => void) | null;
|
|
484
525
|
deselectCallback?: ((option: MultiSelectOption) => void) | null;
|
|
@@ -548,6 +589,8 @@ export declare class WebMultiSelect<T = any> {
|
|
|
548
589
|
private matchingIndices;
|
|
549
590
|
private searchTerm;
|
|
550
591
|
private isLoading;
|
|
592
|
+
private searchDebounceTimer?;
|
|
593
|
+
private searchAbortController?;
|
|
551
594
|
private showSelectedPopover;
|
|
552
595
|
private selectedPopoverPlacement;
|
|
553
596
|
private dropdownPlacement;
|
|
@@ -624,9 +667,31 @@ export declare class WebMultiSelect<T = any> {
|
|
|
624
667
|
private renderOption;
|
|
625
668
|
private highlightMatch;
|
|
626
669
|
private groupOptions;
|
|
670
|
+
/** Whether the input currently functions as a usable search field (drives placeholder wording). */
|
|
671
|
+
private get isSearchUsable();
|
|
672
|
+
/**
|
|
673
|
+
* Resolve the closed-state input placeholder for the current data/search state.
|
|
674
|
+
* Priority: explicit no-data placeholder (when the list is empty) → "pick" prompt when
|
|
675
|
+
* search is unusable → the search placeholder.
|
|
676
|
+
*/
|
|
677
|
+
private getPlaceholderText;
|
|
627
678
|
private renderBadges;
|
|
628
679
|
private attachEvents;
|
|
629
680
|
private handleSearch;
|
|
681
|
+
/** Abort the search request currently in flight, if any. The aborted request's results
|
|
682
|
+
* are then ignored (and the consumer's `searchCallback` can short-circuit its fetch via
|
|
683
|
+
* the `AbortSignal` it was handed). */
|
|
684
|
+
private abortInFlightSearch;
|
|
685
|
+
/**
|
|
686
|
+
* Invoke the async `searchCallback` and apply its results. Split out of `handleSearch`
|
|
687
|
+
* so it can be called immediately or after the debounce timer.
|
|
688
|
+
*
|
|
689
|
+
* Any request still in flight is aborted before a new one starts, so a slow earlier
|
|
690
|
+
* request can't overwrite a newer one — and consumers that wire the passed `AbortSignal`
|
|
691
|
+
* into their fetch get the request actually cancelled, not just ignored. The
|
|
692
|
+
* `aborted` / `searchTerm === value` guards drop superseded or out-of-order responses.
|
|
693
|
+
*/
|
|
694
|
+
private performAsyncSearch;
|
|
630
695
|
private handleKeydown;
|
|
631
696
|
private handleDropdownClick;
|
|
632
697
|
private handleBadgeClick;
|