@keenmate/svelte-treeview 5.0.0-rc06 → 5.0.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/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.0.0-rc08] - 2026-05-27
11
+
12
+ ### Added
13
+ - **`{ silent: true }` option on highlight/selection methods**: `highlightNode`, `highlightNodes`, `clearHighlight`, `deselectAll` (and deprecated `selectNode`/`selectNodes`) now accept `{ silent: true }` to update state without firing `onNodeClick` / `onHighlightChange` / `onSelectionChange`. Intended for URL-restore flows (deep links loading form data from query params) where firing the change callback would re-trigger form loaders and clobber the data the URL just supplied. Silent mode also skips focusing the tree container so it doesn't steal focus from whatever the user is interacting with.
14
+ - **Array variants on expand/collapse methods**: `expandNodes`, `collapseNodes`, `expandAll`, and `collapseAll` now accept `string | string[]`. Single emit per call regardless of array length.
15
+ - **`{ exclusive: true }` option on `expandNodes` and `expandAll`**: Opens the target path(s) and collapses anything currently expanded that isn't on the union-of-spines (and, for `expandAll`, not under a target subtree). Equivalent to `collapseAll() + expandNodes(path)` but in a single pass with one emit — downstream listeners (transition animations, URL sync, virtualized renderers) don't see the intermediate fully-collapsed state. Non-collapsible nodes (`isCollapsible === false`) are never touched.
16
+ - **`{ noEmit: true }` option on all four expand/collapse methods**: Skips the change emit, enabling batching multiple operations and emitting once at the end via `tree.refresh()`.
17
+ - **`/examples/silent-highlight` demo page**: URL-restore scenario with loud vs. silent toggle showing how form data is preserved in silent mode.
18
+ - **`/examples/expand-collapse` demo page**: Demonstrates array variants and exclusive focus mode.
19
+
20
+ ## [5.0.0-rc07] - 2026-05-23
21
+
22
+ ### Added
23
+ - **`isSelectedMember` prop**: Data field name that seeds `node.isSelected` at `insertArray` time. The controller walks the tree after insert and pre-populates the bindable `selectedPaths` Set with every path where the field is truthy — independent of `isSelectableMember`, so non-selectable nodes can still ship checked.
24
+ - **`isSelectableMember` prop now publicly wired**: Previously declared on the core types but not exposed through `Tree.svelte`'s prop/update interface. Now end-to-end usable. Controls whether a node renders a checkbox and carries the `ltree-clickable` class.
25
+
26
+ ### Fixed
27
+ - **Filter race with async indexing**: A `bind:searchText` change that landed while the FlexSearch index was still being built (via `requestIdleCallback` batches) saw an empty index, hid the entire tree, and never recovered — no mechanism re-applied the filter once indexing completed. `filterNodes` now remembers the active query and the indexer's `onComplete` callback re-runs it, so the visible filter catches up automatically regardless of `indexerBatchSize`, dataset size, or how fast the user types.
28
+
10
29
  ## [5.0.0-rc06] - 2026-03-31
11
30
 
12
31
  ### Added
package/README.md CHANGED
@@ -6,31 +6,18 @@ A high-performance, feature-rich hierarchical tree view component for Svelte 5 w
6
6
 
7
7
  Browse interactive code examples and the full API reference at **[svelte-treeview.keenmate.dev](https://svelte-treeview.keenmate.dev)**
8
8
 
9
- ## What's New in v5.0.0-rc06
10
-
11
- - **Three-level selection**: `focusedNode` (click/arrows), `highlightedPaths` (Ctrl/Shift+click), `selectedPaths` (checkbox data state). Highlight and checkbox are independent build a selection, then check/uncheck all at once.
12
- - **`clickBehavior` prop**: `'select'` | `'expand'` | `'expand-and-focus'` (default). Replaces `shouldToggleOnNodeClick`.
13
- - **`showCheckboxes` + `checkboxMode`**: Custom styled checkboxes with `'independent'` or `'cascade'` mode (parent toggles descendants, indeterminate state).
14
- - **Shift+Arrow/Home/End/PageUp/PageDown**: Extends highlight range via keyboard, like file managers.
15
- - **Pluggable keyboard navigation**: `TreeNavigation<T>` interface — each renderer provides its own spatial implementation. Override individual methods via `TreeNavigationOverrides<T>`.
16
- - **Bulk subtree operations**: `insertBranch()`, `replaceBranch()`, `deleteBranch()` on TreeController — add, replace, or remove entire subtrees with a single emission.
17
- - **Clipboard API**: `copyNodes()`, `cutNodes()`, `pasteNodes()`, `cancelCut()` on TreeController. Cut nodes are visually dimmed. Supports auto-handle and manual (server-driven) paste modes.
18
-
19
- ### v5.0.0-rc03
20
-
21
- - **Multi-select**: Ctrl+click toggles nodes, Shift+click selects ranges, plain click replaces selection. New `selectedPaths` bindable (`Set<string>`), `onSelectionChanged` event, and `rangeSelectionMode` prop (`'visual'` vs `'logical'`).
22
- - **Public multi-select API**: `selectNode(path, mode)`, `selectNodes(paths)`, `deselectAll()`, `getSelectedNodes()`, `isNodeSelected(path)` on TreeController.
23
- - **Selection-aware context menus**: `contextMenuCallback` now receives `selectedNodes` as 3rd parameter for bulk actions.
24
-
25
- ### v5.0.0-rc03
26
-
27
- - **Unified Context Menu API**: New shared type system (`ContextMenuItem`, `ContextMenuDivider`, `ContextMenuEntry`) used by both svelte-treeview and canvas-tree. Breaking: `title` → `label`, `callback` → `onclick`, `isDivider` → separate `ContextMenuDivider` type.
28
- - **Context menu features**: Keyboard shortcuts, nested submenus, named dividers (`──── Section ────`), `isVisible`/`isDisabled` per item, `className="danger"`, async `onclick`.
29
- - **Svelte context menu components**: `ContextMenuItemC` and `ContextMenuDividerC` for declarative snippet-based menus alongside the callback approach.
30
- - **Accordion expand**: `accordionExpand={true}` — expanding a node auto-collapses its siblings.
31
- - **Toggle icon mode**: `toggleIconMode="rotate"` (default) smoothly rotates the expand icon vs `"swap"` which switches between two icons.
32
- - **Fix**: Expand/collapse icon not updating in flat rendering mode.
33
- - **Fix**: `vite.config.ts` no longer requires `@types/node`.
9
+ ## What's New in v5.0.0-rc08
10
+
11
+ - **`{ silent: true }` on highlight/selection methods**: Update tree state from URL params or other external sources without firing `onNodeClick` / `onHighlightChange` / `onSelectionChange` perfect for deep links where you don't want the change callback to re-trigger your form loader. Applies to `highlightNode`, `highlightNodes`, `clearHighlight`, and `deselectAll`.
12
+ - **Array variants for expand/collapse**: `expandNodes`, `collapseNodes`, `expandAll`, and `collapseAll` now take `string | string[]`. Open or close several places in one call.
13
+ - **Exclusive focus mode**: Pass `{ exclusive: true }` to `expandNodes` / `expandAll` to open the target path and collapse everything else in a single pass — no two-step flicker compared to `collapseAll() + expandNodes(path)`.
14
+ - **`noEmit` option for batching**: Suppress the change emit on individual expand/collapse calls when chaining several operations; emit once at the end.
15
+
16
+ ## What's New in v5.0.0-rc07
17
+
18
+ - **`isSelectedMember` prop**: Seed `selectedPaths` directly from your data — point the prop at a boolean field and every node where it's truthy lands pre-checked after `insertArray`.
19
+ - **`isSelectableMember` fully wired through `Tree`**: The prop was already on the core types but wasn't reachable via the component. Now end-to-end usable to control checkbox rendering and clickability per node.
20
+ - **Fix: filter race during async indexing**: `bind:searchText` no longer hides the entire tree if the FlexSearch index is still being built when the filter is applied. The filter now re-applies itself automatically once indexing completes, regardless of `indexerBatchSize` or dataset size.
34
21
 
35
22
  ## v5.0: Core/Renderer Split + Virtual Scroll
36
23
 
@@ -31,6 +31,7 @@
31
31
  parentPathMember?: string | null | undefined;
32
32
  levelMember?: string | null | undefined;
33
33
  isExpandedMember?: string | null | undefined;
34
+ isSelectableMember?: string | null | undefined;
34
35
  isSelectedMember?: string | null | undefined;
35
36
  isDraggableMember?: string | null | undefined;
36
37
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
@@ -184,6 +185,7 @@
184
185
  hasChildrenMember,
185
186
 
186
187
  isExpandedMember,
188
+ isSelectableMember,
187
189
  isSelectedMember,
188
190
  isDraggableMember,
189
191
  getIsDraggableCallback,
@@ -304,6 +306,7 @@
304
306
  levelMember,
305
307
  hasChildrenMember,
306
308
  isExpandedMember,
309
+ isSelectableMember,
307
310
  isSelectedMember,
308
311
  isDraggableMember,
309
312
  getIsDraggableCallback,
@@ -504,20 +507,32 @@
504
507
  );
505
508
 
506
509
  // ── Export public methods (thin proxies) ────────────────────────────
507
- export async function expandNodes(nodePath: string) {
508
- controller.expandNodes(nodePath);
510
+ export async function expandNodes(
511
+ nodePath: string | string[],
512
+ options?: { exclusive?: boolean; noEmit?: boolean }
513
+ ) {
514
+ controller.expandNodes(nodePath, options);
509
515
  }
510
516
 
511
- export async function collapseNodes(nodePath: string) {
512
- controller.collapseNodes(nodePath);
517
+ export async function collapseNodes(
518
+ nodePath: string | string[],
519
+ options?: { noEmit?: boolean }
520
+ ) {
521
+ controller.collapseNodes(nodePath, options);
513
522
  }
514
523
 
515
- export function expandAll(nodePath?: string | null | undefined) {
516
- controller.expandAll(nodePath);
524
+ export function expandAll(
525
+ nodePath?: string | string[] | null | undefined,
526
+ options?: { exclusive?: boolean; noEmit?: boolean }
527
+ ) {
528
+ controller.expandAll(nodePath, options);
517
529
  }
518
530
 
519
- export function collapseAll(nodePath?: string | null | undefined) {
520
- controller.collapseAll(nodePath);
531
+ export function collapseAll(
532
+ nodePath?: string | string[] | null | undefined,
533
+ options?: { noEmit?: boolean }
534
+ ) {
535
+ controller.collapseAll(nodePath, options);
521
536
  }
522
537
 
523
538
  export function filterNodes(searchTextVal: string, searchOptions?: SearchOptions): void {
@@ -610,16 +625,28 @@
610
625
  }
611
626
 
612
627
  // Multi-select methods
613
- export function selectNode(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace') {
614
- controller.selectNode(path, mode);
628
+ export function selectNode(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace', options?: { silent?: boolean }) {
629
+ controller.selectNode(path, mode, options);
630
+ }
631
+
632
+ export function selectNodes(paths: string[], options?: { silent?: boolean }) {
633
+ controller.selectNodes(paths, options);
634
+ }
635
+
636
+ export function highlightNode(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace', options?: { silent?: boolean }) {
637
+ controller.highlightNode(path, mode, options);
638
+ }
639
+
640
+ export function highlightNodes(paths: string[], options?: { silent?: boolean }) {
641
+ controller.highlightNodes(paths, options);
615
642
  }
616
643
 
617
- export function selectNodes(paths: string[]) {
618
- controller.selectNodes(paths);
644
+ export function clearHighlight(options?: { silent?: boolean }) {
645
+ controller.clearHighlight(options);
619
646
  }
620
647
 
621
- export function deselectAll() {
622
- controller.deselectAll();
648
+ export function deselectAll(options?: { silent?: boolean }) {
649
+ controller.deselectAll(options);
623
650
  }
624
651
 
625
652
  export function getSelectedNodes(): LTreeNode<T>[] {
@@ -656,6 +683,7 @@
656
683
  | "levelMember"
657
684
  | "hasChildrenMember"
658
685
  | "isExpandedMember"
686
+ | "isSelectableMember"
659
687
  | "isSelectedMember"
660
688
  | "isDraggableMember"
661
689
  | "getIsDraggableCallback"
@@ -728,6 +756,7 @@
728
756
  if (updates.levelMember !== undefined) levelMember = updates.levelMember;
729
757
  if (updates.hasChildrenMember !== undefined) hasChildrenMember = updates.hasChildrenMember;
730
758
  if (updates.isExpandedMember !== undefined) isExpandedMember = updates.isExpandedMember;
759
+ if (updates.isSelectableMember !== undefined) isSelectableMember = updates.isSelectableMember;
731
760
  if (updates.isSelectedMember !== undefined) isSelectedMember = updates.isSelectedMember;
732
761
  if (updates.isDraggableMember !== undefined) isDraggableMember = updates.isDraggableMember;
733
762
  if (updates.getIsDraggableCallback !== undefined) getIsDraggableCallback = updates.getIsDraggableCallback;
@@ -11,6 +11,7 @@ declare function $$render<T>(): {
11
11
  parentPathMember?: string | null | undefined;
12
12
  levelMember?: string | null | undefined;
13
13
  isExpandedMember?: string | null | undefined;
14
+ isSelectableMember?: string | null | undefined;
14
15
  isSelectedMember?: string | null | undefined;
15
16
  isDraggableMember?: string | null | undefined;
16
17
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
@@ -133,10 +134,20 @@ declare function $$render<T>(): {
133
134
  navigationOverrides?: TreeNavigationOverrides<T>;
134
135
  };
135
136
  exports: {
136
- expandNodes: (nodePath: string) => Promise<void>;
137
- collapseNodes: (nodePath: string) => Promise<void>;
138
- expandAll: (nodePath?: string | null | undefined) => void;
139
- collapseAll: (nodePath?: string | null | undefined) => void;
137
+ expandNodes: (nodePath: string | string[], options?: {
138
+ exclusive?: boolean;
139
+ noEmit?: boolean;
140
+ }) => Promise<void>;
141
+ collapseNodes: (nodePath: string | string[], options?: {
142
+ noEmit?: boolean;
143
+ }) => Promise<void>;
144
+ expandAll: (nodePath?: string | string[] | null | undefined, options?: {
145
+ exclusive?: boolean;
146
+ noEmit?: boolean;
147
+ }) => void;
148
+ collapseAll: (nodePath?: string | string[] | null | undefined, options?: {
149
+ noEmit?: boolean;
150
+ }) => void;
140
151
  filterNodes: (searchTextVal: string, searchOptions?: SearchOptions) => void;
141
152
  searchNodes: (searchTextVal: string | null | undefined, searchOptions?: SearchOptions) => LTreeNode<T>[];
142
153
  getChildren: (parentPath: string) => LTreeNode<T>[];
@@ -177,9 +188,24 @@ declare function $$render<T>(): {
177
188
  setExpandedPaths: (paths: string[]) => void;
178
189
  getAllData: () => T[];
179
190
  closeContextMenu: () => void;
180
- selectNode: (path: string, mode?: "replace" | "toggle" | "range") => void;
181
- selectNodes: (paths: string[]) => void;
182
- deselectAll: () => void;
191
+ selectNode: (path: string, mode?: "replace" | "toggle" | "range", options?: {
192
+ silent?: boolean;
193
+ }) => void;
194
+ selectNodes: (paths: string[], options?: {
195
+ silent?: boolean;
196
+ }) => void;
197
+ highlightNode: (path: string, mode?: "replace" | "toggle" | "range", options?: {
198
+ silent?: boolean;
199
+ }) => void;
200
+ highlightNodes: (paths: string[], options?: {
201
+ silent?: boolean;
202
+ }) => void;
203
+ clearHighlight: (options?: {
204
+ silent?: boolean;
205
+ }) => void;
206
+ deselectAll: (options?: {
207
+ silent?: boolean;
208
+ }) => void;
183
209
  getSelectedNodes: () => LTreeNode<T>[];
184
210
  isNodeSelected: (path: string) => boolean;
185
211
  scrollToPath: (path: string, options?: {
@@ -195,6 +221,7 @@ declare function $$render<T>(): {
195
221
  parentPathMember?: string | null | undefined;
196
222
  levelMember?: string | null | undefined;
197
223
  isExpandedMember?: string | null | undefined;
224
+ isSelectableMember?: string | null | undefined;
198
225
  isSelectedMember?: string | null | undefined;
199
226
  isDraggableMember?: string | null | undefined;
200
227
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
@@ -315,7 +342,7 @@ declare function $$render<T>(): {
315
342
  onTreeKeydown?: (event: KeyboardEvent, controller: TreeController<T>) => boolean | void;
316
343
  /** Override individual navigation methods (e.g. for custom ArrowDown/Up behavior) */
317
344
  navigationOverrides?: TreeNavigationOverrides<T>;
318
- }, "treeId" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "isSelectedMember" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "orderMember" | "isSorted" | "sortCallback" | "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "showCheckboxes" | "checkboxMode" | "beforeCheckboxToggleCallback" | "rangeSelectionMode" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayDebugInformation" | "shouldDisplayContextMenuInDebugMode" | "onNodeClick" | "onHighlightChange" | "onSelectionChange" | "onNodeDragStart" | "onNodeDragOver" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "getContextMenuItemsCallback" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "bodyClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset" | "accordionExpand">>) => void;
345
+ }, "treeId" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "isSelectableMember" | "isSelectedMember" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "orderMember" | "isSorted" | "sortCallback" | "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "showCheckboxes" | "checkboxMode" | "beforeCheckboxToggleCallback" | "rangeSelectionMode" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayDebugInformation" | "shouldDisplayContextMenuInDebugMode" | "onNodeClick" | "onHighlightChange" | "onSelectionChange" | "onNodeDragStart" | "onNodeDragOver" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "getContextMenuItemsCallback" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "bodyClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset" | "accordionExpand">>) => void;
319
346
  };
320
347
  bindings: "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "searchText" | "insertResult" | "isRendering";
321
348
  slots: {};
@@ -327,10 +354,20 @@ declare class __sveltets_Render<T> {
327
354
  slots(): ReturnType<typeof $$render<T>>['slots'];
328
355
  bindings(): "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "searchText" | "insertResult" | "isRendering";
329
356
  exports(): {
330
- expandNodes: (nodePath: string) => Promise<void>;
331
- collapseNodes: (nodePath: string) => Promise<void>;
332
- expandAll: (nodePath?: string | null | undefined) => void;
333
- collapseAll: (nodePath?: string | null | undefined) => void;
357
+ expandNodes: (nodePath: string | string[], options?: {
358
+ exclusive?: boolean;
359
+ noEmit?: boolean;
360
+ } | undefined) => Promise<void>;
361
+ collapseNodes: (nodePath: string | string[], options?: {
362
+ noEmit?: boolean;
363
+ } | undefined) => Promise<void>;
364
+ expandAll: (nodePath?: string | string[] | null | undefined, options?: {
365
+ exclusive?: boolean;
366
+ noEmit?: boolean;
367
+ } | undefined) => void;
368
+ collapseAll: (nodePath?: string | string[] | null | undefined, options?: {
369
+ noEmit?: boolean;
370
+ } | undefined) => void;
334
371
  filterNodes: (searchTextVal: string, searchOptions?: SearchOptions) => void;
335
372
  searchNodes: (searchTextVal: string | null | undefined, searchOptions?: SearchOptions) => LTreeNode<T>[];
336
373
  getChildren: (parentPath: string) => LTreeNode<T>[];
@@ -371,9 +408,24 @@ declare class __sveltets_Render<T> {
371
408
  setExpandedPaths: (paths: string[]) => void;
372
409
  getAllData: () => T[];
373
410
  closeContextMenu: () => void;
374
- selectNode: (path: string, mode?: "replace" | "toggle" | "range") => void;
375
- selectNodes: (paths: string[]) => void;
376
- deselectAll: () => void;
411
+ selectNode: (path: string, mode?: "replace" | "toggle" | "range", options?: {
412
+ silent?: boolean;
413
+ } | undefined) => void;
414
+ selectNodes: (paths: string[], options?: {
415
+ silent?: boolean;
416
+ } | undefined) => void;
417
+ highlightNode: (path: string, mode?: "replace" | "toggle" | "range", options?: {
418
+ silent?: boolean;
419
+ } | undefined) => void;
420
+ highlightNodes: (paths: string[], options?: {
421
+ silent?: boolean;
422
+ } | undefined) => void;
423
+ clearHighlight: (options?: {
424
+ silent?: boolean;
425
+ } | undefined) => void;
426
+ deselectAll: (options?: {
427
+ silent?: boolean;
428
+ } | undefined) => void;
377
429
  getSelectedNodes: () => LTreeNode<T>[];
378
430
  isNodeSelected: (path: string) => boolean;
379
431
  scrollToPath: (path: string, options?: {
@@ -389,6 +441,7 @@ declare class __sveltets_Render<T> {
389
441
  parentPathMember?: string | null | undefined;
390
442
  levelMember?: string | null | undefined;
391
443
  isExpandedMember?: string | null | undefined;
444
+ isSelectableMember?: string | null | undefined;
392
445
  isSelectedMember?: string | null | undefined;
393
446
  isDraggableMember?: string | null | undefined;
394
447
  getIsDraggableCallback?: ((node: LTreeNode<T>) => boolean) | undefined;
@@ -509,7 +562,7 @@ declare class __sveltets_Render<T> {
509
562
  onTreeKeydown?: ((event: KeyboardEvent, controller: TreeController<T>) => boolean | void) | undefined;
510
563
  /** Override individual navigation methods (e.g. for custom ArrowDown/Up behavior) */
511
564
  navigationOverrides?: Partial<import("../core/navigation.js").TreeNavigation<T>> | undefined;
512
- }, "treeId" | "data" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "shouldDisplayDebugInformation" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "beforeCheckboxToggleCallback" | "getContextMenuItemsCallback" | "isSelectedMember" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "showCheckboxes" | "checkboxMode" | "rangeSelectionMode" | "initializeIndexCallback" | "searchText" | "shouldUseInternalSearchIndex" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "accordionExpand" | "onNodeClick" | "onNodeDragStart" | "onNodeDragOver" | "onHighlightChange" | "onSelectionChange" | "bodyClass" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
565
+ }, "treeId" | "data" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isSelectableMember" | "isSelectedMember" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "shouldDisplayDebugInformation" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "beforeCheckboxToggleCallback" | "getContextMenuItemsCallback" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "showCheckboxes" | "checkboxMode" | "rangeSelectionMode" | "initializeIndexCallback" | "searchText" | "shouldUseInternalSearchIndex" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "accordionExpand" | "onNodeClick" | "onNodeDragStart" | "onNodeDragOver" | "onHighlightChange" | "onSelectionChange" | "bodyClass" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
513
566
  };
514
567
  }
515
568
  interface $$IsomorphicComponent {
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "5.0.0-rc06";
1
+ export declare const VERSION = "5.0.0-rc08";
2
2
  export declare const PACKAGE_NAME = "@keenmate/svelte-treeview";
3
3
  export declare const AUTHOR = "KeenMate";
4
4
  export declare const LICENSE = "MIT";
@@ -1,6 +1,6 @@
1
1
  // Auto-generated file - do not edit manually
2
2
  // Generated by scripts/generate-constants.js
3
- export const VERSION = "5.0.0-rc06";
3
+ export const VERSION = "5.0.0-rc08";
4
4
  export const PACKAGE_NAME = "@keenmate/svelte-treeview";
5
5
  export const AUTHOR = "KeenMate";
6
6
  export const LICENSE = "MIT";
@@ -56,6 +56,7 @@ export interface TreeControllerProps<T> {
56
56
  parentPathMember?: string | null | undefined;
57
57
  levelMember?: string | null | undefined;
58
58
  isExpandedMember?: string | null | undefined;
59
+ isSelectableMember?: string | null | undefined;
59
60
  isSelectedMember?: string | null | undefined;
60
61
  isDraggableMember?: string | null | undefined;
61
62
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
@@ -297,10 +298,20 @@ export declare class TreeController<T> {
297
298
  };
298
299
  constructor(props: TreeControllerProps<T>);
299
300
  handleVirtualScroll: (event: Event) => void;
300
- expandNodes(nodePath: string): Promise<void>;
301
- collapseNodes(nodePath: string): Promise<void>;
302
- expandAll(nodePath?: string | null | undefined): void;
303
- collapseAll(nodePath?: string | null | undefined): void;
301
+ expandNodes(nodePath: string | string[], options?: {
302
+ exclusive?: boolean;
303
+ noEmit?: boolean;
304
+ }): Promise<void>;
305
+ collapseNodes(nodePath: string | string[], options?: {
306
+ noEmit?: boolean;
307
+ }): Promise<void>;
308
+ expandAll(nodePath?: string | string[] | null | undefined, options?: {
309
+ exclusive?: boolean;
310
+ noEmit?: boolean;
311
+ }): void;
312
+ collapseAll(nodePath?: string | string[] | null | undefined, options?: {
313
+ noEmit?: boolean;
314
+ }): void;
304
315
  filterNodes(searchTextVal: string, searchOptions?: SearchOptions): void;
305
316
  searchNodes(searchTextVal: string | null | undefined, searchOptions?: SearchOptions): LTreeNode<T>[];
306
317
  getChildren(parentPath: string): LTreeNode<T>[];
@@ -440,12 +451,21 @@ export declare class TreeController<T> {
440
451
  /** Get ALL nodes between two paths (inclusive), in depth-first tree order.
441
452
  * Includes collapsed/hidden nodes — "logical" range selection. */
442
453
  private _getAllNodesBetween;
443
- /** Highlight a node with the given mode */
444
- highlightNode(path: string, mode?: 'replace' | 'toggle' | 'range'): void;
445
- /** Highlight multiple nodes by paths (replaces current highlights) */
446
- highlightNodes(paths: string[]): void;
447
- /** Clear all highlights */
448
- clearHighlight(): void;
454
+ /** Highlight a node with the given mode.
455
+ * Pass `{ silent: true }` to update state without firing `onNodeClick` / `onHighlightChange`
456
+ * (useful when restoring state from URL params or other external sources). */
457
+ highlightNode(path: string, mode?: 'replace' | 'toggle' | 'range', options?: {
458
+ silent?: boolean;
459
+ }): void;
460
+ /** Highlight multiple nodes by paths (replaces current highlights).
461
+ * Pass `{ silent: true }` to skip `onHighlightChange`. */
462
+ highlightNodes(paths: string[], options?: {
463
+ silent?: boolean;
464
+ }): void;
465
+ /** Clear all highlights. Pass `{ silent: true }` to skip `onHighlightChange`. */
466
+ clearHighlight(options?: {
467
+ silent?: boolean;
468
+ }): void;
449
469
  /** Get all highlighted nodes */
450
470
  getHighlightedNodes(): LTreeNode<T>[];
451
471
  /** Check if a specific node path is highlighted */
@@ -454,12 +474,18 @@ export declare class TreeController<T> {
454
474
  getSelectedNodes(): LTreeNode<T>[];
455
475
  /** Check if a specific node path is selected (checked) */
456
476
  isNodeSelected(path: string): boolean;
457
- /** Clear all checkbox selections */
458
- deselectAll(): void;
477
+ /** Clear all checkbox selections. Pass `{ silent: true }` to skip `onSelectionChange`. */
478
+ deselectAll(options?: {
479
+ silent?: boolean;
480
+ }): void;
459
481
  /** @deprecated Use highlightNode() instead */
460
- selectNode(path: string, mode?: 'replace' | 'toggle' | 'range'): void;
482
+ selectNode(path: string, mode?: 'replace' | 'toggle' | 'range', options?: {
483
+ silent?: boolean;
484
+ }): void;
461
485
  /** @deprecated Use highlightNodes() instead */
462
- selectNodes(paths: string[]): void;
486
+ selectNodes(paths: string[], options?: {
487
+ silent?: boolean;
488
+ }): void;
463
489
  private _onNodeRightClicked;
464
490
  private isDropAllowedByMode;
465
491
  private calculateDropPosition;
@@ -249,7 +249,7 @@ export class TreeController {
249
249
  this.onRenderCompleteHandler = props.onRenderComplete;
250
250
  // ── Create LTree ────────────────────────────────────────────────
251
251
  // svelte-ignore non_reactive_update
252
- this.tree = createLTree(props.idMember, props.pathMember, props.parentPathMember, props.levelMember, props.hasChildrenMember, props.isExpandedMember, props.isSelectedMember, props.isDraggableMember, props.getIsDraggableCallback, props.isDropAllowedMember, props.allowedDropPositionsMember, props.displayValueMember, props.getDisplayValueCallback, props.searchValueMember, props.getSearchValueCallback, props.getAllowedDropPositionsCallback, props.isCollapsibleMember, props.getIsCollapsibleCallback, props.orderMember, this.treeId, this.treePathSeparator, props.expandLevel, props.shouldUseInternalSearchIndex, props.initializeIndexCallback, props.indexerBatchSize, props.indexerTimeout, {
252
+ this.tree = createLTree(props.idMember, props.pathMember, props.parentPathMember, props.levelMember, props.hasChildrenMember, props.isExpandedMember, props.isSelectableMember, props.isSelectedMember, props.isDraggableMember, props.getIsDraggableCallback, props.isDropAllowedMember, props.allowedDropPositionsMember, props.displayValueMember, props.getDisplayValueCallback, props.searchValueMember, props.getSearchValueCallback, props.getAllowedDropPositionsCallback, props.isCollapsibleMember, props.getIsCollapsibleCallback, props.orderMember, this.treeId, this.treePathSeparator, props.expandLevel, props.shouldUseInternalSearchIndex, props.initializeIndexCallback, props.indexerBatchSize, props.indexerTimeout, {
253
253
  shouldDisplayDebugInformation: props.shouldDisplayDebugInformation,
254
254
  isSorted: props.isSorted,
255
255
  sortCallback: props.sortCallback
@@ -352,6 +352,19 @@ export class TreeController {
352
352
  this.vsMeasuredRowHeight = null;
353
353
  this.vsDetectedHeight = null;
354
354
  this.insertResult = this.tree.insertArray(this.data);
355
+ // Seed selectedPaths from node.isSelected flags written by insertArray
356
+ if (this.tree.isSelectedMember) {
357
+ const seeded = new Set();
358
+ const walk = (node) => {
359
+ if (node.isSelected)
360
+ seeded.add(node.path);
361
+ for (const key in node.children)
362
+ walk(node.children[key]);
363
+ };
364
+ for (const key in this.tree.root.children)
365
+ walk(this.tree.root.children[key]);
366
+ this.selectedPaths = seeded;
367
+ }
355
368
  }
356
369
  });
357
370
  // Progressive rendering for flat mode
@@ -497,17 +510,17 @@ export class TreeController {
497
510
  });
498
511
  };
499
512
  // ── Public API methods ──────────────────────────────────────────────
500
- async expandNodes(nodePath) {
501
- this.tree.expandNodes(nodePath);
513
+ async expandNodes(nodePath, options) {
514
+ this.tree.expandNodes(nodePath, options);
502
515
  }
503
- async collapseNodes(nodePath) {
504
- this.tree.collapseNodes(nodePath);
516
+ async collapseNodes(nodePath, options) {
517
+ this.tree.collapseNodes(nodePath, options);
505
518
  }
506
- expandAll(nodePath) {
507
- this.tree?.expandAll(nodePath);
519
+ expandAll(nodePath, options) {
520
+ this.tree?.expandAll(nodePath, options);
508
521
  }
509
- collapseAll(nodePath) {
510
- this.tree?.collapseAll(nodePath);
522
+ collapseAll(nodePath, options) {
523
+ this.tree?.collapseAll(nodePath, options);
511
524
  }
512
525
  filterNodes(searchTextVal, searchOptions) {
513
526
  this.tree?.filterNodes(searchTextVal, searchOptions);
@@ -1314,12 +1327,13 @@ export class TreeController {
1314
1327
  this.onSelectionChangeHandler = updates.onSelectionChange;
1315
1328
  }
1316
1329
  // ── Internal event handlers ─────────────────────────────────────────
1317
- async _onNodeClicked(node, modifiers) {
1330
+ async _onNodeClicked(node, modifiers, options) {
1318
1331
  if (this.contextMenuVisible) {
1319
1332
  this.closeContextMenu();
1320
1333
  }
1321
1334
  const ctrl = modifiers?.ctrl ?? false;
1322
1335
  const shift = modifiers?.shift ?? false;
1336
+ const silent = options?.silent ?? false;
1323
1337
  uiLogger.debug(`[highlight] Click on ${node.path}`, { ctrl, shift, lastAnchor: this.lastHighlightedPath, prevCount: this.highlightedPaths.size });
1324
1338
  if (ctrl) {
1325
1339
  // Toggle this node in/out of highlight
@@ -1365,11 +1379,17 @@ export class TreeController {
1365
1379
  }
1366
1380
  // Update focus
1367
1381
  this._setFocusedNode(node);
1368
- this.onNodeClickHandler?.(node);
1369
- this._notifyHighlightChanged();
1382
+ if (!silent) {
1383
+ this.onNodeClickHandler?.(node);
1384
+ this._notifyHighlightChanged();
1385
+ }
1370
1386
  this.tree.refresh();
1371
- // Focus the tree container so keyboard navigation works after clicking a node
1372
- this.containerElement?.focus();
1387
+ // Focus the tree container so keyboard navigation works after clicking a node.
1388
+ // Skip in silent mode — programmatic highlight (e.g. from URL params) shouldn't
1389
+ // steal focus from whatever the user is currently interacting with.
1390
+ if (!silent) {
1391
+ this.containerElement?.focus();
1392
+ }
1373
1393
  }
1374
1394
  /** Get all descendant paths of a node (depth-first) */
1375
1395
  _getDescendantPaths(node) {
@@ -1635,23 +1655,26 @@ export class TreeController {
1635
1655
  return result;
1636
1656
  }
1637
1657
  // ── Public highlight methods (UI selection) ────────────────────────
1638
- /** Highlight a node with the given mode */
1639
- highlightNode(path, mode = 'replace') {
1658
+ /** Highlight a node with the given mode.
1659
+ * Pass `{ silent: true }` to update state without firing `onNodeClick` / `onHighlightChange`
1660
+ * (useful when restoring state from URL params or other external sources). */
1661
+ highlightNode(path, mode = 'replace', options) {
1640
1662
  const node = this.tree.getNodeByPath(path);
1641
1663
  if (!node)
1642
1664
  return;
1643
1665
  if (mode === 'toggle') {
1644
- this._onNodeClicked(node, { ctrl: true, shift: false });
1666
+ this._onNodeClicked(node, { ctrl: true, shift: false }, options);
1645
1667
  }
1646
1668
  else if (mode === 'range') {
1647
- this._onNodeClicked(node, { ctrl: false, shift: true });
1669
+ this._onNodeClicked(node, { ctrl: false, shift: true }, options);
1648
1670
  }
1649
1671
  else {
1650
- this._onNodeClicked(node);
1672
+ this._onNodeClicked(node, undefined, options);
1651
1673
  }
1652
1674
  }
1653
- /** Highlight multiple nodes by paths (replaces current highlights) */
1654
- highlightNodes(paths) {
1675
+ /** Highlight multiple nodes by paths (replaces current highlights).
1676
+ * Pass `{ silent: true }` to skip `onHighlightChange`. */
1677
+ highlightNodes(paths, options) {
1655
1678
  this._clearAllHighlightFlags();
1656
1679
  const newPaths = new Set();
1657
1680
  let lastNode = null;
@@ -1669,15 +1692,17 @@ export class TreeController {
1669
1692
  this._setFocusedNode(lastNode);
1670
1693
  this.lastHighlightedPath = lastNode.path;
1671
1694
  }
1672
- this._notifyHighlightChanged();
1695
+ if (!options?.silent)
1696
+ this._notifyHighlightChanged();
1673
1697
  this.tree.refresh();
1674
1698
  }
1675
- /** Clear all highlights */
1676
- clearHighlight() {
1699
+ /** Clear all highlights. Pass `{ silent: true }` to skip `onHighlightChange`. */
1700
+ clearHighlight(options) {
1677
1701
  this._clearAllHighlightFlags();
1678
1702
  this.highlightedPaths = new Set();
1679
1703
  this.lastHighlightedPath = null;
1680
- this._notifyHighlightChanged();
1704
+ if (!options?.silent)
1705
+ this._notifyHighlightChanged();
1681
1706
  this.tree.refresh();
1682
1707
  }
1683
1708
  /** Get all highlighted nodes */
@@ -1709,20 +1734,21 @@ export class TreeController {
1709
1734
  isNodeSelected(path) {
1710
1735
  return this.selectedPaths.has(path);
1711
1736
  }
1712
- /** Clear all checkbox selections */
1713
- deselectAll() {
1737
+ /** Clear all checkbox selections. Pass `{ silent: true }` to skip `onSelectionChange`. */
1738
+ deselectAll(options) {
1714
1739
  this._clearAllSelectionFlags();
1715
1740
  this.selectedPaths = new Set();
1716
- this._notifySelectionChanged();
1741
+ if (!options?.silent)
1742
+ this._notifySelectionChanged();
1717
1743
  this.tree.refresh();
1718
1744
  }
1719
1745
  /** @deprecated Use highlightNode() instead */
1720
- selectNode(path, mode = 'replace') {
1721
- this.highlightNode(path, mode);
1746
+ selectNode(path, mode = 'replace', options) {
1747
+ this.highlightNode(path, mode, options);
1722
1748
  }
1723
1749
  /** @deprecated Use highlightNodes() instead */
1724
- selectNodes(paths) {
1725
- this.highlightNodes(paths);
1750
+ selectNodes(paths, options) {
1751
+ this.highlightNodes(paths, options);
1726
1752
  }
1727
1753
  _onNodeRightClicked(node, event) {
1728
1754
  if (!this.hasContextMenuSnippet && !this.getContextMenuItemsHandler) {
@@ -15,15 +15,10 @@ export interface LTreeNode<T> {
15
15
  children: Record<string, LTreeNode<T>>;
16
16
  hasChildren: boolean;
17
17
  data: T | null | undefined;
18
- useCallback: boolean;
19
- priority: number | null | undefined;
20
18
  isDraggable: boolean;
21
19
  isCollapsible: boolean;
22
20
  isDropAllowed: boolean;
23
21
  allowedDropPositions: DropPosition[] | null | undefined;
24
- isInsertAllowed: boolean;
25
- isNestAllowed: boolean;
26
- isCheckboxVisible: boolean | null | undefined;
27
22
  visualState: VisualState;
28
23
  isExpanded: boolean;
29
24
  isFocused: boolean;
@@ -15,15 +15,10 @@ export function createLTreeNode(data) {
15
15
  children: {},
16
16
  hasChildren: false,
17
17
  data: undefined,
18
- useCallback: false,
19
- priority: undefined,
20
18
  isDraggable: true,
21
19
  isCollapsible: true,
22
20
  isDropAllowed: true,
23
21
  allowedDropPositions: undefined,
24
- isInsertAllowed: true,
25
- isNestAllowed: true,
26
- isCheckboxVisible: false,
27
22
  visualState: VisualState.notSelected,
28
23
  isExpanded: false,
29
24
  isFocused: false,
@@ -1,4 +1,4 @@
1
1
  import { Index } from 'flexsearch';
2
2
  import { type LTreeNode } from './ltree-node.svelte';
3
3
  import type { Ltree } from './types.js';
4
- export declare function createLTree<T>(_idMember: string, _pathMember: string, _parentPathMember?: string | null | undefined, _levelMember?: string | null | undefined, _hasChildrenMember?: string | null | undefined, _isExpandedMember?: string | null | undefined, _isSelectableMember?: string | null | undefined, _isDraggableMember?: string | null | undefined, _getIsDraggableCallback?: (node: LTreeNode<T>) => boolean, _isDropAllowedMember?: string | null | undefined, _allowedDropPositionsMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => import('./types.js').DropPosition[] | null | undefined, _isCollapsibleMember?: string | null | undefined, _getIsCollapsibleCallback?: (node: LTreeNode<T>) => boolean, _orderMember?: string | null | undefined, _treeId?: string, _treePathSeparator?: string | null | undefined, _expandLevel?: number | null | undefined, _shouldUseInternalSearchIndex?: boolean | null | undefined, _initializeIndexCallback?: () => Index, _indexerBatchSize?: number | null | undefined, _indexerTimeout?: number | null | undefined, opts?: Partial<Ltree<T>>): Ltree<T>;
4
+ export declare function createLTree<T>(_idMember: string, _pathMember: string, _parentPathMember?: string | null | undefined, _levelMember?: string | null | undefined, _hasChildrenMember?: string | null | undefined, _isExpandedMember?: string | null | undefined, _isSelectableMember?: string | null | undefined, _isSelectedMember?: string | null | undefined, _isDraggableMember?: string | null | undefined, _getIsDraggableCallback?: (node: LTreeNode<T>) => boolean, _isDropAllowedMember?: string | null | undefined, _allowedDropPositionsMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => import('./types.js').DropPosition[] | null | undefined, _isCollapsibleMember?: string | null | undefined, _getIsCollapsibleCallback?: (node: LTreeNode<T>) => boolean, _orderMember?: string | null | undefined, _treeId?: string, _treePathSeparator?: string | null | undefined, _expandLevel?: number | null | undefined, _shouldUseInternalSearchIndex?: boolean | null | undefined, _initializeIndexCallback?: () => Index, _indexerBatchSize?: number | null | undefined, _indexerTimeout?: number | null | undefined, opts?: Partial<Ltree<T>>): Ltree<T>;
@@ -9,12 +9,13 @@ import { perfStart, perfEnd, perfSummary } from '../perf-logger.js';
9
9
  function getField(item, member) {
10
10
  return item[member];
11
11
  }
12
- export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _getIsDraggableCallback, _isDropAllowedMember, _allowedDropPositionsMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _getAllowedDropPositionsCallback, _isCollapsibleMember, _getIsCollapsibleCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
12
+ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isSelectedMember, _isDraggableMember, _getIsDraggableCallback, _isDropAllowedMember, _allowedDropPositionsMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _getAllowedDropPositionsCallback, _isCollapsibleMember, _getIsCollapsibleCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
13
13
  let shouldCalculateParentPath = isEmptyString(_parentPathMember);
14
14
  let shouldCalculateLevel = isEmptyString(_levelMember);
15
15
  let shouldCalculateHasChildren = isEmptyString(_hasChildrenMember);
16
16
  let shouldCalculateIsExpanded = isEmptyString(_isExpandedMember);
17
17
  let shouldCalculateIsSelectable = isEmptyString(_isSelectableMember);
18
+ let shouldCalculateIsSelected = isEmptyString(_isSelectedMember);
18
19
  let shouldCalculateIsDraggable = isEmptyString(_isDraggableMember);
19
20
  let shouldCalculateIsDropAllowed = isEmptyString(_isDropAllowedMember);
20
21
  let shouldCalculateAllowedDropPositions = isEmptyString(_allowedDropPositionsMember);
@@ -31,7 +32,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
31
32
  if (_shouldUseInternalSearchIndex)
32
33
  searchIndex = _initializeIndexCallback ? _initializeIndexCallback() : createSearchIndex();
33
34
  let changeTracker = $state(Symbol());
34
- let size = 0;
35
35
  let nodeCount = 0;
36
36
  let maxLevel = 0;
37
37
  let flatTreeNodes = [];
@@ -42,6 +42,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
42
42
  let cachedVisibleFlatNodesTracker = null;
43
43
  // Async search indexing infrastructure
44
44
  let indexer = null;
45
+ // Last search text passed to filterNodes. The FlexSearch index is built
46
+ // asynchronously via the Indexer, so a filter applied while the index is
47
+ // still warming up returns 0 matches and hides the entire tree. We track
48
+ // the active query here and re-apply it from the indexer's onComplete
49
+ // callback so the visible filter eventually reflects the final index.
50
+ let _lastFilterSearchText = null;
51
+ let _lastFilterSearchOptions = undefined;
45
52
  // Initialize indexer when search index is available
46
53
  if (_shouldUseInternalSearchIndex && searchIndex) {
47
54
  indexer = new Indexer(_treeId || 'unknown', searchIndex, shouldCalculateSearchValue, _searchValueMember, _getSearchValueCallback, _indexerBatchSize || 25, // batch size with fallback
@@ -62,6 +69,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
62
69
  levelMember: _levelMember,
63
70
  isExpandedMember: _isExpandedMember,
64
71
  isSelectableMember: _isSelectableMember,
72
+ isSelectedMember: _isSelectedMember,
65
73
  isDraggableMember: _isDraggableMember,
66
74
  getIsDraggableCallback: _getIsDraggableCallback,
67
75
  isDropAllowedMember: _isDropAllowedMember,
@@ -104,7 +112,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
104
112
  const _tracker = changeTracker;
105
113
  // Return cached result if changeTracker hasn't changed
106
114
  if (_tracker === cachedVisibleFlatNodesTracker && cachedVisibleFlatNodes.length > 0) {
107
- // console.log(`[visibleFlatNodes] Cache HIT - returning ${cachedVisibleFlatNodes.length} nodes`);
108
115
  return cachedVisibleFlatNodes;
109
116
  }
110
117
  const computeStart = performance.now();
@@ -192,6 +199,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
192
199
  node.isExpanded = (node.level ?? 0) <= _expandLevel;
193
200
  if (!shouldCalculateIsSelectable)
194
201
  node.isSelectable = getField(row, _isSelectableMember);
202
+ if (!shouldCalculateIsSelected)
203
+ node.isSelected = getField(row, _isSelectedMember);
195
204
  if (!shouldCalculateIsDraggable)
196
205
  node.isDraggable = getField(row, _isDraggableMember);
197
206
  if (!shouldCalculateIsCollapsible)
@@ -270,7 +279,14 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
270
279
  if (itemsToIndex.length > 0 && indexer) {
271
280
  indexer.setCallbacks(undefined, // no progress callback for now
272
281
  () => {
273
- // Completion callback - refresh tree when indexing is done
282
+ // Completion callback. Re-apply any active filter before
283
+ // emitting — a filterNodes() call that landed while indexing
284
+ // was still in flight saw an incomplete index, so the visible
285
+ // tree may be missing matching paths until we refresh.
286
+ if (!isEmptyString(_lastFilterSearchText)) {
287
+ // isEmptyString guarantees non-empty string here.
288
+ this.filterNodes(_lastFilterSearchText, _lastFilterSearchOptions);
289
+ }
274
290
  if (!noEmitChanges) {
275
291
  this._emitTreeChanged();
276
292
  }
@@ -318,6 +334,12 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
318
334
  return null;
319
335
  },
320
336
  filterNodes(_searchText, _searchOptions) {
337
+ // Remember the active query so the indexer's onComplete callback can
338
+ // re-apply it once the FlexSearch index has finished warming up —
339
+ // otherwise a filter typed while indexing is still in flight hides
340
+ // the entire tree and never recovers.
341
+ _lastFilterSearchText = _searchText;
342
+ _lastFilterSearchOptions = _searchOptions;
321
343
  if (isEmptyString(_searchText)) {
322
344
  // Clear filter when search is empty
323
345
  filteredRoot.children = {};
@@ -349,7 +371,6 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
349
371
  perfStart(`[${_treeId}] createFilteredTree`);
350
372
  filteredRoot.children = {};
351
373
  filteredTree = null;
352
- // isFiltered = false;
353
374
  // 1. Expand all target paths to include their parents
354
375
  const allRequiredPaths = new Set();
355
376
  targetPaths.forEach((path) => {
@@ -416,28 +437,89 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
416
437
  this.isFiltered = false;
417
438
  this._emitTreeChanged();
418
439
  },
419
- expandAll(nodePath) {
440
+ expandAll(nodePath, options) {
420
441
  perfStart(`[${_treeId}] expandAll`);
442
+ const self = this;
443
+ const exclusive = options?.exclusive ?? false;
444
+ const noEmit = options?.noEmit ?? false;
421
445
  function setExpandedRecursive(node, value) {
422
446
  node.isExpanded = value;
423
447
  for (const key in node.children) {
424
448
  setExpandedRecursive(node.children[key], value);
425
449
  }
426
450
  }
427
- if (isEmptyString(nodePath)) {
451
+ const paths = Array.isArray(nodePath)
452
+ ? nodePath.filter((p) => !isEmptyString(p))
453
+ : isEmptyString(nodePath)
454
+ ? []
455
+ : [nodePath];
456
+ if (paths.length === 0) {
457
+ // Whole-tree expand. exclusive has no meaning (nothing to exclude from).
428
458
  setExpandedRecursive(root, true);
429
459
  }
430
460
  else {
431
- const target = this.getNodeByPath(nodePath);
432
- if (target)
433
- setExpandedRecursive(target, true);
461
+ if (exclusive) {
462
+ // Build spine: every ancestor path of every target must stay expanded
463
+ // so the target subtree is reachable.
464
+ const spineSet = new Set();
465
+ for (const p of paths) {
466
+ const segs = p.split(this.treePathSeparator);
467
+ for (let i = 0; i < segs.length; i++) {
468
+ spineSet.add(segs.slice(0, i + 1).join(this.treePathSeparator));
469
+ }
470
+ }
471
+ const isUnderTarget = (path) => {
472
+ for (const tp of paths) {
473
+ if (path === tp || path.startsWith(tp + this.treePathSeparator))
474
+ return true;
475
+ }
476
+ return false;
477
+ };
478
+ // Walk currently-expanded subtree; collapse anything not on a spine
479
+ // and not under a target. Only descends into expanded branches.
480
+ const trim = (node) => {
481
+ for (const key in node.children) {
482
+ const child = node.children[key];
483
+ if (!child.isExpanded)
484
+ continue;
485
+ if (spineSet.has(child.path) || isUnderTarget(child.path)) {
486
+ trim(child);
487
+ }
488
+ else if (self.getNodeIsCollapsible(child)) {
489
+ child.isExpanded = false;
490
+ trim(child);
491
+ }
492
+ }
493
+ };
494
+ trim(root);
495
+ }
496
+ // Expand spine + entire subtree under each target.
497
+ for (const p of paths) {
498
+ let node = root;
499
+ const segments = p.split(this.treePathSeparator);
500
+ for (let i = 0; i < segments.length; i++) {
501
+ const segment = segmentPrefix + segments[i];
502
+ if (node && node.children.hasOwnProperty(segment)) {
503
+ node = node.children[segment];
504
+ node.isExpanded = true;
505
+ }
506
+ else {
507
+ node = undefined;
508
+ break;
509
+ }
510
+ }
511
+ if (node)
512
+ setExpandedRecursive(node, true);
513
+ }
434
514
  }
435
- this._emitTreeChanged();
515
+ if (!noEmit)
516
+ this._emitTreeChanged();
436
517
  perfEnd(`[${_treeId}] expandAll`);
437
518
  },
438
- collapseAll(nodePath) {
519
+ collapseAll(nodePath, options) {
439
520
  perfStart(`[${_treeId}] collapseAll`);
440
521
  const self = this;
522
+ const noEmit = options?.noEmit ?? false;
441
523
  function collapseRecursive(node) {
442
524
  if (node.isExpanded && self.getNodeIsCollapsible(node)) {
443
525
  node.isExpanded = false;
@@ -446,15 +528,23 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
446
528
  collapseRecursive(node.children[key]);
447
529
  }
448
530
  }
449
- if (isEmptyString(nodePath)) {
531
+ const paths = Array.isArray(nodePath)
532
+ ? nodePath.filter((p) => !isEmptyString(p))
533
+ : isEmptyString(nodePath)
534
+ ? []
535
+ : [nodePath];
536
+ if (paths.length === 0) {
450
537
  collapseRecursive(root);
451
538
  }
452
539
  else {
453
- const target = this.getNodeByPath(nodePath);
454
- if (target)
455
- collapseRecursive(target);
540
+ for (const p of paths) {
541
+ const target = this.getNodeByPath(p);
542
+ if (target)
543
+ collapseRecursive(target);
544
+ }
456
545
  }
457
- this._emitTreeChanged();
546
+ if (!noEmit)
547
+ this._emitTreeChanged();
458
548
  perfEnd(`[${_treeId}] collapseAll`);
459
549
  },
460
550
  insert: function (path, data, noEmitChanges = false) {
@@ -467,59 +557,97 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
467
557
  }
468
558
  node = node.children[segment];
469
559
  }
470
- // Mark as end of path and store data
471
- if (node.hasChildren) {
472
- size++;
473
- }
474
560
  node.hasChildren = true;
475
561
  node.data = data;
476
562
  if (!noEmitChanges) {
477
563
  this._emitTreeChanged();
478
564
  }
479
565
  },
480
- expandNodes: function (path, noEmitChanges = false) {
566
+ expandNodes: function (path, options) {
481
567
  perfStart(`[${_treeId}] expandNodes`);
482
- let node = this.isFiltered ? filteredRoot : root;
568
+ const self = this;
569
+ const exclusive = options?.exclusive ?? false;
570
+ const noEmit = options?.noEmit ?? false;
571
+ const rootNode = this.isFiltered ? filteredRoot : root;
572
+ const paths = Array.isArray(path) ? path : [path];
483
573
  let hasChanges = false;
484
- const segments = path.split(this.treePathSeparator);
485
- for (let i = 0; i < segments.length; i++) {
486
- const segment = segmentPrefix + segments[i];
487
- if (node.children.hasOwnProperty(segment)) {
488
- node = node.children[segment];
489
- // Only mark as changed if actually changing from collapsed to expanded
490
- if (!node.isExpanded) {
491
- node.isExpanded = true;
492
- hasChanges = true;
574
+ if (exclusive) {
575
+ // Build spine set: union of every ancestor path across all inputs.
576
+ const spineSet = new Set();
577
+ for (const p of paths) {
578
+ const segs = p.split(this.treePathSeparator);
579
+ for (let i = 0; i < segs.length; i++) {
580
+ spineSet.add(segs.slice(0, i + 1).join(this.treePathSeparator));
581
+ }
582
+ }
583
+ // Walk currently-expanded subtree, collapse anything off-spine.
584
+ const trim = (node) => {
585
+ for (const key in node.children) {
586
+ const child = node.children[key];
587
+ if (!child.isExpanded)
588
+ continue;
589
+ if (spineSet.has(child.path)) {
590
+ trim(child);
591
+ }
592
+ else if (self.getNodeIsCollapsible(child)) {
593
+ child.isExpanded = false;
594
+ hasChanges = true;
595
+ trim(child);
596
+ }
597
+ }
598
+ };
599
+ trim(rootNode);
600
+ }
601
+ for (const p of paths) {
602
+ let node = rootNode;
603
+ const segments = p.split(this.treePathSeparator);
604
+ for (let i = 0; i < segments.length; i++) {
605
+ const segment = segmentPrefix + segments[i];
606
+ if (node && node.children.hasOwnProperty(segment)) {
607
+ node = node.children[segment];
608
+ if (!node.isExpanded) {
609
+ node.isExpanded = true;
610
+ hasChanges = true;
611
+ }
612
+ }
613
+ else {
614
+ break;
493
615
  }
494
616
  }
495
617
  }
496
- // Only emit changes if something actually changed
497
- if (!noEmitChanges && hasChanges) {
618
+ if (!noEmit && hasChanges) {
498
619
  this._emitTreeChanged();
499
620
  }
500
621
  perfEnd(`[${_treeId}] expandNodes`);
501
- return this; // Return the API object for chaining
622
+ return this;
502
623
  },
503
- collapseNodes: function (path, noEmitChanges = false) {
504
- let node = this.isFiltered ? filteredRoot : this.root;
624
+ collapseNodes: function (path, options) {
625
+ const noEmit = options?.noEmit ?? false;
626
+ const rootNode = this.isFiltered ? filteredRoot : this.root;
627
+ const paths = Array.isArray(path) ? path : [path];
505
628
  let hasChanges = false;
506
- const segments = path.split(this.treePathSeparator);
507
- for (let i = 0; i < segments.length; i++) {
508
- const segment = segmentPrefix + segments[i];
509
- if (node.children.hasOwnProperty(segment)) {
510
- node = node.children[segment];
629
+ for (const p of paths) {
630
+ let node = rootNode;
631
+ const segments = p.split(this.treePathSeparator);
632
+ for (let i = 0; i < segments.length; i++) {
633
+ const segment = segmentPrefix + segments[i];
634
+ if (node && node.children.hasOwnProperty(segment)) {
635
+ node = node.children[segment];
636
+ }
637
+ else {
638
+ node = undefined;
639
+ break;
640
+ }
641
+ }
642
+ if (node && node.isExpanded) {
643
+ node.isExpanded = false;
644
+ hasChanges = true;
511
645
  }
512
646
  }
513
- // Only collapse the target node, not ancestors
514
- if (node.isExpanded) {
515
- node.isExpanded = false;
516
- hasChanges = true;
517
- }
518
- // Only emit changes if something actually changed
519
- if (!noEmitChanges && hasChanges) {
647
+ if (!noEmit && hasChanges) {
520
648
  this._emitTreeChanged();
521
649
  }
522
- return this; // Return the API object for chaining
650
+ return this;
523
651
  },
524
652
  // Private helper methods
525
653
  getNodeByPath: function (path, _root) {
@@ -1188,6 +1316,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
1188
1316
  node.isExpanded = (node.level ?? 0) <= _expandLevel;
1189
1317
  if (!shouldCalculateIsSelectable)
1190
1318
  node.isSelectable = getField(row, _isSelectableMember);
1319
+ if (!shouldCalculateIsSelected)
1320
+ node.isSelected = getField(row, _isSelectedMember);
1191
1321
  if (!shouldCalculateIsDraggable)
1192
1322
  node.isDraggable = getField(row, _isDraggableMember);
1193
1323
  if (!shouldCalculateIsCollapsible)
@@ -1,7 +1,6 @@
1
1
  import type { SearchOptions } from 'flexsearch';
2
2
  import type { LTreeNode, DropPosition } from './ltree-node.svelte.js';
3
3
  export type { LTreeNode, DropPosition } from './ltree-node.svelte.js';
4
- export type Tuple<T, U> = [T, U];
5
4
  export type DragDropMode = 'none' | 'self' | 'cross' | 'both';
6
5
  export type ToggleIconMode = 'rotate' | 'swap';
7
6
  export type ClickBehavior = 'select' | 'expand' | 'expand-and-focus';
@@ -87,10 +86,10 @@ export interface Ltree<T> {
87
86
  orderMember?: string | null | undefined;
88
87
  isSorted: boolean | null | undefined;
89
88
  sortCallback?: (items: LTreeNode<T>[]) => LTreeNode<T>[];
90
- indexingCompleteCallback?: () => void;
91
89
  filteredTree: LTreeNode<T>[] | null;
92
90
  isFiltered: boolean;
93
91
  isSelectableMember: string | null | undefined;
92
+ isSelectedMember: string | null | undefined;
94
93
  isDraggableMember: string | null | undefined;
95
94
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
96
95
  isDropAllowedMember: string | null | undefined;
@@ -118,12 +117,22 @@ export interface Ltree<T> {
118
117
  searchNodes(_searchText: string | null | undefined, _searchOptions?: SearchOptions): LTreeNode<T>[];
119
118
  createFilteredTree(targetPaths: string[]): void;
120
119
  clearFilter(): void;
121
- expandAll(nodePath?: string | null | undefined): void;
122
- collapseAll(nodePath?: string | null | undefined): void;
120
+ expandAll(nodePath?: string | string[] | null | undefined, options?: {
121
+ exclusive?: boolean;
122
+ noEmit?: boolean;
123
+ }): void;
124
+ collapseAll(nodePath?: string | string[] | null | undefined, options?: {
125
+ noEmit?: boolean;
126
+ }): void;
123
127
  insert(path: string, data: T, noEmitChanges?: boolean): void;
124
128
  getNodeByPath(path: string, _root?: LTreeNode<T> | null | undefined): LTreeNode<T> | null;
125
- expandNodes(path: string): Ltree<T>;
126
- collapseNodes(path: string): Ltree<T>;
129
+ expandNodes(path: string | string[], options?: {
130
+ exclusive?: boolean;
131
+ noEmit?: boolean;
132
+ }): Ltree<T>;
133
+ collapseNodes(path: string | string[], options?: {
134
+ noEmit?: boolean;
135
+ }): Ltree<T>;
127
136
  getNodeDisplayValue(node: LTreeNode<T>): string;
128
137
  getNodeSearchValue(node: LTreeNode<T>): string;
129
138
  _defaultSort(self: Ltree<T>, items: LTreeNode<T>[]): LTreeNode<T>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/svelte-treeview",
3
- "version": "5.0.0-rc06",
3
+ "version": "5.0.0-rc08",
4
4
  "scripts": {
5
5
  "dev": "vite dev --port 17777",
6
6
  "build": "vite build && npm run prepack",
@@ -13,6 +13,10 @@
13
13
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
14
14
  "test": "vitest",
15
15
  "test:run": "vitest run",
16
+ "test:e2e": "playwright test",
17
+ "test:e2e:ui": "playwright test --ui",
18
+ "test:e2e:headed": "playwright test --headed",
19
+ "test:e2e:install": "playwright install chromium",
16
20
  "format": "prettier --write .",
17
21
  "lint": "prettier --check . && eslint ."
18
22
  },
@@ -45,6 +49,7 @@
45
49
  "devDependencies": {
46
50
  "@eslint/compat": "^1.2.5",
47
51
  "@eslint/js": "^9.18.0",
52
+ "@playwright/test": "^1.59.1",
48
53
  "@sveltejs/adapter-auto": "^6.0.0",
49
54
  "@sveltejs/adapter-static": "^3.0.0",
50
55
  "@sveltejs/kit": "^2.22.0",