@keenmate/web-multiselect 1.12.0-rc03 → 1.12.0-rc05
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 +38 -3
- package/component-variables.manifest.json +1 -0
- package/dist/index.d.ts +196 -27
- package/dist/multiselect.js +1259 -977
- package/dist/multiselect.umd.js +17 -16
- package/dist/style.css +1 -1
- package/docs/examples.md +54 -4
- package/docs/usage.md +37 -5
- package/package.json +7 -2
- package/src/css/controls.css +50 -4
- package/src/css/floating.css +28 -0
- package/src/css/options.css +5 -0
- package/src/css/variables.css +17 -1
package/README.md
CHANGED
|
@@ -21,11 +21,25 @@ 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-rc05
|
|
25
25
|
|
|
26
|
-
**
|
|
26
|
+
> ⚠️ **Breaking change — event-handler rename, no aliases.** The fire-and-forget notification callbacks are renamed to `on*` with **no backward-compatible aliases**: `selectCallback` → `onSelect`, `deselectCallback` → `onDeselect`, `changeCallback` → `onChange`, and on `ActionButton`, `isVisibleCallback` / `isDisabledCallback` → `getIsVisibleCallback` / `getIsDisabledCallback`. If you assign these as element properties or config keys, **rename them or your handlers silently stop firing**. The `select` / `deselect` / `change` **DOM events are unchanged**, so `addEventListener(...)` consumers are unaffected. Full rationale in the first bullet below.
|
|
27
27
|
|
|
28
|
-
**
|
|
28
|
+
- **Events — fire-and-forget callbacks renamed to `on*` (breaking)** — `selectCallback` / `deselectCallback` / `changeCallback` are now `onSelect` / `onDeselect` / `onChange`, and on `ActionButton` the predicate callbacks become `getIsVisibleCallback` / `getIsDisabledCallback` while `onClick` keeps its name. The rule is simple: a function whose return value the component ignores is an *event* and takes the `on*` shape; functions whose return value is consumed stay `get*Callback` / `before*Callback`. The bubbling `select` / `deselect` / `change` DOM events are unchanged — the `on*` properties are just their JS-property twins. There are no deprecated aliases, so update `el.selectCallback` → `el.onSelect` (and the matching config keys); `addEventListener('select' | 'deselect' | 'change', …)` is unaffected.
|
|
29
|
+
- **Selection — new `beforeSelectCallback` / `beforeDeselectCallback` interceptors** — veto a selection or deselection before it happens by returning `false`; each receives the option being toggled plus the current selection. The veto is silent — a blocked action mutates no state and fires no event, so your handler owns any feedback. The deselect veto is enforced across *every* interactive removal path (dropdown toggle, badge `×`, selected-items popover, and the "remove hidden" badge) by routing them through a single funnel, so no affordance can bypass it; programmatic `setSelected()` and the Select-All / Clear-All buttons deliberately skip it. Typical uses: mutually-exclusive options, or a required item that can't be removed.
|
|
30
|
+
- **Dropdown options — hover tooltips, independently configurable** — option rows can now show a hover tooltip via `enable-option-tooltips`, built on the same Floating-UI `Tooltip` engine as the badge and action tooltips (show/hide delays, plus re-attachment as virtual-scrolled rows recycle into view). Default content is the option's display value with its subtitle on a second line; override per option with `getOptionTooltipCallback`. Unlike the badge tooltips they share an engine with, option tooltips are fully independent: `option-tooltip-placement` defaults to `top-start` so the tooltip anchors to a wide row's start edge instead of centering in the middle of a 1920px-wide row, `option-tooltip-follow-cursor` makes it track the pointer, and a dedicated `.ms__option-tooltip` class plus `--ms-option-tooltip-*` variables (each defaulting to the shared `--ms-tooltip-*` surface) let you restyle them separately. Demonstrated on the new `examples-tooltips.html`.
|
|
31
|
+
- **Action buttons — position, rows, and alignment** — the action-buttons block can sit at the `bottom` of the dropdown as a sticky footer (`actions-position`), buttons can be arranged across multiple stacked rows via a per-button `row` property (row 1 always at the panel's outer edge, higher rows stacking inward toward the options list — so `column-reverse` keeps row 1 at the bottom for the footer case), and `actions-align` controls horizontal arrangement (`stretch` default, plus `left` / `right` / `center` / `space-between`). Internally `.ms__actions` became a vertical stack of `.ms__actions-row` lines. All additive and backward compatible — no `row` means a single row, and `stretch` preserves the previous full-width buttons.
|
|
32
|
+
- **Action buttons — smarter built-in defaults** — the built-in `select-all` and `clear-all` actions now manage their own disabled state: Select All disables once every selectable option is chosen, and Clear All disables while nothing is selected. This applies only when you haven't set an explicit static `isDisabled` or a `getIsDisabledCallback` (precedence: dynamic callback › static `isDisabled` › built-in default), so existing custom action configs are untouched.
|
|
33
|
+
- **Single-select — dropdown no longer reopens with a stale filter** — the single-select input doubles as label display and search box; reopening used to blank the visible text while leaving the list filtered by the previous search, so the box and the list silently disagreed. `open()` now mirrors the kept search term back into the box, keeping the visible input and the filtered results in sync (honoring `should-keep-search-on-close`).
|
|
34
|
+
- **Async search — dropdown stops jumping between empty and loading states** — the "No data" empty state and the loading spinner now share a min-height (`--ms-state-min-height`, a new themable variable), so the panel keeps a stable footprint instead of resizing as an async search swaps one block for the other.
|
|
35
|
+
- **Docs — new live demo page for events & interceptors** — `examples-events-callbacks.html` shows the DOM events, the `on*` property twin firing in parallel, and interactive veto demos for the new interceptors.
|
|
36
|
+
|
|
37
|
+
## What's New in v1.12.0-rc04
|
|
38
|
+
|
|
39
|
+
- **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.
|
|
40
|
+
- **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.
|
|
41
|
+
- **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.
|
|
42
|
+
- **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.
|
|
29
43
|
|
|
30
44
|
> ⚠️ **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
45
|
|
|
@@ -106,8 +120,29 @@ npm run build
|
|
|
106
120
|
|
|
107
121
|
# Create package tarball
|
|
108
122
|
npm run package
|
|
123
|
+
|
|
124
|
+
# Run tests
|
|
125
|
+
npm run test:unit # Vitest (happy-dom) — fast logic checks
|
|
126
|
+
npm run test:e2e # Playwright (browser) — interaction/visual
|
|
127
|
+
npm test # both
|
|
109
128
|
```
|
|
110
129
|
|
|
130
|
+
## Code structure
|
|
131
|
+
|
|
132
|
+
Follows the BlissFramework four-layer web-component layout:
|
|
133
|
+
|
|
134
|
+
| Layer | File | Role |
|
|
135
|
+
|-------|------|------|
|
|
136
|
+
| Element | `src/web-component.ts` | `MultiSelectElement` — custom-element I/O wrapper, `ATTRIBUTE_TABLE`-driven |
|
|
137
|
+
| Logic | `src/multiselect.ts` | `WebMultiSelect<T>` — framework-agnostic core |
|
|
138
|
+
| Service | `src/tooltip.ts`, `src/virtual-scroll.ts` | single-purpose helpers (`Tooltip`, `VirtualScroll`) |
|
|
139
|
+
| Side | `src/types.ts`, `src/logger.ts`, `src/vendor/` | types, logging, vendored deps |
|
|
140
|
+
|
|
141
|
+
Two deviations from the canonical shape, both intentional:
|
|
142
|
+
|
|
143
|
+
- **`MultiSelectElement extends BaseElement`, not `HTMLElement` directly.** `BaseElement` is a local `const` resolving to `HTMLElement` in the browser and to a stub class under SSR (`typeof HTMLElement === 'undefined'`), so importing the module in Node doesn't throw. A literal `grep "extends HTMLElement"` structure check will not match here by design.
|
|
144
|
+
- **`src/vite-env.d.ts`** is a standard Vite ambient-types file, not part of the four-layer model.
|
|
145
|
+
|
|
111
146
|
## License
|
|
112
147
|
|
|
113
148
|
MIT — see [LICENSE](./LICENSE).
|
|
@@ -241,6 +241,7 @@
|
|
|
241
241
|
{ "name": "ms-checkbox-checked-bg-hover", "category": "checkbox", "usage": "Checked checkbox hover background" },
|
|
242
242
|
{ "name": "ms-checkbox-checked-border-color-hover", "category": "checkbox", "usage": "Checked checkbox hover border" },
|
|
243
243
|
|
|
244
|
+
{ "name": "ms-state-min-height", "category": "empty", "usage": "Shared min-height for the empty and loading states so the dropdown doesn't resize when swapping between them" },
|
|
244
245
|
{ "name": "ms-empty-padding", "category": "empty", "usage": "Empty state padding" },
|
|
245
246
|
{ "name": "ms-empty-font-size", "category": "empty", "usage": "Empty state font size" },
|
|
246
247
|
{ "name": "ms-empty-color", "category": "empty", "usage": "Empty state text color" },
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ declare interface ActionButton<T = any> {
|
|
|
11
11
|
text: string;
|
|
12
12
|
/** Optional CSS class(es) to add to the button */
|
|
13
13
|
cssClass?: string;
|
|
14
|
+
/**
|
|
15
|
+
* 1-based row this button belongs to (default `1`). Buttons sharing a `row` render on the same
|
|
16
|
+
* horizontal line; different values stack into multiple rows. Row 1 sits at the panel's outer edge
|
|
17
|
+
* and higher rows stack inward toward the options list — so with `actions-position="top"` row 1 is
|
|
18
|
+
* the topmost line, and with `actions-position="bottom"` row 1 is the bottommost line.
|
|
19
|
+
*/
|
|
20
|
+
row?: number;
|
|
14
21
|
/** Optional tooltip text */
|
|
15
22
|
tooltip?: string;
|
|
16
23
|
/** Static visibility - set to false to hide button */
|
|
@@ -20,9 +27,9 @@ declare interface ActionButton<T = any> {
|
|
|
20
27
|
/** Custom click handler (required for 'custom' action) */
|
|
21
28
|
onClick?: (multiselect: any) => void | Promise<void>;
|
|
22
29
|
/** Dynamic visibility callback - return false to hide button (takes priority over isVisible) */
|
|
23
|
-
|
|
30
|
+
getIsVisibleCallback?: (multiselect: any) => boolean;
|
|
24
31
|
/** Dynamic disabled state callback - return true to disable button (takes priority over isDisabled) */
|
|
25
|
-
|
|
32
|
+
getIsDisabledCallback?: (multiselect: any) => boolean;
|
|
26
33
|
/** Dynamic text callback - return button text (takes priority over text) */
|
|
27
34
|
getTextCallback?: (multiselect: any) => string;
|
|
28
35
|
/** Dynamic CSS class callback - return class name(s) (takes priority over cssClass) */
|
|
@@ -31,6 +38,9 @@ declare interface ActionButton<T = any> {
|
|
|
31
38
|
getTooltipCallback?: (multiselect: any) => string;
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
/** Horizontal arrangement of buttons within an action row. `stretch` = full-width (default). */
|
|
42
|
+
declare type ActionsAlign = 'stretch' | 'left' | 'right' | 'center' | 'space-between';
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* Layout mode for action buttons container
|
|
36
46
|
* - 'nowrap': Buttons stay in single row (default)
|
|
@@ -38,6 +48,9 @@ declare interface ActionButton<T = any> {
|
|
|
38
48
|
*/
|
|
39
49
|
declare type ActionsLayout = 'nowrap' | 'wrap';
|
|
40
50
|
|
|
51
|
+
/** Where the action-buttons block sits in the dropdown panel. */
|
|
52
|
+
declare type ActionsPosition = 'top' | 'bottom';
|
|
53
|
+
|
|
41
54
|
/**
|
|
42
55
|
* Context provided to renderBadgeContentCallback
|
|
43
56
|
*/
|
|
@@ -196,8 +209,20 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
196
209
|
checkboxAlign?: 'top' | 'center' | 'bottom';
|
|
197
210
|
/** Hint text shown above the input while the dropdown is open. */
|
|
198
211
|
searchHint?: string;
|
|
199
|
-
/** Placeholder text for the search input */
|
|
212
|
+
/** Placeholder text for the search input (shown while search is usable) */
|
|
200
213
|
searchPlaceholder?: string;
|
|
214
|
+
/**
|
|
215
|
+
* Placeholder shown when search is disabled (input acts as a picker rather than a search box).
|
|
216
|
+
* Applies when `isSearchEnabled` is false or `searchInputMode` is 'readonly'/'hidden'.
|
|
217
|
+
* Default: "Pick an option..."
|
|
218
|
+
*/
|
|
219
|
+
selectPlaceholder?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Placeholder shown when there are no options to choose from (e.g. an unresolved cascade parent).
|
|
222
|
+
* Opt-in: when unset, the normal search/select placeholder is used even with an empty list.
|
|
223
|
+
* Lets users see there is no data without opening the dropdown.
|
|
224
|
+
*/
|
|
225
|
+
noDataPlaceholder?: string;
|
|
201
226
|
/** Minimum width for the dropdown (e.g., '20rem', '300px') */
|
|
202
227
|
dropdownMinWidth?: string | null;
|
|
203
228
|
/** Maximum width for the dropdown (e.g., '40rem', '500px') */
|
|
@@ -220,12 +245,23 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
220
245
|
searchMode?: SearchMode;
|
|
221
246
|
/** Layout mode for action buttons: 'nowrap' (default) or 'wrap' for multi-row */
|
|
222
247
|
actionsLayout?: ActionsLayout;
|
|
248
|
+
/** Where the action-buttons block sits in the dropdown: 'top' (default) or 'bottom' (sticky footer). */
|
|
249
|
+
actionsPosition?: ActionsPosition;
|
|
250
|
+
/** Horizontal arrangement of buttons within a row: 'stretch' (default, full-width), 'left', 'right', 'center', or 'space-between'. */
|
|
251
|
+
actionsAlign?: ActionsAlign;
|
|
223
252
|
/** Auto-switch from badges to count when threshold is exceeded */
|
|
224
253
|
badgesThreshold?: number | null;
|
|
225
254
|
/** Maximum number of badges to show in partial mode (used with thresholdMode='partial') */
|
|
226
255
|
badgesMaxVisible?: number | null;
|
|
227
256
|
/** Minimum search length before loading data */
|
|
228
257
|
minSearchLength?: number;
|
|
258
|
+
/**
|
|
259
|
+
* Debounce delay in milliseconds before the async `searchCallback` is invoked.
|
|
260
|
+
* Each keystroke resets the timer, so only the last input in a burst fires a request.
|
|
261
|
+
* Applies to the async `searchCallback` path only — local in-memory filtering stays instant.
|
|
262
|
+
* Default: 0 (no debounce — callback runs on every keystroke).
|
|
263
|
+
*/
|
|
264
|
+
searchDebounce?: number;
|
|
229
265
|
/** Minimum items before virtual scroll activates (default: 100) */
|
|
230
266
|
virtualScrollThreshold?: number;
|
|
231
267
|
/** Fixed height for each option in pixels (required for virtual scroll, default: 50) */
|
|
@@ -236,16 +272,38 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
236
272
|
virtualScrollBuffer?: number;
|
|
237
273
|
/** Pre-process search term before calling searchCallback. Return null to prevent search. Use for accent removal, validation, etc. */
|
|
238
274
|
beforeSearchCallback?: ((searchTerm: string) => string | null) | null;
|
|
239
|
-
/**
|
|
240
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Interceptor: runs before an option is selected via user interaction.
|
|
277
|
+
* Receives the option about to be added and the current selection (before the change).
|
|
278
|
+
* Return `false` to block the selection; return `true`/`undefined` to allow.
|
|
279
|
+
* Silent — a blocked action fires no event. Bypassed by programmatic `setSelected`
|
|
280
|
+
* and the Select-All action button.
|
|
281
|
+
*/
|
|
282
|
+
beforeSelectCallback?: ((option: T, selectedOptions: T[]) => boolean | void) | null;
|
|
283
|
+
/**
|
|
284
|
+
* Interceptor: runs before an option is deselected via user interaction — the dropdown
|
|
285
|
+
* option toggle, a badge's remove (×) button, the selected-items popover's remove button,
|
|
286
|
+
* and the "remove hidden" badge (checked per item). Receives the option about to be
|
|
287
|
+
* removed and the current selection (before the change). Return `false` to block the
|
|
288
|
+
* deselection; return `true`/`undefined` to allow. Silent — a blocked action fires no
|
|
289
|
+
* event. Bypassed by programmatic `setSelected` and the Clear-All action button.
|
|
290
|
+
*/
|
|
291
|
+
beforeDeselectCallback?: ((option: T, selectedOptions: T[]) => boolean | void) | null;
|
|
292
|
+
/**
|
|
293
|
+
* Async function to load data: `(searchTerm, signal) => Promise<options[]>`.
|
|
294
|
+
* The optional second argument is an `AbortSignal` that fires when a newer search
|
|
295
|
+
* supersedes this one (or the component is destroyed). Wire it into your `fetch`
|
|
296
|
+
* to cancel the in-flight request; ignoring it is fine — stale results are discarded.
|
|
297
|
+
*/
|
|
298
|
+
searchCallback?: ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | null;
|
|
241
299
|
/** Callback to add a new option when isAddNewAllowed is true */
|
|
242
300
|
addNewCallback?: ((value: string) => T | Promise<T>) | null;
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
|
|
301
|
+
/** Event handler: an option was selected (fire-and-forget; return value ignored). Mirrors the bubbling `select` CustomEvent on the element. */
|
|
302
|
+
onSelect?: ((option: T) => void) | null;
|
|
303
|
+
/** Event handler: an option was deselected (fire-and-forget). Mirrors the bubbling `deselect` CustomEvent on the element. */
|
|
304
|
+
onDeselect?: ((option: T) => void) | null;
|
|
305
|
+
/** Event handler: the selection set changed (fire-and-forget). Mirrors the bubbling `change` CustomEvent on the element. */
|
|
306
|
+
onChange?: ((selectedOptions: T[]) => void) | null;
|
|
249
307
|
/** Callback to format count badge text (for i18n/pluralization). When moreCount is provided, it's for the "+X more" badge in partial mode. */
|
|
250
308
|
getCounterCallback?: ((count: number, moreCount?: number) => string) | null;
|
|
251
309
|
/** Enable tooltips on selected item badges (internal: isBadgeTooltipsEnabled) */
|
|
@@ -262,6 +320,18 @@ declare interface MultiSelectConfig<T = any> {
|
|
|
262
320
|
badgeTooltipDelay?: number;
|
|
263
321
|
/** Offset distance for tooltip in pixels */
|
|
264
322
|
badgeTooltipOffset?: number;
|
|
323
|
+
/** Enable tooltips on dropdown options (internal: isOptionTooltipsEnabled) */
|
|
324
|
+
isOptionTooltipsEnabled?: boolean;
|
|
325
|
+
/** Callback to generate custom tooltip content for a dropdown option. Default: display value, plus subtitle on the next line when present. */
|
|
326
|
+
getOptionTooltipCallback?: ((item: T) => string | HTMLElement) | null;
|
|
327
|
+
/** Option tooltip placement. Default `'top-start'` (anchored to the row's start edge, so it doesn't center on a full-width row). Use `'left'`/`'right'` (start/end side) for a narrow multiselect. */
|
|
328
|
+
optionTooltipPlacement?: Placement;
|
|
329
|
+
/** Delay before showing an option tooltip (ms). Falls back to `badgeTooltipDelay`, then `100`. */
|
|
330
|
+
optionTooltipDelay?: number;
|
|
331
|
+
/** Offset distance for an option tooltip (px). Falls back to `badgeTooltipOffset`, then `8`. */
|
|
332
|
+
optionTooltipOffset?: number;
|
|
333
|
+
/** Anchor the option tooltip to the mouse pointer and follow it across the row (best for full-width rows). Default `false`. */
|
|
334
|
+
isOptionTooltipFollowCursor?: boolean;
|
|
265
335
|
/** Container element for dropdown/hint/popover (for Shadow DOM support) */
|
|
266
336
|
container?: HTMLElement | null;
|
|
267
337
|
/** Host element for appending hidden inputs (for form integration with shadow DOM) */
|
|
@@ -296,6 +366,7 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
296
366
|
private _getDisabledCallback?;
|
|
297
367
|
private _getValueFormatCallback?;
|
|
298
368
|
private _getBadgeTooltipCallback?;
|
|
369
|
+
private _getOptionTooltipCallback?;
|
|
299
370
|
private _getRemoveButtonTooltipCallback?;
|
|
300
371
|
private _renderOptionContentCallback?;
|
|
301
372
|
private _renderBadgeContentCallback?;
|
|
@@ -304,12 +375,17 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
304
375
|
private _renderSelectedContentCallback?;
|
|
305
376
|
private _getCounterCallback?;
|
|
306
377
|
private _actionButtons?;
|
|
378
|
+
private _batchDepth;
|
|
379
|
+
private _batchPartial;
|
|
380
|
+
private _batchNeedsReinit;
|
|
307
381
|
private _beforeSearchCallback?;
|
|
382
|
+
private _beforeSelectCallback?;
|
|
383
|
+
private _beforeDeselectCallback?;
|
|
308
384
|
private _searchCallback?;
|
|
309
385
|
private _addNewCallback?;
|
|
310
|
-
private
|
|
311
|
-
private
|
|
312
|
-
private
|
|
386
|
+
private _onSelect?;
|
|
387
|
+
private _onDeselect?;
|
|
388
|
+
private _onChange?;
|
|
313
389
|
static get observedAttributes(): string[];
|
|
314
390
|
constructor();
|
|
315
391
|
/**
|
|
@@ -323,6 +399,20 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
323
399
|
connectedCallback(): void;
|
|
324
400
|
disconnectedCallback(): void;
|
|
325
401
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
402
|
+
/**
|
|
403
|
+
* Set several attributes in one in-place update — a single re-render instead of one per
|
|
404
|
+
* attribute (and a single reinit at most if any change is structural). Keys are attribute
|
|
405
|
+
* names in kebab-case, exactly as `setAttribute`. A value of `null`/`undefined`/`false`
|
|
406
|
+
* removes the attribute; `true` sets it to an empty string; anything else is stringified.
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* el.setAttributes({
|
|
410
|
+
* 'search-placeholder': t('search'),
|
|
411
|
+
* 'select-placeholder': t('pick'),
|
|
412
|
+
* 'no-data-placeholder': t('noData'),
|
|
413
|
+
* });
|
|
414
|
+
*/
|
|
415
|
+
setAttributes(attrs: Record<string, string | number | boolean | null | undefined>): void;
|
|
326
416
|
private render;
|
|
327
417
|
private renderDebugInfo;
|
|
328
418
|
private updateDebugInfo;
|
|
@@ -407,6 +497,18 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
407
497
|
get checkboxAlign(): string | null;
|
|
408
498
|
set enableBadgeTooltips(value: boolean);
|
|
409
499
|
get enableBadgeTooltips(): boolean;
|
|
500
|
+
set enableOptionTooltips(value: boolean);
|
|
501
|
+
get enableOptionTooltips(): boolean;
|
|
502
|
+
set getOptionTooltipCallback(callback: ((item: T) => string | HTMLElement) | undefined);
|
|
503
|
+
get getOptionTooltipCallback(): ((item: T) => string | HTMLElement) | undefined;
|
|
504
|
+
set optionTooltipPlacement(value: string | null);
|
|
505
|
+
get optionTooltipPlacement(): string | null;
|
|
506
|
+
set optionTooltipFollowCursor(value: boolean);
|
|
507
|
+
get optionTooltipFollowCursor(): boolean;
|
|
508
|
+
set actionsPosition(value: string | null);
|
|
509
|
+
get actionsPosition(): string | null;
|
|
510
|
+
set actionsAlign(value: string | null);
|
|
511
|
+
get actionsAlign(): string | null;
|
|
410
512
|
set badgeTooltipPlacement(value: string | null);
|
|
411
513
|
get badgeTooltipPlacement(): string | null;
|
|
412
514
|
set getBadgeTooltipCallback(callback: ((item: T) => string | HTMLElement) | undefined);
|
|
@@ -419,16 +521,20 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
|
|
|
419
521
|
get getCounterCallback(): ((count: number, moreCount?: number) => string) | undefined;
|
|
420
522
|
get beforeSearchCallback(): ((searchTerm: string) => string | null) | undefined;
|
|
421
523
|
set beforeSearchCallback(callback: ((searchTerm: string) => string | null) | undefined);
|
|
422
|
-
get
|
|
423
|
-
set
|
|
524
|
+
get beforeSelectCallback(): ((option: T, selectedOptions: T[]) => boolean | void) | undefined;
|
|
525
|
+
set beforeSelectCallback(callback: ((option: T, selectedOptions: T[]) => boolean | void) | undefined);
|
|
526
|
+
get beforeDeselectCallback(): ((option: T, selectedOptions: T[]) => boolean | void) | undefined;
|
|
527
|
+
set beforeDeselectCallback(callback: ((option: T, selectedOptions: T[]) => boolean | void) | undefined);
|
|
528
|
+
get searchCallback(): ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | undefined;
|
|
529
|
+
set searchCallback(callback: ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | undefined);
|
|
424
530
|
get addNewCallback(): ((value: string) => T | Promise<T>) | undefined;
|
|
425
531
|
set addNewCallback(callback: ((value: string) => T | Promise<T>) | undefined);
|
|
426
|
-
get
|
|
427
|
-
set
|
|
428
|
-
get
|
|
429
|
-
set
|
|
430
|
-
get
|
|
431
|
-
set
|
|
532
|
+
get onSelect(): ((option: T) => void) | undefined;
|
|
533
|
+
set onSelect(callback: ((option: T) => void) | undefined);
|
|
534
|
+
get onDeselect(): ((option: T) => void) | undefined;
|
|
535
|
+
set onDeselect(callback: ((option: T) => void) | undefined);
|
|
536
|
+
get onChange(): ((selectedOptions: T[]) => void) | undefined;
|
|
537
|
+
set onChange(callback: ((selectedOptions: T[]) => void) | undefined);
|
|
432
538
|
get actionButtons(): any[] | undefined;
|
|
433
539
|
set actionButtons(value: any[] | undefined);
|
|
434
540
|
get selectedValue(): string | number | (string | number)[] | null;
|
|
@@ -478,11 +584,11 @@ export declare interface MultiSelectOption {
|
|
|
478
584
|
*/
|
|
479
585
|
export declare interface MultiSelectOptions extends MultiSelectConfig<MultiSelectOption> {
|
|
480
586
|
options?: MultiSelectOption[];
|
|
481
|
-
searchCallback?: ((searchTerm: string) => Promise<MultiSelectOption[]>) | null;
|
|
587
|
+
searchCallback?: ((searchTerm: string, signal?: AbortSignal) => Promise<MultiSelectOption[]>) | null;
|
|
482
588
|
addNewCallback?: ((value: string) => MultiSelectOption | Promise<MultiSelectOption>) | null;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
589
|
+
onSelect?: ((option: MultiSelectOption) => void) | null;
|
|
590
|
+
onDeselect?: ((option: MultiSelectOption) => void) | null;
|
|
591
|
+
onChange?: ((selectedOptions: MultiSelectOption[]) => void) | null;
|
|
486
592
|
}
|
|
487
593
|
|
|
488
594
|
/**
|
|
@@ -548,6 +654,8 @@ export declare class WebMultiSelect<T = any> {
|
|
|
548
654
|
private matchingIndices;
|
|
549
655
|
private searchTerm;
|
|
550
656
|
private isLoading;
|
|
657
|
+
private searchDebounceTimer?;
|
|
658
|
+
private searchAbortController?;
|
|
551
659
|
private showSelectedPopover;
|
|
552
660
|
private selectedPopoverPlacement;
|
|
553
661
|
private dropdownPlacement;
|
|
@@ -620,13 +728,43 @@ export declare class WebMultiSelect<T = any> {
|
|
|
620
728
|
* Render the Select All / Clear All / custom action buttons row.
|
|
621
729
|
* Returns the empty string if multiple-select is off or no buttons are configured.
|
|
622
730
|
*/
|
|
731
|
+
/**
|
|
732
|
+
* Default enabled/disabled state for the built-in actions, applied only when the consumer hasn't
|
|
733
|
+
* set an explicit `isDisabled` / `getIsDisabledCallback`:
|
|
734
|
+
* - `select-all` is disabled when it would add nothing (every selectable, non-disabled filtered
|
|
735
|
+
* option is already selected — this also covers an empty list).
|
|
736
|
+
* - `clear-all` is disabled when nothing is selected.
|
|
737
|
+
*/
|
|
738
|
+
private getBuiltInActionDisabled;
|
|
623
739
|
private renderActionsHTML;
|
|
624
740
|
private renderOption;
|
|
625
741
|
private highlightMatch;
|
|
626
742
|
private groupOptions;
|
|
743
|
+
/** Whether the input currently functions as a usable search field (drives placeholder wording). */
|
|
744
|
+
private get isSearchUsable();
|
|
745
|
+
/**
|
|
746
|
+
* Resolve the closed-state input placeholder for the current data/search state.
|
|
747
|
+
* Priority: explicit no-data placeholder (when the list is empty) → "pick" prompt when
|
|
748
|
+
* search is unusable → the search placeholder.
|
|
749
|
+
*/
|
|
750
|
+
private getPlaceholderText;
|
|
627
751
|
private renderBadges;
|
|
628
752
|
private attachEvents;
|
|
629
753
|
private handleSearch;
|
|
754
|
+
/** Abort the search request currently in flight, if any. The aborted request's results
|
|
755
|
+
* are then ignored (and the consumer's `searchCallback` can short-circuit its fetch via
|
|
756
|
+
* the `AbortSignal` it was handed). */
|
|
757
|
+
private abortInFlightSearch;
|
|
758
|
+
/**
|
|
759
|
+
* Invoke the async `searchCallback` and apply its results. Split out of `handleSearch`
|
|
760
|
+
* so it can be called immediately or after the debounce timer.
|
|
761
|
+
*
|
|
762
|
+
* Any request still in flight is aborted before a new one starts, so a slow earlier
|
|
763
|
+
* request can't overwrite a newer one — and consumers that wire the passed `AbortSignal`
|
|
764
|
+
* into their fetch get the request actually cancelled, not just ignored. The
|
|
765
|
+
* `aborted` / `searchTerm === value` guards drop superseded or out-of-order responses.
|
|
766
|
+
*/
|
|
767
|
+
private performAsyncSearch;
|
|
630
768
|
private handleKeydown;
|
|
631
769
|
private handleDropdownClick;
|
|
632
770
|
private handleBadgeClick;
|
|
@@ -646,6 +784,23 @@ export declare class WebMultiSelect<T = any> {
|
|
|
646
784
|
private focusPreviousMatch;
|
|
647
785
|
private scrollToFocused;
|
|
648
786
|
private toggleOption;
|
|
787
|
+
/**
|
|
788
|
+
* The single funnel for an interactive (user-initiated) selection. Consults
|
|
789
|
+
* `beforeSelectCallback` and only mutates state if allowed, so the veto can
|
|
790
|
+
* never be bypassed by a new UI entry point. Programmatic `setSelected` and
|
|
791
|
+
* the Select-All button deliberately do not route through here.
|
|
792
|
+
* Returns true if the option was selected, false if the veto blocked it.
|
|
793
|
+
*/
|
|
794
|
+
private interactiveSelect;
|
|
795
|
+
/**
|
|
796
|
+
* The single funnel for an interactive (user-initiated) deselection. Every
|
|
797
|
+
* removal affordance — dropdown toggle, badge × button, selected-items
|
|
798
|
+
* popover × button, and the "remove hidden" badge — routes through here so
|
|
799
|
+
* the `beforeDeselectCallback` veto applies uniformly. Programmatic
|
|
800
|
+
* `setSelected` and the Clear-All button deliberately bypass it.
|
|
801
|
+
* Returns true if the option was deselected, false if the veto blocked it.
|
|
802
|
+
*/
|
|
803
|
+
private interactiveDeselect;
|
|
649
804
|
private handleAddNew;
|
|
650
805
|
private selectOption;
|
|
651
806
|
private deselectOption;
|
|
@@ -654,7 +809,7 @@ export declare class WebMultiSelect<T = any> {
|
|
|
654
809
|
/**
|
|
655
810
|
* Re-render and fire callbacks after a selection state change.
|
|
656
811
|
* `added` / `removed` drive per-item select/deselect callbacks.
|
|
657
|
-
* `
|
|
812
|
+
* `onChange` fires once if anything actually changed.
|
|
658
813
|
*/
|
|
659
814
|
private commit;
|
|
660
815
|
private open;
|
|
@@ -738,6 +893,20 @@ export declare class WebMultiSelect<T = any> {
|
|
|
738
893
|
/** Build the remove-button tooltip text (callback > format string with {0} > "Remove {name}"). */
|
|
739
894
|
private buildRemoveButtonTooltipText;
|
|
740
895
|
private attachBadgeTooltips;
|
|
896
|
+
/** Build the option tooltip content (callback overrides; default = displayValue + optional subtitle on next line). */
|
|
897
|
+
private buildOptionTooltipContent;
|
|
898
|
+
/**
|
|
899
|
+
* Attach hover tooltips to the currently rendered dropdown options. Prunes existing option
|
|
900
|
+
* tooltips first, so it's safe to call on every render and on every virtual-scroll range change
|
|
901
|
+
* (where option DOM is recycled). Each option resolves its source object via `data-index` into
|
|
902
|
+
* `filteredOptions`, the same global index `renderOption` was given.
|
|
903
|
+
*/
|
|
904
|
+
private attachOptionTooltips;
|
|
905
|
+
/**
|
|
906
|
+
* Destroy only the option tooltips (prefixed `option-`). Called before re-rendering or
|
|
907
|
+
* recycling the options list so per-option tooltip state doesn't leak.
|
|
908
|
+
*/
|
|
909
|
+
private destroyAllOptionTooltips;
|
|
741
910
|
private attachActionButtonTooltips;
|
|
742
911
|
/**
|
|
743
912
|
* Destroy only the action-button tooltips. Called from `renderDropdown`/`renderDropdownVirtual`
|