@keenmate/web-multiselect 1.12.0-rc05 → 1.12.0-rc07

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 CHANGED
@@ -21,25 +21,22 @@ 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 New in v1.12.0-rc05
25
-
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
-
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 buttonssmarter 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-selectdropdown 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
- - **Placeholderscontext-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.
24
+ ## What's New in v1.12.0-rc07
25
+
26
+ - **Selection`setSelected(values, { notify: true })` announces programmatic changes** `setSelected()` stays silent by default (restoring saved state, cascade/dependent resets, and server-authoritative corrections must not re-fire `change`, or they trip "the user changed it" handlers and can bounce in a feedback loop), but the new `{ notify: true }` option fires a **single aggregate `change`** no per-item `select`/`deselect` flood for when a programmatic change is a deliberate user gesture, e.g. a custom action button that sets the selection and should reach the same listeners a manual pick does. Threaded through both the internal picker and the web component's `setSelected`.
27
+ - **Cascade mode — Select All now honours the value policy** — the built-in `select-all` action added every selectable node's value directly, bypassing the cascade projection, so with `checkbox-mode="cascade"` + `cascade-select-policy="rolled-up"` it emitted every node instead of the rolled-up roots (unlike a click, which rolls up). `selectAll()` is now cascade-aware — it fills the checked-atom set from the visible nodes and projects through the active policy via a shared `commitCascadeAtoms` helper — so Select All emits the same shape a click does. Shown in the new "Action Buttons" section of `examples-tree.html`.
28
+ - **Options no longer text-select on click-drag** — clicking a row, or dragging across the dropdown, used to highlight the labels like selectable text, which reads as broken for a pure selection gesture (most visible on the denser tree rows). Options now set `user-select: none` (with the `-webkit-` prefix) on `.ms__option`. Purely presentational no API or behaviour change.
29
+
30
+ ## What's New in v1.12.0-rc06
31
+
32
+ - **Tree of options options can render as an always-expanded hierarchy** — give each option a materialized dot-path (`"1"`, `"1.1"`, `"1.1.1"`, …) and set `path-member` (or `el.getPathCallback`) and tree mode auto-enables, deriving parent/level from the path and rendering depth-first with indentation. The hierarchy model is a trimmed lift of `@keenmate/web-treeview`'s `ltree` (`src/tree/`) so ordering matches the treeview. There is no collapse/chevron — reach for `@keenmate/web-treeview` when you need expand/collapse. A separate `renderTreeNode` path carries the same selection/checkbox/icon/subtitle content plus depth indentation (via `--ms-tree-depth``src/css/tree.css`, tuned with `--ms-tree-indent`), and search filters the hierarchy to matches plus their ancestors so indentation stays coherent.
33
+ - **Per-node selectability mark tree nodes non-selectable without greying them out** — `is-selectable-member` (a data flag) or `el.getIsSelectableCallback` (a predicate over the built `LTreeNode`, so it can read the derived `node.hasChildren`) makes the common "leaves only" rule a one-liner. A non-selectable node is distinct from a disabled one: it renders *normally* but drops its checkbox, is skipped by keyboard focus, and can't be toggled by click/Enter or Select All. New `.ms__option--tree-unselectable` theming hook (plus `data-selectable="false"`) ships with only `cursor: default` and no hover highlight, so structural branch rows read as structure while staying visually normal.
34
+ - **External search now composes with tree mode** previously the async `searchCallback` path set the flat `filteredOptions` but not the parallel `treeNodes`, so an externalized search over a tree rendered blank or mis-indexed. It now rebuilds the hierarchy from the returned matches, keeping their ancestors (`rebuildTreeVisibleFromMatches`) — the async analogue of the built-in matches-plus-ancestors behaviour — and the below-min-length / error fallbacks are tree-aware too. A new `examples-search-index.html` demonstrates delegating the whole search to FlexSearch (fuzzy / accent-insensitive / ranked) over both a flat list and the full ISCO-08 tree, with an exact/prefix code lookup so `2211` resolves to its node and `72` to the whole subtree. FlexSearch stays a dev/consumer dependency — never in the shipped bundle.
35
+ - **Badge full title render a supplied breadcrumb on badges** — new `full-title-member` (or `el.getFullTitleCallback`) reads a fully-qualified label that ships with the data (e.g. `"Fruit / Pome fruit / Apple"`); it is never computed. Turn on `show-badge-full-title` and badges render that full title instead of the display value, falling back to the display value for options without one, while an explicit `getBadgeDisplayCallback` still wins. Pairs naturally with tree mode to show the full path in the badge.
36
+ - **Adjustable row height and long-title handling** — new CSS variables (no new attributes) control every `.ms__option`, flat and tree: `--ms-option-min-height` sets a consistent minimum row height for non-virtual rows (they still grow if content is taller; virtual rows keep the fixed `--ms-option-height`), and `--ms-option-title-white-space` / `-overflow` / `-text-overflow` flip a title from the default wrap to a one-line ellipsis. `.ms__option-title` is the label hook (the analog of web-treeview's `.wtv__node-label`), and because its flex ancestors already set `min-width: 0`, ellipsis works with no extra CSS — pair it with `enable-option-tooltips` to reveal the full text on hover.
37
+ - **Cascade checkboxes for tree mode, with a value policy** — set `checkbox-mode="cascade"` and checking a node toggles its whole subtree while a partially-selected branch shows a tristate (dash) box. Orthogonally, `cascade-select-policy` decides *which values* the selection emits: `rolled-up` (default) collapses a fully-selected subtree to one "complete node" value and surfaces individual descendants under partially-selected branches (the minimal cover — the bit svelte-treeview lacks); `leaves` emits only checked leaves; `all` emits every checked node like web-treeview. Canonical state is the set of checked leaf atoms and the emitted `selectedValues` is a pure projection, so badges/form/`change` are untouched and every policy round-trips. Non-breaking — `independent` (the previous behaviour) is the default. Logic is isolated in `src/tree/cascade.ts`; tristate renders from a CSS modifier class (virtual-scroll-safe). Demoed as §10 on `examples-tree.html`.
38
+ - **More pronounced option checkboxes** — the unchecked box draws its border from a dedicated mid-grey token (`--ms-checkbox-border-color`, default `#8f8f8f`) instead of the faint generic `--ms-border`, so it reads as a real, clickable control, with a slightly bolder checkmark. New themeable tokens `--ms-checkbox-border-width` (1px), `--ms-checkbox-border-color`, and `--ms-checkbox-checkmark-thickness` drive it, and `--ms-checkbox-border` / `--ms-checkbox-checked-border` recompose from the width token — override any of them to restyle.
39
+ - **Fixesstable tree rows across search** — two virtual-scroll/tree regressions are fixed: row height no longer jumps as filtering crosses the virtual-scroll threshold (non-virtual rows now pin to `--ms-option-height` when virtual scroll is enabled), and clearing a search with Escape (or reopening after a search) no longer leaves the dropdown blank both the Escape-clear and `close()` paths route through a tree-aware `resetVisibleToAll()` that rebuilds `treeNodes` from the full tree.
43
40
 
44
41
  > ⚠️ **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.
45
42
 
package/dist/index.d.ts CHANGED
@@ -120,6 +120,31 @@ export declare const interactionLogger: any;
120
120
  */
121
121
  export declare const LOGGING_CATEGORIES: string[];
122
122
 
123
+ declare interface LTreeNode<T> {
124
+ treeId: string;
125
+ id: NodeId;
126
+ /** Materialized dot-path, e.g. "1.2.3". */
127
+ path: string;
128
+ /** This node's own segment relative to its parent. */
129
+ pathSegment: string;
130
+ parentPath: string | null | undefined;
131
+ /** Depth in the tree (1-based; root children are level 1). */
132
+ level: number | null | undefined;
133
+ /** Children keyed by prefixed segment (see `segmentPrefix` in ltree.ts). */
134
+ children: Record<string, LTreeNode<T>>;
135
+ hasChildren: boolean;
136
+ /**
137
+ * Whether this node may be selected. Defaults to `true`; set `false` (via
138
+ * `isSelectableMember`/`getIsSelectableCallback`) to make a node non-interactive
139
+ * — it renders normally (no grey/disabled styling) but has no checkbox, is
140
+ * skipped by keyboard focus, and cannot be toggled or selected by Select-All.
141
+ * This is distinct from `disabled` (which greys the row out).
142
+ */
143
+ isSelectable: boolean;
144
+ /** The original option object this node was built from. */
145
+ data: T | null | undefined;
146
+ }
147
+
123
148
  /**
124
149
  * Generic configuration options for the MultiSelect component
125
150
  * @template T The type of data items
@@ -137,6 +162,15 @@ declare interface MultiSelectConfig<T = any> {
137
162
  getDisplayValueCallback?: (item: T) => string;
138
163
  /** Callback to customize badge display text (defaults to display value if not provided) */
139
164
  getBadgeDisplayCallback?: (item: T) => string;
165
+ /**
166
+ * Member property name for a "full title" — a fully-qualified label that ships with the
167
+ * data (e.g. a breadcrumb like "Fruit / Pome fruit / Apple"). It is never computed by the
168
+ * component. When `isBadgeFullTitleShown` is on, badges display this instead of the display
169
+ * value (falling back to the display value when an option has none).
170
+ */
171
+ fullTitleMember?: string;
172
+ /** Callback to extract the full title from an item (takes precedence over `fullTitleMember`). */
173
+ getFullTitleCallback?: (item: T) => string;
140
174
  /** Callback to add custom CSS classes to badges - return string or array of class names */
141
175
  getBadgeClassCallback?: (item: T) => string | string[];
142
176
  /** Callback to inject custom CSS into Shadow DOM - return CSS string for styling custom classes */
@@ -153,6 +187,55 @@ declare interface MultiSelectConfig<T = any> {
153
187
  subtitleMember?: string;
154
188
  /** Callback to extract subtitle from item */
155
189
  getSubtitleCallback?: (item: T) => string;
190
+ /**
191
+ * Enable tree mode: options are rendered as a hierarchy, indented by depth.
192
+ * Auto-enabled when a path source (`pathMember`/`getPathCallback`) is set;
193
+ * pass `false` to force it off. The tree is always fully expanded — there is
194
+ * no collapse (use @keenmate/web-treeview if you need expand/collapse).
195
+ */
196
+ isTreeEnabled?: boolean;
197
+ /** Member property name holding each option's materialized dot-path (e.g. "1.2.3"). */
198
+ pathMember?: string;
199
+ /** Callback returning an option's materialized dot-path (takes precedence over pathMember). */
200
+ getPathCallback?: (item: T) => string;
201
+ /** Member holding an option's parent path (otherwise derived from its path). */
202
+ parentPathMember?: string;
203
+ /** Member holding an option's depth/level (otherwise derived from its path). */
204
+ levelMember?: string;
205
+ /** Member holding a precomputed hasChildren flag (otherwise derived from the tree). */
206
+ hasChildrenMember?: string;
207
+ /** Path separator for tree paths. Default: "." */
208
+ treePathSeparator?: string;
209
+ /**
210
+ * Member holding a per-option `isSelectable` flag for tree mode. A node with a
211
+ * falsy value renders normally (NOT greyed like `disabled`) but has no checkbox,
212
+ * is skipped by keyboard focus, and cannot be toggled or picked by Select-All.
213
+ * Options default to selectable. Tree mode only.
214
+ */
215
+ isSelectableMember?: string;
216
+ /**
217
+ * Callback deciding whether a tree node is selectable (takes precedence over
218
+ * `isSelectableMember`). Receives the built tree node, so `node.hasChildren` /
219
+ * `node.level` are available — e.g. `(node) => !node.hasChildren` for a
220
+ * leaves-only tree. Tree mode only.
221
+ */
222
+ getIsSelectableCallback?: (node: LTreeNode<T>) => boolean;
223
+ /**
224
+ * Tree checkbox interaction. `independent` (default) toggles only the clicked
225
+ * node. `cascade` checks a node's whole subtree and shows a tristate
226
+ * (checked / indeterminate / unchecked) box on branches. Tree + multiple only.
227
+ */
228
+ checkboxMode?: 'independent' | 'cascade';
229
+ /**
230
+ * In `cascade` mode, which values a selection emits (badges / form / change):
231
+ * - `rolled-up` (default) — minimal cover: a fully-selected subtree collapses
232
+ * to its root ("complete node"); partially-selected branches emit their
233
+ * individually-checked descendants. Rolls to the nearest selectable
234
+ * descendant when the complete node itself is non-selectable.
235
+ * - `leaves` — only the checked leaf-level nodes.
236
+ * - `all` — every fully-checked node (branches and leaves), like web-treeview.
237
+ */
238
+ cascadeSelectPolicy?: 'rolled-up' | 'leaves' | 'all';
156
239
  /** Member property name for group extraction */
157
240
  groupMember?: string;
158
241
  /** Callback to extract group from item */
@@ -199,6 +282,12 @@ declare interface MultiSelectConfig<T = any> {
199
282
  isAddNewAllowed?: boolean;
200
283
  /** Show count badge next to toggle icon (internal: isCounterShown) */
201
284
  isCounterShown?: boolean;
285
+ /**
286
+ * Make badges display each option's `fullTitleMember` / `getFullTitleCallback` value
287
+ * instead of its display value. Falls back to the display value for options without a
288
+ * full title. An explicit `getBadgeDisplayCallback` still takes precedence. Off by default.
289
+ */
290
+ isBadgeFullTitleShown?: boolean;
202
291
  /** Keep initial options visible when searchCallback is active and search term is empty/short (internal: isKeepOptionsOnSearch) */
203
292
  isKeepOptionsOnSearch?: boolean;
204
293
  /** Keep search text and filtered results when dropdown closes (default: true) */
@@ -359,11 +448,15 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
359
448
  private _getIconCallback?;
360
449
  private _subtitleMember?;
361
450
  private _getSubtitleCallback?;
451
+ private _getFullTitleCallback?;
362
452
  private _groupMember?;
363
453
  private _getGroupCallback?;
364
454
  private _renderGroupLabelContentCallback?;
365
455
  private _disabledMember?;
366
456
  private _getDisabledCallback?;
457
+ private _getPathCallback?;
458
+ private _isTreeEnabled?;
459
+ private _getIsSelectableCallback?;
367
460
  private _getValueFormatCallback?;
368
461
  private _getBadgeTooltipCallback?;
369
462
  private _getOptionTooltipCallback?;
@@ -447,10 +540,28 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
447
540
  get iconMember(): string | null;
448
541
  set subtitleMember(value: string | null);
449
542
  get subtitleMember(): string | null;
543
+ set fullTitleMember(value: string | null);
544
+ get fullTitleMember(): string | null;
450
545
  set groupMember(value: string | null);
451
546
  get groupMember(): string | null;
452
547
  set disabledMember(value: string | null);
453
548
  get disabledMember(): string | null;
549
+ set pathMember(value: string | null);
550
+ get pathMember(): string | null;
551
+ set parentPathMember(value: string | null);
552
+ get parentPathMember(): string | null;
553
+ set levelMember(value: string | null);
554
+ get levelMember(): string | null;
555
+ set hasChildrenMember(value: string | null);
556
+ get hasChildrenMember(): string | null;
557
+ set isSelectableMember(value: string | null);
558
+ get isSelectableMember(): string | null;
559
+ set treePathSeparator(value: string | null);
560
+ get treePathSeparator(): string | null;
561
+ set checkboxMode(value: 'independent' | 'cascade' | null);
562
+ get checkboxMode(): 'independent' | 'cascade' | null;
563
+ set cascadeSelectPolicy(value: 'rolled-up' | 'leaves' | 'all' | null);
564
+ get cascadeSelectPolicy(): 'rolled-up' | 'leaves' | 'all' | null;
454
565
  set getValueCallback(callback: ((item: T) => string | number) | undefined);
455
566
  get getValueCallback(): ((item: T) => string | number) | undefined;
456
567
  set getDisplayValueCallback(callback: ((item: T) => string) | undefined);
@@ -467,8 +578,24 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
467
578
  get getIconCallback(): ((item: T) => string) | undefined;
468
579
  set getSubtitleCallback(callback: ((item: T) => string) | undefined);
469
580
  get getSubtitleCallback(): ((item: T) => string) | undefined;
581
+ /** Callback returning an option's full title (used by badges when show-badge-full-title is on). */
582
+ set getFullTitleCallback(callback: ((item: T) => string) | undefined);
583
+ get getFullTitleCallback(): ((item: T) => string) | undefined;
470
584
  set getGroupCallback(callback: ((item: T) => string) | undefined);
471
585
  get getGroupCallback(): ((item: T) => string) | undefined;
586
+ /** Callback returning an option's materialized dot-path (enables tree mode). */
587
+ set getPathCallback(callback: ((item: T) => string) | undefined);
588
+ get getPathCallback(): ((item: T) => string) | undefined;
589
+ /** Force tree mode on/off. When unset, tree mode auto-enables if a path source is present. */
590
+ set isTreeEnabled(value: boolean | undefined);
591
+ get isTreeEnabled(): boolean | undefined;
592
+ /**
593
+ * Callback deciding whether a tree node is selectable (takes precedence over
594
+ * `is-selectable-member`). Receives the built node — e.g.
595
+ * `el.getIsSelectableCallback = (node) => !node.hasChildren` for leaves only.
596
+ */
597
+ set getIsSelectableCallback(callback: ((node: LTreeNode<T>) => boolean) | undefined);
598
+ get getIsSelectableCallback(): ((node: LTreeNode<T>) => boolean) | undefined;
472
599
  set renderGroupLabelContentCallback(callback: ((groupName: string) => string | HTMLElement) | undefined);
473
600
  get renderGroupLabelContentCallback(): ((groupName: string) => string | HTMLElement) | undefined;
474
601
  set getDisabledCallback(callback: ((item: T) => boolean) | undefined);
@@ -540,7 +667,9 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
540
667
  get selectedValue(): string | number | (string | number)[] | null;
541
668
  get selectedItem(): T | null;
542
669
  getSelected(): T[];
543
- setSelected(values: (string | number)[]): void;
670
+ setSelected(values: (string | number)[], opts?: {
671
+ notify?: boolean;
672
+ }): void;
544
673
  getValue(): string | number | (string | number)[] | null;
545
674
  destroy(): void;
546
675
  }
@@ -591,6 +720,17 @@ export declare interface MultiSelectOptions extends MultiSelectConfig<MultiSelec
591
720
  onChange?: ((selectedOptions: MultiSelectOption[]) => void) | null;
592
721
  }
593
722
 
723
+ /**
724
+ * LTreeNode — a single node in the option tree.
725
+ *
726
+ * Trimmed lift of web-treeview's `ltree/ltree-node.ts`. The multiselect renders
727
+ * every node expanded (no collapse), so this keeps only the structural fields
728
+ * needed to order nodes and indent them: path/parent/level/children/hasChildren
729
+ * plus the original option `data`. Expand state, drag-drop, checkbox, selection
730
+ * and drop-position fields are all thrown out.
731
+ */
732
+ declare type NodeId = string | number;
733
+
594
734
  /**
595
735
  * Context provided to renderOptionContentCallback
596
736
  */
@@ -649,6 +789,10 @@ export declare class WebMultiSelect<T = any> {
649
789
  private selectedOptions;
650
790
  private allOptions;
651
791
  private filteredOptions;
792
+ private tree;
793
+ private treeNodes;
794
+ private cascadeIndex;
795
+ private cascadeCheckedAtoms;
652
796
  private hiddenInputs;
653
797
  private focusedIndex;
654
798
  private matchingIndices;
@@ -702,14 +846,79 @@ export declare class WebMultiSelect<T = any> {
702
846
  * text independently. Doesn't fit the extractField shape (no tuple/member layer of its own).
703
847
  */
704
848
  private getItemBadgeDisplayValue;
849
+ /**
850
+ * Full title — a fully-qualified label supplied with the data (never computed here). Used by
851
+ * badges when `isBadgeFullTitleShown` is on. Returns undefined when the option has none.
852
+ */
853
+ private getItemFullTitle;
705
854
  private getItemSearchValue;
706
855
  private getItemIcon;
707
856
  private getItemSubtitle;
708
857
  private getItemGroup;
709
858
  private getItemDisabled;
859
+ /**
860
+ * Tree mode: whether the visible node at `index` may be selected. Non-selectable
861
+ * nodes (see `isSelectableMember`/`getIsSelectableCallback`) still render — just
862
+ * without a checkbox — but are skipped by focus and cannot be toggled. Always
863
+ * true outside tree mode. `treeNodes` is index-aligned with `filteredOptions`.
864
+ */
865
+ private isIndexSelectable;
866
+ /** Whether an option may be selected. Always true outside tree mode. */
867
+ private isOptionSelectable;
710
868
  constructor(element: HTMLElement, options?: Partial<MultiSelectConfig<T>>);
711
869
  private init;
712
870
  private parseOptions;
871
+ /** Whether options should be rendered as an (always-expanded) tree. */
872
+ private isTreeMode;
873
+ /** (Re)build the ltree from `allOptions` and derive the visible flat list. */
874
+ private buildTree;
875
+ /**
876
+ * Whether cascade checkbox mode is active: a multi-select tree with
877
+ * `checkbox-mode="cascade"`. Checking a node then toggles its whole subtree
878
+ * and branches show a tristate box.
879
+ */
880
+ private isCascadeMode;
881
+ private cascadePolicy;
882
+ /** Refresh the derived checked-atom set from the emitted `selectedValues`. */
883
+ private refreshCascadeAtoms;
884
+ /**
885
+ * Toggle a tree node in cascade mode: flip its whole subtree, re-project the
886
+ * checked atoms to emitted values under the active policy, and commit the diff
887
+ * so badges / form / change events reflect the policy (rolled-up branches, etc.).
888
+ */
889
+ private toggleTreeCascade;
890
+ /**
891
+ * Given a checked-atom set, project it to emitted values under the active
892
+ * policy, diff it against the current selection, and commit. Shared by every
893
+ * cascade entry point (node toggle, Select All) so they all emit the same
894
+ * policy-projected shape (e.g. a full subtree rolls up to one value).
895
+ */
896
+ private commitCascadeAtoms;
897
+ /**
898
+ * Derive `treeNodes` + `filteredOptions` from the full tree, applying the
899
+ * current search term. Matching nodes keep all their ancestors visible so
900
+ * indentation stays coherent (the tree is always fully expanded).
901
+ */
902
+ private rebuildTreeVisible;
903
+ /**
904
+ * Reset the visible list to "everything". **Tree-aware**: in tree mode it
905
+ * rebuilds `treeNodes` (kept index-aligned with `filteredOptions`) from the
906
+ * full tree, so the two never drift. A raw `filteredOptions = [...allOptions]`
907
+ * would leave `treeNodes` stale after clearing a search — the virtual list
908
+ * then reserves height for every option but renders blank rows because
909
+ * `treeNodes[index]` is undefined. Always use this to clear the visible list.
910
+ */
911
+ private resetVisibleToAll;
912
+ /**
913
+ * Tree mode: derive the visible list from an **external** set of matched
914
+ * options — e.g. the results returned by `searchCallback` — keeping each
915
+ * match's ancestors so indentation stays coherent. This is the async-search
916
+ * analogue of `rebuildTreeVisible`: the matching is done by the caller (their
917
+ * own index/engine) instead of a local substring test, but ancestor
918
+ * preservation and `treeNodes`/`filteredOptions` index-alignment still happen
919
+ * here. Pass all options to show the whole tree.
920
+ */
921
+ private rebuildTreeVisibleFromMatches;
713
922
  private buildHTML;
714
923
  /**
715
924
  * Check if virtual scroll should be used
@@ -738,6 +947,14 @@ export declare class WebMultiSelect<T = any> {
738
947
  private getBuiltInActionDisabled;
739
948
  private renderActionsHTML;
740
949
  private renderOption;
950
+ /**
951
+ * Render a single tree-mode row. Separate from `renderOption`: a tree row is
952
+ * indented by its depth (via the `--ms-tree-depth` custom property) and
953
+ * tagged branch/leaf, but otherwise carries the same selection/checkbox/
954
+ * icon/subtitle content. The tree is always fully expanded, so there is no
955
+ * chevron/toggle — every node is just a normal, selectable option.
956
+ */
957
+ private renderTreeNode;
741
958
  private highlightMatch;
742
959
  private groupOptions;
743
960
  /** Whether the input currently functions as a usable search field (drives placeholder wording). */
@@ -774,6 +991,13 @@ export declare class WebMultiSelect<T = any> {
774
991
  * Returning -1 from `compute` is a no-op (used for empty list / no match).
775
992
  */
776
993
  private focusBy;
994
+ /**
995
+ * Given a target index and a preferred direction, return the nearest index
996
+ * whose node is selectable (skipping non-selectable tree nodes). Falls back to
997
+ * the opposite direction, then to -1 if nothing is selectable. No-op outside
998
+ * tree mode.
999
+ */
1000
+ private resolveSelectableIndex;
777
1001
  private focusNext;
778
1002
  private focusPrevious;
779
1003
  private focusFirst;
@@ -866,7 +1090,18 @@ export declare class WebMultiSelect<T = any> {
866
1090
  private updateHiddenInput;
867
1091
  private getFormValue;
868
1092
  getSelected(): T[];
869
- setSelected(values: (string | number)[]): void;
1093
+ /**
1094
+ * Set the selection programmatically. **Silent by default** — it does not fire
1095
+ * `select`/`deselect`/`change` (so restoring saved state, cascade resets, or a
1096
+ * server-authoritative correction can't loop back or trip "user changed it"
1097
+ * handlers). Pass `{ notify: true }` to announce the result as a **single
1098
+ * aggregate `change`** — for a deliberate user gesture (e.g. an action button)
1099
+ * that should reach the same listeners a manual pick does, without the per-item
1100
+ * `select`/`deselect` flood a bulk change would otherwise cause.
1101
+ */
1102
+ setSelected(values: (string | number)[], opts?: {
1103
+ notify?: boolean;
1104
+ }): void;
870
1105
  /**
871
1106
  * Merge a partial config update into the live picker without tearing down the DOM.
872
1107
  *