@keenmate/web-multiselect 1.12.0-rc06 → 1.12.0-rc08

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,29 +21,19 @@ 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-rc06
25
-
26
- - **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.
27
- - **Per-node selectabilitymark 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.
28
- - **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 behaviourand 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.
29
- - **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.
30
- - **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.
31
- - **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`.
32
- - **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.
33
- - **Fixes — stable 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.
34
-
35
- ## What's New in v1.12.0-rc05
36
-
37
- > ⚠️ **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.
38
-
39
- - **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.
40
- - **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.
41
- - **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`.
42
- - **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.
43
- - **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.
44
- - **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`).
45
- - **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.
46
- - **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.
24
+ ## What's New in v1.12.0-rc08
25
+
26
+ - **Panel sizingdropdown and popover are independently sizable via CSS variables** — The options dropdown and selected-items popover no longer inherit the input's width. `--ms-dropdown-width` (defaults to the live input width) and `--ms-selected-popover-width` (intrinsic 32rem) drive them, alongside the existing max-height variables. Set them at app level (`web-multiselect { --ms-dropdown-width: 60rem }`) or override a single instance with the new `dropdown-width` / `selected-popover-width` attributes, which write those variables inline on the element. This also fixes a latent bug where `--ms-selected-popover-width` was dead (an internal width-sync always overrode it), so the popover now honours its 32rem default. See section 14 of `examples-tree.html`.
27
+ - **Tree renderingthe option render callback receives full tree context** — `renderOptionContentCallback(item, ctx)` gains `isTreeNode`, `isBranch`, `isLeaf`, `childCount`, `level`, `depth`, `path`, `isSelectable`, and the cascade `isIndeterminate` tristate. Rendering a child-count badge on branches or branch/leaf-specific markup is now a one-liner, without re-deriving the hierarchy yourself. All fields are optional (flat options report `isTreeNode: false`), so existing callbacks are untouched. Demoed as the new "Custom Node Rendering" section.
28
+ - **Cascade counter — the `[N]` chip counts what you actually picked** In cascade mode the counter used to show the emitted value count, which balloons under `cascade-select-policy="leaves"` / `"all"` (one branch click can emit five values)jarring next to a couple of rolled-up badges. It now counts the rolled-up minimal cover the branches you selected regardless of emit policy, and gained a hover tooltip listing those items. Under the default `rolled-up` policy nothing changes.
29
+ - **Theming cleanupdead input size-variant surface removed from the bundle** — The unused `.ms__input--xs/sm/lg/xl` preset classes and their `--ms-input-size-*` variable chain were never wired to anything (no `size` attribute toggles them), so they've been commented out kept for a possible future preset API but no longer shipped as dead CSS (the compiled stylesheet shrank ~2 kB). Input sizing still works through `--ms-rem` for proportional global scale or the individual `--ms-input-*` variables for targeted overrides.
30
+ - **Fixes badge hover and live cascade switching** — Badge hover no longer washes the chip to white (the hover background fell through to the white input background; it now deepens the accent tint, dark-mode aware). And switching `checkbox-mode` or `cascade-select-policy` live rebuilds the cascade index and silently re-projects the current selection, so badges, form value, and checkboxes all reflect the new mode instantly.
31
+
32
+ ## What's New in v1.12.0-rc07
33
+
34
+ - **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`.
35
+ - **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`.
36
+ - **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.
47
37
 
48
38
  > ⚠️ **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.
49
39
 
@@ -147,6 +147,7 @@
147
147
  { "name": "ms-dropdown-border-radius", "category": "dropdown", "usage": "Dropdown border radius" },
148
148
  { "name": "ms-dropdown-box-shadow", "category": "dropdown", "usage": "Dropdown shadow" },
149
149
  { "name": "ms-dropdown-box-shadow-semantic", "category": "dropdown", "usage": "Dropdown semantic shadow" },
150
+ { "name": "ms-dropdown-width", "category": "dropdown", "usage": "Options dropdown width (defaults to the input width; also set via the dropdown-width attribute)" },
150
151
  { "name": "ms-options-max-height", "category": "dropdown", "usage": "Options list max height" },
151
152
  { "name": "ms-z-index-dropdown", "category": "dropdown", "usage": "Dropdown z-index" },
152
153
  { "name": "ms-z-index-sticky", "category": "dropdown", "usage": "Sticky elements z-index" },
package/dist/index.d.ts CHANGED
@@ -667,7 +667,9 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
667
667
  get selectedValue(): string | number | (string | number)[] | null;
668
668
  get selectedItem(): T | null;
669
669
  getSelected(): T[];
670
- setSelected(values: (string | number)[]): void;
670
+ setSelected(values: (string | number)[], opts?: {
671
+ notify?: boolean;
672
+ }): void;
671
673
  getValue(): string | number | (string | number)[] | null;
672
674
  destroy(): void;
673
675
  }
@@ -743,6 +745,24 @@ declare interface OptionContentRenderContext {
743
745
  isMatched: boolean;
744
746
  /** Whether the option is disabled */
745
747
  isDisabled: boolean;
748
+ /** True when this row is a tree node (path-member / tree mode). Absent/false for flat options. */
749
+ isTreeNode?: boolean;
750
+ /** Tree only: the node has children (a branch). */
751
+ isBranch?: boolean;
752
+ /** Tree only: the node has no children (a leaf). */
753
+ isLeaf?: boolean;
754
+ /** Tree only: number of direct children (0 for a leaf). */
755
+ childCount?: number;
756
+ /** Tree only: 1-based depth level as derived from the path (top level = 1). */
757
+ level?: number;
758
+ /** Tree only: 0-based indentation depth (`level - 1`), matching `--ms-tree-depth`. */
759
+ depth?: number;
760
+ /** Tree only: the node's materialized path (e.g. "1.1.2"). */
761
+ path?: string;
762
+ /** Tree only: the node is selectable (branches marked non-selectable are `false`). */
763
+ isSelectable?: boolean;
764
+ /** Tree only: cascade tristate — a partially-checked branch (some but not all descendants). */
765
+ isIndeterminate?: boolean;
746
766
  }
747
767
 
748
768
  /**
@@ -885,6 +905,24 @@ export declare class WebMultiSelect<T = any> {
885
905
  * so badges / form / change events reflect the policy (rolled-up branches, etc.).
886
906
  */
887
907
  private toggleTreeCascade;
908
+ /**
909
+ * Given a checked-atom set, project it to emitted values under the active
910
+ * policy, diff it against the current selection, and commit. Shared by every
911
+ * cascade entry point (node toggle, Select All) so they all emit the same
912
+ * policy-projected shape (e.g. a full subtree rolls up to one value).
913
+ */
914
+ private commitCascadeAtoms;
915
+ /**
916
+ * The "meaningful selection" list used by the counter chip — the rolled-up
917
+ * minimal cover, regardless of the active emit policy. In cascade mode
918
+ * `leaves`/`all` emit many values for a single branch pick, which made the
919
+ * counter read e.g. `[5]` for what a person experiences as two selections.
920
+ * The counter should count the branches actually chosen, and stay stable when
921
+ * the policy knob flips. Outside cascade this is just the selected options.
922
+ */
923
+ private counterSelection;
924
+ /** Native `title` for the counter chip: the picked items, capped so it can't grow unbounded. */
925
+ private buildCounterTooltip;
888
926
  /**
889
927
  * Derive `treeNodes` + `filteredOptions` from the full tree, applying the
890
928
  * current search term. Matching nodes keep all their ancestors visible so
@@ -1081,7 +1119,18 @@ export declare class WebMultiSelect<T = any> {
1081
1119
  private updateHiddenInput;
1082
1120
  private getFormValue;
1083
1121
  getSelected(): T[];
1084
- setSelected(values: (string | number)[]): void;
1122
+ /**
1123
+ * Set the selection programmatically. **Silent by default** — it does not fire
1124
+ * `select`/`deselect`/`change` (so restoring saved state, cascade resets, or a
1125
+ * server-authoritative correction can't loop back or trip "user changed it"
1126
+ * handlers). Pass `{ notify: true }` to announce the result as a **single
1127
+ * aggregate `change`** — for a deliberate user gesture (e.g. an action button)
1128
+ * that should reach the same listeners a manual pick does, without the per-item
1129
+ * `select`/`deselect` flood a bulk change would otherwise cause.
1130
+ */
1131
+ setSelected(values: (string | number)[], opts?: {
1132
+ notify?: boolean;
1133
+ }): void;
1085
1134
  /**
1086
1135
  * Merge a partial config update into the live picker without tearing down the DOM.
1087
1136
  *