@keenmate/svelte-treeview 4.0.0-rc07 → 4.0.0

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
@@ -142,6 +142,71 @@ import '@keenmate/svelte-treeview/styles.scss';
142
142
  />
143
143
  ```
144
144
 
145
+ ### With Advanced Search Options
146
+
147
+ ```svelte
148
+ <script lang="ts">
149
+ import { Tree } from '@keenmate/svelte-treeview';
150
+ import type { SearchOptions } from 'flexsearch';
151
+
152
+ let treeRef;
153
+ const data = [/* your data */];
154
+
155
+ // Programmatic search with FlexSearch options
156
+ function performAdvancedSearch(searchTerm: string) {
157
+ const searchOptions: SearchOptions = {
158
+ suggest: true, // Enable suggestions for typos
159
+ limit: 10, // Limit results to 10 items
160
+ bool: "and" // Use AND logic for multiple terms
161
+ };
162
+
163
+ const results = treeRef.searchNodes(searchTerm, searchOptions);
164
+ console.log('Advanced search results:', results);
165
+ }
166
+
167
+ // Programmatic filtering with options
168
+ function filterWithOptions(searchTerm: string) {
169
+ const searchOptions: SearchOptions = {
170
+ threshold: 0.8, // Similarity threshold
171
+ depth: 2 // Search depth
172
+ };
173
+
174
+ treeRef.filterNodes(searchTerm, searchOptions);
175
+ }
176
+ </script>
177
+
178
+ <Tree
179
+ bind:this={treeRef}
180
+ {data}
181
+ idMember="path"
182
+ pathMember="path"
183
+ shouldUseInternalSearchIndex={true}
184
+ searchValueMember="name"
185
+ />
186
+
187
+ <button onclick={() => performAdvancedSearch('document')}>
188
+ Advanced Search
189
+ </button>
190
+ <button onclick={() => filterWithOptions('project')}>
191
+ Filter with Options
192
+ </button>
193
+ ```
194
+
195
+ #### FlexSearch Options Reference
196
+
197
+ The `searchOptions` parameter accepts any options supported by FlexSearch. Common options include:
198
+
199
+ | Option | Type | Description | Example |
200
+ |--------|------|-------------|---------|
201
+ | `suggest` | `boolean` | Enable suggestions for typos | `{ suggest: true }` |
202
+ | `limit` | `number` | Maximum number of results | `{ limit: 10 }` |
203
+ | `threshold` | `number` | Similarity threshold (0-1) | `{ threshold: 0.8 }` |
204
+ | `depth` | `number` | Search depth for nested content | `{ depth: 2 }` |
205
+ | `bool` | `string` | Boolean logic: "and", "or" | `{ bool: "and" }` |
206
+ | `where` | `object` | Filter by field values | `{ where: { type: "folder" } }` |
207
+
208
+ For complete FlexSearch documentation, visit: [FlexSearch Options](https://github.com/nextapps-de/flexsearch#options)
209
+
145
210
  ### With Drag & Drop
146
211
 
147
212
  ```svelte
@@ -183,6 +248,7 @@ import '@keenmate/svelte-treeview/styles.scss';
183
248
  data={targetData}
184
249
  idMember="path"
185
250
  pathMember="path"
251
+ dragOverNodeClass="ltree-dragover-highlight"
186
252
  onNodeDrop={onDrop}
187
253
  />
188
254
  </div>
@@ -199,7 +265,7 @@ The component uses CSS custom properties for easy theming:
199
265
 
200
266
  ```css
201
267
  :root {
202
- --tree-node-indent-per-level: 0.5rem;
268
+ --tree-node-indent-per-level: 0.5rem; /* Controls indentation for each hierarchy level */
203
269
  --ltree-primary: #0d6efd;
204
270
  --ltree-primary-rgb: 13, 110, 253;
205
271
  --ltree-success: #198754;
@@ -212,6 +278,8 @@ The component uses CSS custom properties for easy theming:
212
278
  }
213
279
  ```
214
280
 
281
+ **Note**: The `--tree-node-indent-per-level` variable controls the consistent indentation applied at each hierarchy level. Each nested level receives this fixed indent amount, creating proper visual hierarchy without exponential indentation growth.
282
+
215
283
  ### SCSS Variables (if using SCSS)
216
284
 
217
285
  If you're building the styles from SCSS source, you can override these variables:
@@ -232,6 +300,7 @@ $primary-color: #custom-color;
232
300
  - `.ltree-node-content` - Node content area
233
301
  - `.ltree-toggle-icon` - Expand/collapse icons
234
302
  - `.ltree-selected-*` - Selected node styles
303
+ - `.ltree-dragover-*` - Drag-over node styles
235
304
  - `.ltree-draggable` - Draggable nodes
236
305
  - `.ltree-context-menu` - Context menu styling
237
306
  - `.ltree-drag-over` - Applied during drag operations
@@ -258,6 +327,13 @@ The component includes several pre-built classes for styling selected nodes:
258
327
  | `ltree-selected-border` | Border and background highlight | Solid border with light background |
259
328
  | `ltree-selected-brackets` | Decorative brackets around text | ❯ **Node Text** ❮ |
260
329
 
330
+ **Available Drag-over Node Classes:**
331
+
332
+ | Class | Description | Visual Effect |
333
+ |-------|-------------|---------------|
334
+ | `ltree-dragover-highlight` | Dashed border with success color background | Green dashed border with subtle background |
335
+ | `ltree-dragover-glow` | Blue glow effect | Glowing shadow effect with primary color theme |
336
+
261
337
  ### Custom Icon Classes
262
338
 
263
339
  ```svelte
@@ -323,7 +399,9 @@ Without both requirements, no search indexing will occur.
323
399
  | Prop | Type | Default | Description |
324
400
  |------|------|---------|-------------|
325
401
  | `treeId` | `string \| null` | auto-generated | Unique identifier for the tree |
402
+ | `treePathSeparator` | `string \| null` | `"."` | Separator character for hierarchical paths (e.g., "." for "1.2.3" or "/" for "1/2/3") |
326
403
  | `selectedNode` | `LTreeNode<T>` (bindable) | `undefined` | Currently selected node |
404
+ | `insertResult` | `InsertArrayResult<T>` (bindable) | `undefined` | Result of the last data insertion including failed nodes |
327
405
 
328
406
  #### Behavior Properties
329
407
  | Prop | Type | Default | Description |
@@ -347,6 +425,7 @@ Without both requirements, no search indexing will occur.
347
425
  |------|------|---------|-------------|
348
426
  | `bodyClass` | `string \| null` | `undefined` | CSS class for tree body |
349
427
  | `selectedNodeClass` | `string \| null` | `undefined` | CSS class for selected nodes |
428
+ | `dragOverNodeClass` | `string \| null` | `undefined` | CSS class for nodes being dragged over |
350
429
  | `expandIconClass` | `string \| null` | `"ltree-icon-expand"` | CSS class for expand icons |
351
430
  | `collapseIconClass` | `string \| null` | `"ltree-icon-collapse"` | CSS class for collapse icons |
352
431
  | `leafIconClass` | `string \| null` | `"ltree-icon-leaf"` | CSS class for leaf node icons |
@@ -370,6 +449,8 @@ Without both requirements, no search indexing will occur.
370
449
  | `collapseNodes` | `nodePath: string` | Collapse nodes at specified path |
371
450
  | `expandAll` | `nodePath?: string` | Expand all nodes or nodes under path |
372
451
  | `collapseAll` | `nodePath?: string` | Collapse all nodes or nodes under path |
452
+ | `filterNodes` | `searchText: string, searchOptions?: SearchOptions` | Filter the tree display using internal search index with optional FlexSearch options |
453
+ | `searchNodes` | `searchText: string \| null \| undefined, searchOptions?: SearchOptions` | Search nodes using internal search index and return matching nodes with optional FlexSearch options |
373
454
  | `scrollToPath` | `path: string, options?: ScrollToPathOptions` | Scroll to and highlight a specific node |
374
455
 
375
456
  #### ScrollToPath Options
@@ -541,12 +622,74 @@ interface NodeData {
541
622
  - Second level: `"1.1"`, `"1.2"`, `"2.1"`
542
623
  - Third level: `"1.1.1"`, `"1.2.1"`, `"2.1.1"`
543
624
 
625
+ ### Insert Result Information
626
+
627
+ The tree provides detailed information about data insertion through the `insertResult` bindable property:
628
+
629
+ ```typescript
630
+ interface InsertArrayResult<T> {
631
+ successful: number; // Number of nodes successfully inserted
632
+ failed: Array<{ // Nodes that failed to insert
633
+ node: LTreeNode<T>; // The processed tree node
634
+ originalData: T; // The original data object
635
+ error: string; // Error message (usually "Could not find parent...")
636
+ }>;
637
+ total: number; // Total number of nodes processed
638
+ }
639
+ ```
640
+
641
+ #### Usage Example
642
+
643
+ ```svelte
644
+ <script lang="ts">
645
+ import { Tree } from '@keenmate/svelte-treeview';
646
+
647
+ let insertResult = $state();
648
+
649
+ const data = [
650
+ { id: '1', path: '1', name: 'Root' },
651
+ { id: '1.2', path: '1.2', name: 'Child' }, // Missing parent "1.1"
652
+ { id: '1.1.1', path: '1.1.1', name: 'Deep' } // Missing parent "1.1"
653
+ ];
654
+
655
+ // Check results after tree processes data
656
+ $effect(() => {
657
+ if (insertResult) {
658
+ console.log(`✅ ${insertResult.successful} nodes inserted successfully`);
659
+ console.log(`❌ ${insertResult.failed.length} nodes failed to insert`);
660
+
661
+ insertResult.failed.forEach(failure => {
662
+ console.log(`Failed: ${failure.originalData.name} - ${failure.error}`);
663
+ });
664
+ }
665
+ });
666
+ </script>
667
+
668
+ <Tree
669
+ {data}
670
+ idMember="id"
671
+ pathMember="path"
672
+ displayValueMember="name"
673
+ bind:insertResult
674
+ />
675
+ ```
676
+
677
+ #### Benefits
678
+
679
+ - **Data Validation**: Identify missing parent nodes in hierarchical data
680
+ - **Debugging**: Clear error messages with node paths like "Node: 1.1.1 - Could not find parent node: 1.1"
681
+ - **Data Integrity**: Handle incomplete datasets gracefully
682
+ - **Search Accuracy**: Failed nodes are excluded from search index, ensuring search results match visible tree
683
+ - **User Feedback**: Inform users about data issues with detailed failure information
684
+
544
685
  ## 🚀 Performance
545
686
 
546
687
  The component is optimized for large datasets:
547
688
 
548
689
  - **LTree**: Efficient hierarchical data structure
549
690
  - **Async Search Indexing**: Uses `requestIdleCallback` for non-blocking search index building
691
+ - **Accurate Search Results**: Search index only includes successfully inserted nodes, ensuring results match visible tree structure
692
+ - **Consistent Visual Hierarchy**: Optimized CSS-based indentation prevents exponential spacing growth
550
693
  - **Virtual Scrolling**: (Coming soon)
551
694
  - **Lazy Loading**: (Coming soon)
552
695
  - **Search Indexing**: Uses FlexSearch for fast search operations
@@ -22,13 +22,14 @@
22
22
  collapseIconClass?: string | null | undefined;
23
23
  leafIconClass?: string | null | undefined;
24
24
  selectedNodeClass?: string | null | undefined;
25
+ dragOverNodeClass?: string | null | undefined;
25
26
  isDraggedNode?: boolean | null | undefined;
26
27
  }
27
28
 
28
29
  // Destructure props using Svelte 5 syntax
29
30
  let {
30
31
  node,
31
- children,
32
+ children = undefined,
32
33
  onNodeClicked,
33
34
  onNodeRightClicked,
34
35
  onNodeDragStart,
@@ -43,16 +44,20 @@
43
44
  collapseIconClass = "ltree-icon-collapse",
44
45
  leafIconClass = "ltree-icon-leaf",
45
46
  selectedNodeClass,
47
+ dragOverNodeClass,
46
48
  isDraggedNode = false,
47
49
  }: Props = $props()
48
50
 
49
51
  const trie = getContext<Ltree<T>>("Ltree")
50
52
 
53
+ // Drag over state
54
+ let isDraggedOver = $state(false);
55
+
51
56
  // Convert reactive statements to derived values
52
57
  const childrenWithData = $derived(Object.values(node?.children || []))
53
58
  const hasChildren = $derived(node?.hasChildren || false)
54
59
  const indentStyle = $derived(
55
- `margin-left: calc(${node?.level || 0} * var(--tree-node-indent-per-level, 0.5rem))`,
60
+ `margin-left: var(--tree-node-indent-per-level, 0.5rem)`,
56
61
  )
57
62
 
58
63
  function toggleExpanded() {
@@ -71,9 +76,9 @@
71
76
  </script>
72
77
 
73
78
  <!-- svelte-ignore a11y_no_static_element_interactions -->
74
- <div
75
- class="ltree-node"
76
- id="{node.treeId}-{node.id}"
79
+ <div
80
+ class="ltree-node"
81
+ id="{node.treeId}-{node.id}"
77
82
  data-tree-path="{node.path}"
78
83
  style={indentStyle}
79
84
  >
@@ -96,7 +101,7 @@
96
101
  <!-- svelte-ignore a11y_click_events_have_key_events -->
97
102
  <!-- svelte-ignore a11y_no_static_element_interactions -->
98
103
  <div
99
- class="ltree-node-content {node.isSelected ? selectedNodeClass : ''}"
104
+ class="ltree-node-content {node.isSelected ? selectedNodeClass : ''} {isDraggedOver && dragOverNodeClass ? dragOverNodeClass : ''}"
100
105
  class:ltree-clickable={node.isSelectable}
101
106
  class:ltree-dragged={isDraggedNode}
102
107
  class:ltree-draggable={node?.isDraggable}
@@ -125,12 +130,25 @@
125
130
  }
126
131
  }}
127
132
  ondragover={(e) => {
128
- if (e.dataTransfer?.types.includes("application/svelte-treeview"))
133
+ if (e.dataTransfer?.types.includes("application/svelte-treeview")) {
129
134
  e.preventDefault();
135
+ isDraggedOver = true;
136
+ }
130
137
  onNodeDragOver?.(node, e);
131
138
  }}
139
+ ondragleave={(e) => {
140
+ // Only reset if we're actually leaving the node (not entering a child)
141
+ const rect = e.currentTarget.getBoundingClientRect();
142
+ const x = e.clientX;
143
+ const y = e.clientY;
144
+
145
+ if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
146
+ isDraggedOver = false;
147
+ }
148
+ }}
132
149
  ondrop={(e) => {
133
150
  e.stopPropagation();
151
+ isDraggedOver = false;
134
152
  onNodeDrop?.(node, e);
135
153
  }}
136
154
  >
@@ -158,6 +176,7 @@
158
176
  {collapseIconClass}
159
177
  {leafIconClass}
160
178
  {selectedNodeClass}
179
+ {dragOverNodeClass}
161
180
  {isDraggedNode}
162
181
  />
163
182
  {/each}
@@ -15,6 +15,7 @@ declare function $$render<T>(): {
15
15
  collapseIconClass?: string | null | undefined;
16
16
  leafIconClass?: string | null | undefined;
17
17
  selectedNodeClass?: string | null | undefined;
18
+ dragOverNodeClass?: string | null | undefined;
18
19
  isDraggedNode?: boolean | null | undefined;
19
20
  };
20
21
  exports: {};
@@ -1,9 +1,9 @@
1
1
  <script lang="ts" generics="T">
2
- import type { Index } from 'flexsearch';
2
+ import type { Index, SearchOptions } from 'flexsearch';
3
3
  import Node from './Node.svelte';
4
4
  import { type LTreeNode } from '../ltree/ltree-node.svelte.js';
5
5
  import { createLTree } from '../ltree/ltree.svelte.js';
6
- import { type Ltree } from '../ltree/types.js';
6
+ import { type Ltree, type InsertArrayResult } from '../ltree/types.js';
7
7
  import { setContext, tick } from 'svelte';
8
8
 
9
9
  // Context menu state
@@ -37,11 +37,13 @@
37
37
  getSearchValueCallback?: (node: LTreeNode<T>) => string;
38
38
 
39
39
  treeId?: string | null | undefined;
40
+ treePathSeparator?: string | null | undefined;
40
41
  sortCallback?: (items: LTreeNode<T>[]) => LTreeNode<T>[];
41
42
 
42
43
  // DATA
43
44
  data: T[];
44
45
  selectedNode?: LTreeNode<T> | null | undefined;
46
+ insertResult?: InsertArrayResult<T> | null | undefined;
45
47
 
46
48
  // SLOTS
47
49
  nodeTemplate?: any;
@@ -70,6 +72,7 @@
70
72
  // VISUALS
71
73
  bodyClass?: string | null | undefined;
72
74
  selectedNodeClass?: string | null | undefined;
75
+ dragOverNodeClass?: string | null | undefined;
73
76
  expandIconClass?: string | null | undefined;
74
77
  collapseIconClass?: string | null | undefined;
75
78
  leafIconClass?: string | null | undefined;
@@ -79,6 +82,7 @@
79
82
 
80
83
  let {
81
84
  treeId,
85
+ treePathSeparator,
82
86
 
83
87
  // MAPPINGS
84
88
  idMember,
@@ -102,13 +106,14 @@
102
106
  // DATA
103
107
  data = $bindable(),
104
108
  selectedNode = $bindable(),
109
+ insertResult = $bindable(),
105
110
 
106
111
  // SLOTS
107
- nodeTemplate,
108
- treeHeader,
109
- treeFooter,
110
- noDataFound,
111
- contextMenu,
112
+ nodeTemplate = undefined,
113
+ treeHeader = undefined,
114
+ treeFooter = undefined,
115
+ noDataFound = undefined,
116
+ contextMenu = undefined,
112
117
 
113
118
  // BEHAVIOUR
114
119
  expandLevel = 2,
@@ -133,21 +138,13 @@
133
138
  collapseIconClass = 'ltree-icon-collapse',
134
139
  leafIconClass = 'ltree-icon-leaf',
135
140
  selectedNodeClass,
141
+ dragOverNodeClass,
136
142
  scrollHighlightTimeout = 4000,
137
143
  scrollHighlightClass = 'ltree-scroll-highlight'
138
144
  }: Props = $props();
139
145
 
140
146
  export async function expandNodes(nodePath: string) {
141
147
  tree.expandNodes(nodePath);
142
-
143
- // trie.dummyText = Date.now().toLocaleString();
144
- // console.log(trie.dummyText);
145
- // rootNodes.forEach((element) => {
146
- // if (element.path === nodePath) {
147
- // element.isExpanded = !element.isExpanded;
148
- // console.log(element)
149
- // }
150
- // });
151
148
  }
152
149
 
153
150
  export async function collapseNodes(nodePath: string) {
@@ -162,6 +159,17 @@
162
159
  tree?.collapseAll(nodePath);
163
160
  }
164
161
 
162
+ export function filterNodes(searchText: string, searchOptions?: SearchOptions): void {
163
+ tree?.filterNodes(searchText, searchOptions);
164
+ }
165
+
166
+ export function searchNodes(
167
+ searchText: string | null | undefined,
168
+ searchOptions?: SearchOptions
169
+ ): LTreeNode<T>[] {
170
+ return tree?.searchNodes(searchText, searchOptions) || [];
171
+ }
172
+
165
173
  export async function scrollToPath(
166
174
  path: string,
167
175
  options?: { expand?: boolean; highlight?: boolean; scrollOptions?: ScrollIntoViewOptions }
@@ -204,17 +212,7 @@
204
212
  // Highlight the node temporarily if requested
205
213
  if (highlight && scrollHighlightClass) {
206
214
  contentDiv.classList.add(scrollHighlightClass);
207
- console.log(
208
- '🚀 elementId ~ scrollToPath ~ adding scrollHighlightClass:',
209
- elementId,
210
- scrollHighlightClass
211
- );
212
215
  setTimeout(() => {
213
- console.log(
214
- '🚀 elementId ~ scrollToPath ~ removing scrollHighlightClass:',
215
- elementId,
216
- scrollHighlightClass
217
- );
218
216
  contentDiv.classList.remove(scrollHighlightClass);
219
217
  }, scrollHighlightTimeout);
220
218
  }
@@ -244,6 +242,7 @@
244
242
  searchValueMember,
245
243
  getSearchValueCallback,
246
244
  treeId,
245
+ treePathSeparator,
247
246
 
248
247
  expandLevel,
249
248
 
@@ -265,7 +264,9 @@
265
264
  });
266
265
 
267
266
  $effect(() => {
268
- tree?.insertArray(data);
267
+ if (tree && data) {
268
+ insertResult = tree.insertArray(data);
269
+ }
269
270
  });
270
271
 
271
272
  // $inspect("trie change tracker", trie?.changeTracker?.toString());
@@ -428,9 +429,7 @@
428
429
  </div>
429
430
  {/if}
430
431
 
431
- {#if treeHeader}
432
- {@render treeHeader?.()}
433
- {/if}
432
+ {@render treeHeader?.()}
434
433
  <div class:bodyClass>
435
434
  {#if tree?.root}
436
435
  {#key tree.changeTracker}
@@ -449,6 +448,7 @@
449
448
  {collapseIconClass}
450
449
  {leafIconClass}
451
450
  {selectedNodeClass}
451
+ {dragOverNodeClass}
452
452
  isDraggedNode={draggedNode === node}
453
453
  />
454
454
  {:else}
@@ -464,9 +464,8 @@
464
464
  </div>
465
465
  {/if}
466
466
  </div>
467
- {#if treeFooter}
467
+
468
468
  {@render treeFooter?.()}
469
- {/if}
470
469
 
471
470
  <!-- Context Menu -->
472
471
  {#if contextMenuVisible && contextMenu && contextMenuNode}
@@ -1,5 +1,6 @@
1
- import type { Index } from 'flexsearch';
1
+ import type { Index, SearchOptions } from 'flexsearch';
2
2
  import { type LTreeNode } from '../ltree/ltree-node.svelte.js';
3
+ import { type InsertArrayResult } from '../ltree/types.js';
3
4
  declare function $$render<T>(): {
4
5
  props: {
5
6
  trieId?: string | null | undefined;
@@ -18,9 +19,11 @@ declare function $$render<T>(): {
18
19
  searchValueMember?: string | null | undefined;
19
20
  getSearchValueCallback?: (node: LTreeNode<T>) => string;
20
21
  treeId?: string | null | undefined;
22
+ treePathSeparator?: string | null | undefined;
21
23
  sortCallback?: (items: LTreeNode<T>[]) => LTreeNode<T>[];
22
24
  data: T[];
23
25
  selectedNode?: LTreeNode<T> | null | undefined;
26
+ insertResult?: InsertArrayResult<T> | null | undefined;
24
27
  nodeTemplate?: any;
25
28
  treeHeader?: any;
26
29
  treeBody?: any;
@@ -41,6 +44,7 @@ declare function $$render<T>(): {
41
44
  onNodeDrop?: (node: LTreeNode<T>, draggedNode: LTreeNode<T>, event: DragEvent) => void;
42
45
  bodyClass?: string | null | undefined;
43
46
  selectedNodeClass?: string | null | undefined;
47
+ dragOverNodeClass?: string | null | undefined;
44
48
  expandIconClass?: string | null | undefined;
45
49
  collapseIconClass?: string | null | undefined;
46
50
  leafIconClass?: string | null | undefined;
@@ -52,13 +56,15 @@ declare function $$render<T>(): {
52
56
  collapseNodes: (nodePath: string) => Promise<void>;
53
57
  expandAll: (nodePath?: string | null | undefined) => void;
54
58
  collapseAll: (nodePath?: string | null | undefined) => void;
59
+ filterNodes: (searchText: string, searchOptions?: SearchOptions) => void;
60
+ searchNodes: (searchText: string | null | undefined, searchOptions?: SearchOptions) => LTreeNode<T>[];
55
61
  scrollToPath: (path: string, options?: {
56
62
  expand?: boolean;
57
63
  highlight?: boolean;
58
64
  scrollOptions?: ScrollIntoViewOptions;
59
65
  }) => Promise<boolean>;
60
66
  };
61
- bindings: "data" | "selectedNode" | "searchText";
67
+ bindings: "data" | "selectedNode" | "insertResult" | "searchText";
62
68
  slots: {};
63
69
  events: {};
64
70
  };
@@ -66,12 +72,14 @@ declare class __sveltets_Render<T> {
66
72
  props(): ReturnType<typeof $$render<T>>['props'];
67
73
  events(): ReturnType<typeof $$render<T>>['events'];
68
74
  slots(): ReturnType<typeof $$render<T>>['slots'];
69
- bindings(): "data" | "selectedNode" | "searchText";
75
+ bindings(): "data" | "selectedNode" | "insertResult" | "searchText";
70
76
  exports(): {
71
77
  expandNodes: (nodePath: string) => Promise<void>;
72
78
  collapseNodes: (nodePath: string) => Promise<void>;
73
79
  expandAll: (nodePath?: string | null | undefined) => void;
74
80
  collapseAll: (nodePath?: string | null | undefined) => void;
81
+ filterNodes: (searchText: string, searchOptions?: SearchOptions) => void;
82
+ searchNodes: (searchText: string | null | undefined, searchOptions?: SearchOptions) => LTreeNode<T>[];
75
83
  scrollToPath: (path: string, options?: {
76
84
  expand?: boolean;
77
85
  highlight?: boolean;
@@ -78,7 +78,7 @@ export function performanceTest() {
78
78
  console.time('Prefix search for "1."');
79
79
  const results = trie.findByPrefix('1.');
80
80
  console.timeEnd('Prefix search for "1."');
81
- console.log(`Found ${results.length} paths with prefix "1."`);
81
+ console.log(`Found ${results?.length} paths with prefix "1."`);
82
82
  // Memory usage
83
83
  const stats = trie.getStats();
84
84
  console.log('Final statistics:', stats);
@@ -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, _isDropAllowedMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _treeId?: string, _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, _isDraggableMember?: string | null | undefined, _isDropAllowedMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _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,7 +4,7 @@ import { isEmptyString } from '../helpers/string-helpers.js';
4
4
  import { getLevel, getParentPath, getPathSegments, getRelativePath } from '../helpers/ltree-helpers.js';
5
5
  import { createSearchIndex } from './flex.js';
6
6
  import { Indexer } from './indexer.js';
7
- export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _isDropAllowedMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _treeId, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
7
+ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _isSelectableMember, _isDraggableMember, _isDropAllowedMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
8
8
  let shouldCalculateParentPath = isEmptyString(_parentPathMember);
9
9
  let shouldCalculateLevel = isEmptyString(_levelMember);
10
10
  let shouldCalculateHasChildren = isEmptyString(_hasChildrenMember);
@@ -40,7 +40,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
40
40
  }
41
41
  return {
42
42
  // Properties
43
- treePathSeparator: '.',
43
+ treePathSeparator: _treePathSeparator || '.',
44
44
  root,
45
45
  get changeTracker() {
46
46
  return changeTracker;
@@ -87,6 +87,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
87
87
  data = data || [];
88
88
  // Clear any pending indexing from previous calls
89
89
  indexer?.clearQueue();
90
+ flatTreeNodes = [];
90
91
  performance.mark('conversion-start');
91
92
  let mappedData = data.map((row, index) => {
92
93
  const node = createLTreeNode();
@@ -132,22 +133,34 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
132
133
  console.log(`[Tree ${_treeId}] Mapped data after sort`, mappedData);
133
134
  performance.mark('sort-end');
134
135
  performance.mark('insert-start');
135
- const errors = [];
136
+ const failedNodes = [];
136
137
  const itemsToIndex = [];
138
+ let realIndex = 0; // this is used to avoid scenario, when node cannot found a parent
139
+ let successfulCount = 0;
137
140
  mappedData.forEach((node, index) => {
138
141
  const result = this.insertTreeNode(node.parentPath, node, true);
139
142
  if (result) {
140
- errors.push(result);
143
+ failedNodes.push({
144
+ node: node,
145
+ originalData: data[index],
146
+ error: result
147
+ });
141
148
  }
142
149
  else {
150
+ successfulCount++;
143
151
  // Collect items for batch indexing
144
152
  if (_shouldUseInternalSearchIndex && indexer) {
145
- itemsToIndex.push({ node, index });
153
+ flatTreeNodes.push(node);
154
+ itemsToIndex.push({ node, index: realIndex });
155
+ realIndex++;
146
156
  }
147
157
  }
148
158
  });
149
- if (errors.length > 0)
150
- console.warn(`[Tree ${_treeId}]`, errors);
159
+ // Log errors for backward compatibility and debugging
160
+ if (failedNodes.length > 0) {
161
+ const errorMessages = failedNodes.map((f) => f.error);
162
+ console.warn(`[Tree ${_treeId}] ${failedNodes.length} nodes failed to insert:`, errorMessages);
163
+ }
151
164
  // Batch add items to indexer
152
165
  if (itemsToIndex.length > 0 && indexer) {
153
166
  indexer.setCallbacks(undefined, // no progress callback for now
@@ -172,11 +185,16 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
172
185
  console.log(`[Tree ${_treeId}] Conversion took: ${measure.duration}ms`);
173
186
  measure = performance.getEntriesByName('insert-duration')[0];
174
187
  console.log(`[Tree ${_treeId}] Insert took: ${measure.duration}ms`);
188
+ return {
189
+ successful: successfulCount,
190
+ failed: failedNodes,
191
+ total: data.length
192
+ };
175
193
  },
176
194
  insertTreeNode: function (parentPath, newNode, noEmitChanges) {
177
195
  const parentNode = this.getNodeByPath(parentPath);
178
196
  if (!parentNode) {
179
- return `Could not find node for parent path: ${parentPath}`;
197
+ return `Node: ${newNode.path} - Could not find parent node: ${parentPath}`;
180
198
  }
181
199
  if (shouldCalculateLevel) {
182
200
  newNode.level = (parentNode.level || 0) + 1;
@@ -191,13 +209,12 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
191
209
  nodeCount++;
192
210
  maxLevel = Math.max(maxLevel, newNode.level || 0);
193
211
  }
194
- flatTreeNodes.push(newNode);
195
212
  if (!noEmitChanges) {
196
213
  this._emitTreeChanged();
197
214
  }
198
215
  return null;
199
216
  },
200
- filterNodes(_searchText) {
217
+ filterNodes(_searchText, _searchOptions) {
201
218
  if (this.shouldDisplayDebugInformation)
202
219
  console.log(`[Tree ${_treeId}] Filtering nodes by:`, _searchText);
203
220
  if (isEmptyString(_searchText)) {
@@ -214,10 +231,29 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
214
231
  console.warn(`[Tree ${_treeId}] Internal search index is disabled`);
215
232
  return;
216
233
  }
217
- const resultIndices = searchIndex.search(_searchText);
234
+ const resultIndices = searchIndex.search(_searchText, _searchOptions);
218
235
  const foundPaths = resultIndices.map((row) => flatTreeNodes[row].path);
236
+ if (this.shouldDisplayDebugInformation)
237
+ console.warn(`[Tree ${_treeId}] Found indices:`, resultIndices, foundPaths);
219
238
  this.createFilteredTree(foundPaths);
220
239
  },
240
+ searchNodes(_searchText, _searchOptions) {
241
+ if (this.shouldDisplayDebugInformation)
242
+ console.log(`[Tree ${_treeId}] Searching nodes by:`, _searchText);
243
+ if (isEmptyString(_searchText)) {
244
+ return [];
245
+ }
246
+ if (!_shouldUseInternalSearchIndex) {
247
+ if (this.shouldDisplayDebugInformation)
248
+ console.warn(`[Tree ${_treeId}] Internal search index is disabled`);
249
+ return [];
250
+ }
251
+ const resultIndices = searchIndex.search(_searchText, _searchOptions);
252
+ const foundNodes = resultIndices.map((row) => flatTreeNodes[row]);
253
+ if (this.shouldDisplayDebugInformation)
254
+ console.warn(`[Tree ${_treeId}] Found indices:`, resultIndices, foundNodes);
255
+ return foundNodes;
256
+ },
221
257
  createFilteredTree(targetPaths) {
222
258
  filteredRoot.children = {};
223
259
  filteredTree = null;
@@ -1,5 +1,15 @@
1
+ import type { SearchOptions } from 'flexsearch';
1
2
  import type { LTreeNode } from './ltree-node.svelte';
2
3
  export type Tuple<T, U> = [T, U];
4
+ export interface InsertArrayResult<T> {
5
+ successful: number;
6
+ failed: Array<{
7
+ node: LTreeNode<T>;
8
+ originalData: T;
9
+ error: string;
10
+ }>;
11
+ total: number;
12
+ }
3
13
  export interface Ltree<T> {
4
14
  treePathSeparator: string;
5
15
  root: LTreeNode<T>;
@@ -29,9 +39,10 @@ export interface Ltree<T> {
29
39
  nodeCount: number;
30
40
  maxLevel: number;
31
41
  };
32
- insertArray(data: T[]): void;
42
+ insertArray(data: T[]): InsertArrayResult<T>;
33
43
  insertTreeNode(parentPath: string, newNode: LTreeNode<T>, noEmitChanges?: boolean): string | null;
34
- filterNodes(_searchText: string): void;
44
+ filterNodes(_searchText: string, _searchOptions?: SearchOptions): void;
45
+ searchNodes(_searchText: string | null | undefined, _searchOptions?: SearchOptions): LTreeNode<T>[];
35
46
  createFilteredTree(targetPaths: string[]): void;
36
47
  clearFilter(): void;
37
48
  expandAll(nodePath?: string | null | undefined): void;
@@ -190,6 +190,35 @@ $body-color: #212529 !default;
190
190
  }
191
191
  }
192
192
 
193
+ // Drag-over Node Styles
194
+ .ltree-dragover-highlight {
195
+ background-color: rgba(var(--ltree-success-rgb), 0.15) !important;
196
+ border: 2px dashed var(--ltree-success) !important;
197
+ border-radius: 4px !important;
198
+ }
199
+
200
+ .ltree-dragover-glow {
201
+ background-color: rgba(var(--ltree-primary-rgb), 0.1) !important;
202
+ box-shadow: 0 0 8px rgba(var(--ltree-primary-rgb), 0.4) !important;
203
+ border-radius: 4px !important;
204
+ }
205
+
206
+ @keyframes bounce {
207
+ 0%,
208
+ 20%,
209
+ 50%,
210
+ 80%,
211
+ 100% {
212
+ transform: translateY(-50%);
213
+ }
214
+ 40% {
215
+ transform: translateY(-60%);
216
+ }
217
+ 60% {
218
+ transform: translateY(-40%);
219
+ }
220
+ }
221
+
193
222
  // Drag and Drop Styles
194
223
  .ltree-draggable {
195
224
  cursor: grab;
@@ -339,12 +368,24 @@ $body-color: #212529 !default;
339
368
 
340
369
  .ltree-scroll-highlight-arrow {
341
370
  position: relative;
371
+ &::before {
372
+ content: '⇨'; /* arrow pointing right */
373
+ position: absolute;
374
+ left: -1.2em; /* place outside row */
375
+ top: 50%; /* vertical center */
376
+ transform: translateY(-57%);
377
+ font-size: 3em;
378
+ pointer-events: none; /* arrow doesn’t block hover */
379
+ color: var(--ltree-danger);
380
+
381
+ z-index: 1;
382
+ }
342
383
  &::after {
343
384
  content: '⇦'; /* arrow pointing right */
344
385
  position: absolute;
345
386
  right: 0em; /* place outside row */
346
387
  top: 50%; /* vertical center */
347
- transform: translateY(-50%);
388
+ transform: translateY(-57%);
348
389
  font-size: 3em;
349
390
  pointer-events: none; /* arrow doesn’t block hover */
350
391
  color: var(--ltree-danger);
package/dist/styles.css CHANGED
@@ -138,6 +138,29 @@
138
138
  font-weight: bold;
139
139
  }
140
140
 
141
+ .ltree-dragover-highlight {
142
+ background-color: rgba(var(--ltree-success-rgb), 0.15) !important;
143
+ border: 2px dashed var(--ltree-success) !important;
144
+ border-radius: 4px !important;
145
+ }
146
+
147
+ .ltree-dragover-glow {
148
+ background-color: rgba(var(--ltree-primary-rgb), 0.1) !important;
149
+ box-shadow: 0 0 8px rgba(var(--ltree-primary-rgb), 0.4) !important;
150
+ border-radius: 4px !important;
151
+ }
152
+
153
+ @keyframes bounce {
154
+ 0%, 20%, 50%, 80%, 100% {
155
+ transform: translateY(-50%);
156
+ }
157
+ 40% {
158
+ transform: translateY(-60%);
159
+ }
160
+ 60% {
161
+ transform: translateY(-40%);
162
+ }
163
+ }
141
164
  .ltree-draggable {
142
165
  cursor: grab;
143
166
  }
@@ -263,12 +286,23 @@
263
286
  .ltree-scroll-highlight-arrow {
264
287
  position: relative;
265
288
  }
289
+ .ltree-scroll-highlight-arrow::before {
290
+ content: "⇨"; /* arrow pointing right */
291
+ position: absolute;
292
+ left: -1.2em; /* place outside row */
293
+ top: 50%; /* vertical center */
294
+ transform: translateY(-57%);
295
+ font-size: 3em;
296
+ pointer-events: none; /* arrow doesn’t block hover */
297
+ color: var(--ltree-danger);
298
+ z-index: 1;
299
+ }
266
300
  .ltree-scroll-highlight-arrow::after {
267
301
  content: "⇦"; /* arrow pointing right */
268
302
  position: absolute;
269
303
  right: 0em; /* place outside row */
270
304
  top: 50%; /* vertical center */
271
- transform: translateY(-50%);
305
+ transform: translateY(-57%);
272
306
  font-size: 3em;
273
307
  pointer-events: none; /* arrow doesn’t block hover */
274
308
  color: var(--ltree-danger);
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../src/lib/styles/main.scss"],"names":[],"mappings":";AAmCA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAID;EACC,aA5CA;;AA+CA;EACC;EACA,OAlCqB;EAmCrB;EACA;;;AAIF;EACC,aAxDA;EAyDA,WAvDqB;;AAyDrB;EACC;EACA;;AAGD;EACC;EACA;EACA,SAhE0B;EAiE1B,eAhEgC;EAiEhC;EACA;EACA;;AAEA;EACC,kBArEkB;;AAyEpB;EACC;;AAGD;EACC,OA7EuB;EA8EvB;EACA;EACA,cA7E8B;EA8E9B,WAhF2B;EAiF3B,OAhFuB;EAiFvB;EACA;;AAEA;EACC;;AAIF;EACC,cAxF4B;EAyF5B,WAxFyB;;AA2F1B;EACC,aA3F4B;EA4F5B,cA3F6B;;AA8F9B;EACC,WA9FyB;EA+FzB,OA9FqB;;AAiGtB;EACC,YAjGyB;;;AAsG3B;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAID;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAID;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;;AAEA;EACC;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;;;AAKF;EACC;;AAEA;EACC;;;AAIF;EACC;EACA;EACA,YACC;;;AAKD;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACC;EACA;;AAGD;EACC;EACA;;;AAKF;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;;AAGD;EACC;;AAEA;EACC;;AAKH;EACC;EACA;EACA;;;AAKF;EACC,aA3RA;EA4RA;EACA;EACA;EACA;EACA;EACA;;AAGC;EACC;EACA;EACA;EACA;;AAEA;EACC;;AAIF;EACC;EACA;;AAIF;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAMH;EACC;EACA;EACA;;;AAGD;EACC;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;;;AAKF;EAEC;EACA;EACA;EAGA","file":"styles.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../src/lib/styles/main.scss"],"names":[],"mappings":";AAmCA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAID;EACC,aA5CA;;AA+CA;EACC;EACA,OAlCqB;EAmCrB;EACA;;;AAIF;EACC,aAxDA;EAyDA,WAvDqB;;AAyDrB;EACC;EACA;;AAGD;EACC;EACA;EACA,SAhE0B;EAiE1B,eAhEgC;EAiEhC;EACA;EACA;;AAEA;EACC,kBArEkB;;AAyEpB;EACC;;AAGD;EACC,OA7EuB;EA8EvB;EACA;EACA,cA7E8B;EA8E9B,WAhF2B;EAiF3B,OAhFuB;EAiFvB;EACA;;AAEA;EACC;;AAIF;EACC,cAxF4B;EAyF5B,WAxFyB;;AA2F1B;EACC,aA3F4B;EA4F5B,cA3F6B;;AA8F9B;EACC,WA9FyB;EA+FzB,OA9FqB;;AAiGtB;EACC,YAjGyB;;;AAsG3B;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAID;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAID;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;;AAEA;EACC;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;;;AAKF;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;IAKC;;EAED;IACC;;EAED;IACC;;;AAKF;EACC;;AAEA;EACC;;;AAIF;EACC;EACA;EACA,YACC;;;AAKD;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACC;EACA;;AAGD;EACC;EACA;;;AAKF;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;;AAGD;EACC;;AAEA;EACC;;AAKH;EACC;EACA;EACA;;;AAKF;EACC,aAxTA;EAyTA;EACA;EACA;EACA;EACA;EACA;;AAGC;EACC;EACA;EACA;EACA;;AAEA;EACC;;AAIF;EACC;EACA;;AAIF;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAMH;EACC;EACA;EACA;;;AAGD;EACC;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;;;AAKF;EAEC;EACA;EACA;EAGA","file":"styles.css"}
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@keenmate/svelte-treeview",
3
- "version": "4.0.0-rc07",
3
+ "version": "4.0.0",
4
4
  "scripts": {
5
- "dev": "vite dev --port 7777",
5
+ "dev": "vite dev --port 17777",
6
6
  "build": "vite build && npm run prepack",
7
+ "build:showcase": "vite build",
7
8
  "preview": "vite preview",
8
9
  "prepare": "svelte-kit sync || echo ''",
9
10
  "prepack": "svelte-kit sync && svelte-package && sass src/lib/styles.scss dist/styles.css && publint",
@@ -40,10 +41,11 @@
40
41
  "@eslint/compat": "^1.2.5",
41
42
  "@eslint/js": "^9.18.0",
42
43
  "@sveltejs/adapter-auto": "^6.0.0",
44
+ "@sveltejs/adapter-static": "^3.0.0",
43
45
  "@sveltejs/kit": "^2.22.0",
44
46
  "@sveltejs/package": "^2.0.0",
45
47
  "@sveltejs/vite-plugin-svelte": "^6.0.0",
46
- "@types/flexsearch": "^0.7.6",
48
+ "@types/flexsearch": "^0.7.42",
47
49
  "eslint": "^9.18.0",
48
50
  "eslint-config-prettier": "^10.0.1",
49
51
  "eslint-plugin-svelte": "^3.0.0",
@@ -60,7 +62,7 @@
60
62
  "vite": "^7.0.4"
61
63
  },
62
64
  "optionalDependencies": {
63
- "flexsearch": "^0.8.205"
65
+ "flexsearch": "^0.8.212"
64
66
  },
65
67
  "keywords": [
66
68
  "svelte",