@keenmate/svelte-treeview 4.8.0 → 5.0.0-rc02
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 +106 -117
- package/ai/INDEX.txt +310 -0
- package/ai/advanced-patterns.txt +506 -0
- package/ai/basic-setup.txt +336 -0
- package/ai/context-menu.txt +349 -0
- package/ai/data-handling.txt +390 -0
- package/ai/drag-drop.txt +397 -0
- package/ai/events-callbacks.txt +382 -0
- package/ai/import-patterns.txt +271 -0
- package/ai/performance.txt +349 -0
- package/ai/search-features.txt +359 -0
- package/ai/styling-theming.txt +354 -0
- package/ai/tree-editing.txt +423 -0
- package/ai/typescript-types.txt +357 -0
- package/dist/components/Node.svelte +47 -40
- package/dist/components/Node.svelte.d.ts +1 -1
- package/dist/components/Tree.svelte +384 -1479
- package/dist/components/Tree.svelte.d.ts +30 -28
- package/dist/components/TreeProvider.svelte +28 -0
- package/dist/components/TreeProvider.svelte.d.ts +28 -0
- package/dist/constants.generated.d.ts +1 -1
- package/dist/constants.generated.js +1 -1
- package/dist/core/TreeController.svelte.d.ts +353 -0
- package/dist/core/TreeController.svelte.js +1503 -0
- package/dist/core/createTreeController.d.ts +9 -0
- package/dist/core/createTreeController.js +11 -0
- package/dist/global-api.d.ts +1 -1
- package/dist/global-api.js +5 -5
- package/dist/index.d.ts +10 -6
- package/dist/index.js +7 -3
- package/dist/logger.d.ts +7 -6
- package/dist/logger.js +0 -2
- package/dist/ltree/indexer.js +2 -4
- package/dist/ltree/ltree-node.svelte.d.ts +2 -1
- package/dist/ltree/ltree-node.svelte.js +1 -0
- package/dist/ltree/ltree.svelte.d.ts +1 -1
- package/dist/ltree/ltree.svelte.js +168 -175
- package/dist/ltree/types.d.ts +12 -8
- package/dist/perf-logger.d.ts +2 -1
- package/dist/perf-logger.js +0 -2
- package/dist/styles/main.scss +78 -78
- package/dist/styles.css +41 -41
- package/dist/styles.css.map +1 -1
- package/dist/vendor/loglevel/index.d.ts +55 -2
- package/dist/vendor/loglevel/prefix.d.ts +23 -2
- package/package.json +96 -95
- package/dist/ltree/ltree-demo.d.ts +0 -2
- package/dist/ltree/ltree-demo.js +0 -90
- package/dist/vendor/loglevel/loglevel-esm.d.ts +0 -2
- package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +0 -7
- package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +0 -2
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
PERFORMANCE
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
CRITICAL: Use $state.raw() for large datasets
|
|
5
|
+
- Svelte 5's $state() creates deep proxies
|
|
6
|
+
- With 1000+ items, this causes massive slowdown
|
|
7
|
+
- $state.raw() avoids proxy overhead
|
|
8
|
+
|
|
9
|
+
$STATE.RAW() REQUIREMENT
|
|
10
|
+
------------------------
|
|
11
|
+
❌ SLOW - 5000x slower with large datasets:
|
|
12
|
+
let treeData = $state<TreeNode[]>([]);
|
|
13
|
+
|
|
14
|
+
✅ FAST - Items remain plain objects:
|
|
15
|
+
let treeData = $state.raw<TreeNode[]>([]);
|
|
16
|
+
|
|
17
|
+
Why it matters:
|
|
18
|
+
- $state() creates Proxy for every nested object
|
|
19
|
+
- Tree's insertArray() accesses multiple properties per item
|
|
20
|
+
- 8000 items × 10 properties = 80,000 proxy operations
|
|
21
|
+
- Proxy overhead: ~2.2ms per item vs ~0.0004ms plain
|
|
22
|
+
|
|
23
|
+
Symptoms of wrong usage:
|
|
24
|
+
- Tree takes 15-90+ seconds to render
|
|
25
|
+
- Console shows "[Violation] 'message' handler took XXXXms"
|
|
26
|
+
- Same data loads instantly in isolated test
|
|
27
|
+
|
|
28
|
+
The fix does NOT affect reactivity:
|
|
29
|
+
- Array changes still trigger updates
|
|
30
|
+
- Only individual items lose deep reactivity
|
|
31
|
+
- Tree doesn't need deep item reactivity
|
|
32
|
+
|
|
33
|
+
FLAT RENDERING MODE
|
|
34
|
+
-------------------
|
|
35
|
+
Default mode (v4.6+) - significantly faster:
|
|
36
|
+
|
|
37
|
+
<Tree useFlatRendering={true} /> <!-- Default -->
|
|
38
|
+
|
|
39
|
+
Flat mode benefits:
|
|
40
|
+
- Single {#each} loop instead of recursive components
|
|
41
|
+
- ~12x faster initial render (300ms → 25ms for 5500 nodes)
|
|
42
|
+
- Better memory usage
|
|
43
|
+
|
|
44
|
+
Legacy recursive mode:
|
|
45
|
+
<Tree useFlatRendering={false} />
|
|
46
|
+
|
|
47
|
+
Use recursive mode only if:
|
|
48
|
+
- Very small trees (<100 nodes)
|
|
49
|
+
- Need {#key changeTracker} behavior
|
|
50
|
+
|
|
51
|
+
PROGRESSIVE RENDERING
|
|
52
|
+
---------------------
|
|
53
|
+
Renders nodes in batches to prevent UI freeze:
|
|
54
|
+
|
|
55
|
+
<Tree
|
|
56
|
+
progressiveRender={true} <!-- Default -->
|
|
57
|
+
initialBatchSize={20} <!-- First batch (instant) -->
|
|
58
|
+
maxBatchSize={500} <!-- Maximum batch size -->
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
Exponential batching:
|
|
62
|
+
- First batch: 20 nodes (instant feedback)
|
|
63
|
+
- Second batch: 40 nodes
|
|
64
|
+
- Third batch: 80 nodes
|
|
65
|
+
- Doubles until maxBatchSize (500)
|
|
66
|
+
|
|
67
|
+
Result: UI stays responsive during large tree loads
|
|
68
|
+
|
|
69
|
+
ASYNC SEARCH INDEXING
|
|
70
|
+
---------------------
|
|
71
|
+
Search indexing uses requestIdleCallback:
|
|
72
|
+
|
|
73
|
+
<Tree
|
|
74
|
+
shouldUseInternalSearchIndex={true}
|
|
75
|
+
indexerBatchSize={25} <!-- Nodes per idle callback -->
|
|
76
|
+
indexerTimeout={50} <!-- Max wait before forcing -->
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
Benefits:
|
|
80
|
+
- Tree renders immediately
|
|
81
|
+
- Indexing runs during browser idle time
|
|
82
|
+
- Large datasets don't freeze UI
|
|
83
|
+
|
|
84
|
+
Check indexing status:
|
|
85
|
+
const stats = treeRef.statistics;
|
|
86
|
+
console.log('Indexing:', stats.isIndexing);
|
|
87
|
+
console.log('Pending:', stats.pendingIndexCount);
|
|
88
|
+
|
|
89
|
+
VIRTUAL SCROLL MODE
|
|
90
|
+
-------------------
|
|
91
|
+
For very large trees (10,000+ nodes), virtual scroll only renders
|
|
92
|
+
visible rows + overscan, keeping DOM size at ~50 nodes:
|
|
93
|
+
|
|
94
|
+
<Tree
|
|
95
|
+
virtualScroll={true}
|
|
96
|
+
virtualContainerHeight="500px" <!-- CSS height (auto-detected if not set) -->
|
|
97
|
+
virtualOverscan={5} <!-- Extra rows above/below viewport -->
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
How it works:
|
|
101
|
+
- Fixed-height container with overflow-y: auto
|
|
102
|
+
- Full-height spacer div for correct scrollbar proportions
|
|
103
|
+
- translateY(offset) positions visible window
|
|
104
|
+
- rAF-throttled scroll handler prevents excessive updates
|
|
105
|
+
- Auto-measures row height from first rendered node
|
|
106
|
+
|
|
107
|
+
Virtual scroll requires flat rendering (useFlatRendering=true, the default).
|
|
108
|
+
|
|
109
|
+
scrollToPath in virtual mode:
|
|
110
|
+
- Uses index-based scrolling (finds node in flat array)
|
|
111
|
+
- Centers target in viewport
|
|
112
|
+
- Waits for scroll + re-render (multiple rAF cycles)
|
|
113
|
+
- Applies highlight with retry
|
|
114
|
+
|
|
115
|
+
RENDERING MODE COMPARISON
|
|
116
|
+
-------------------------
|
|
117
|
+
| Mode | DOM Nodes | Best For |
|
|
118
|
+
|------|-----------|----------|
|
|
119
|
+
| Recursive | All nodes | Small trees (<100) |
|
|
120
|
+
| Flat | All nodes | Medium trees (100-10K) |
|
|
121
|
+
| Virtual | ~50 nodes | Large trees (10K+) |
|
|
122
|
+
|
|
123
|
+
RENDER PROGRESS CALLBACKS
|
|
124
|
+
-------------------------
|
|
125
|
+
Monitor progressive rendering:
|
|
126
|
+
|
|
127
|
+
<Tree
|
|
128
|
+
{data}
|
|
129
|
+
progressiveRender={true}
|
|
130
|
+
bind:isRendering
|
|
131
|
+
onRenderStart={() => console.log('Render started')}
|
|
132
|
+
onRenderProgress={(rendered, total) =>
|
|
133
|
+
console.log(`Rendered ${rendered}/${total}`)
|
|
134
|
+
}
|
|
135
|
+
onRenderComplete={() => console.log('Render complete')}
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
isRendering is a bindable boolean — useful for showing spinners/progress bars.
|
|
139
|
+
|
|
140
|
+
SORTING PERFORMANCE
|
|
141
|
+
-------------------
|
|
142
|
+
Level-first sorting is critical:
|
|
143
|
+
|
|
144
|
+
// REQUIRED: Sort by level first
|
|
145
|
+
sortCallback={(nodes) => {
|
|
146
|
+
return nodes.sort((a, b) => {
|
|
147
|
+
const aLevel = a.path.split('.').length;
|
|
148
|
+
const bLevel = b.path.split('.').length;
|
|
149
|
+
if (aLevel !== bLevel) return aLevel - bLevel;
|
|
150
|
+
return (a.data?.name ?? '').localeCompare(b.data?.name ?? '');
|
|
151
|
+
});
|
|
152
|
+
}}
|
|
153
|
+
|
|
154
|
+
For pre-sorted data (skip internal sort):
|
|
155
|
+
<Tree isSorted={true} />
|
|
156
|
+
|
|
157
|
+
PERFORMANCE LOGGING
|
|
158
|
+
-------------------
|
|
159
|
+
Enable performance measurements:
|
|
160
|
+
|
|
161
|
+
import { enablePerfLogging, setPerfThreshold } from '@keenmate/svelte-treeview';
|
|
162
|
+
|
|
163
|
+
enablePerfLogging();
|
|
164
|
+
setPerfThreshold(100); // Only log operations > 100ms
|
|
165
|
+
|
|
166
|
+
Or from browser console:
|
|
167
|
+
window.components['svelte-treeview'].perf.enable()
|
|
168
|
+
|
|
169
|
+
Logged operations:
|
|
170
|
+
- insertArray (conversion/sort/insert phases)
|
|
171
|
+
- filterNodes
|
|
172
|
+
- expandAll
|
|
173
|
+
- collapseAll
|
|
174
|
+
|
|
175
|
+
Output includes:
|
|
176
|
+
- Duration
|
|
177
|
+
- Item count
|
|
178
|
+
- Per-item time
|
|
179
|
+
- Items/sec throughput
|
|
180
|
+
|
|
181
|
+
CATEGORY LOGGING
|
|
182
|
+
----------------
|
|
183
|
+
import { setCategoryLevel } from '@keenmate/svelte-treeview';
|
|
184
|
+
|
|
185
|
+
// Enable specific categories
|
|
186
|
+
setCategoryLevel('LTREE:RENDER', 'debug');
|
|
187
|
+
setCategoryLevel('LTREE:DATA', 'debug');
|
|
188
|
+
|
|
189
|
+
Categories:
|
|
190
|
+
- LTREE:INIT - Initialization
|
|
191
|
+
- LTREE:DATA - Data operations
|
|
192
|
+
- LTREE:RENDER - Rendering
|
|
193
|
+
- LTREE:INDEX - Search indexing
|
|
194
|
+
- LTREE:DRAG - Drag and drop
|
|
195
|
+
- LTREE:UI - User interactions
|
|
196
|
+
|
|
197
|
+
BENCHMARKS
|
|
198
|
+
----------
|
|
199
|
+
Typical performance (5500 nodes):
|
|
200
|
+
|
|
201
|
+
| Operation | Time |
|
|
202
|
+
|-----------|------|
|
|
203
|
+
| Initial render (flat) | ~25ms |
|
|
204
|
+
| Initial render (recursive) | ~300ms |
|
|
205
|
+
| Initial render (virtual) | ~5ms |
|
|
206
|
+
| Expand/collapse | ~100-150ms |
|
|
207
|
+
| Search filtering | <50ms |
|
|
208
|
+
| insertArray | <100ms |
|
|
209
|
+
|
|
210
|
+
MEMORY OPTIMIZATION
|
|
211
|
+
-------------------
|
|
212
|
+
For very large datasets (10,000+ nodes):
|
|
213
|
+
|
|
214
|
+
1. Use $state.raw():
|
|
215
|
+
let data = $state.raw<Item[]>([]);
|
|
216
|
+
|
|
217
|
+
2. Enable progressive render:
|
|
218
|
+
<Tree progressiveRender={true} />
|
|
219
|
+
|
|
220
|
+
3. Limit initial expand:
|
|
221
|
+
<Tree expandLevel={1} />
|
|
222
|
+
|
|
223
|
+
4. Use virtual scrolling for 10K+ nodes:
|
|
224
|
+
<Tree virtualScroll={true} virtualContainerHeight="500px" />
|
|
225
|
+
|
|
226
|
+
AVOID O(n²) PATTERNS
|
|
227
|
+
--------------------
|
|
228
|
+
In sortCallback, avoid:
|
|
229
|
+
|
|
230
|
+
❌ nodes.forEach(n => {
|
|
231
|
+
const siblings = nodes.filter(s => s.parentPath === n.parentPath);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
✅ Pre-compute:
|
|
235
|
+
const siblingMap = new Map();
|
|
236
|
+
nodes.forEach(n => {
|
|
237
|
+
const parent = n.parentPath || '';
|
|
238
|
+
if (!siblingMap.has(parent)) siblingMap.set(parent, []);
|
|
239
|
+
siblingMap.get(parent).push(n);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
BATCH DATA UPDATES
|
|
243
|
+
------------------
|
|
244
|
+
Instead of multiple small updates:
|
|
245
|
+
|
|
246
|
+
❌ for (const item of items) {
|
|
247
|
+
data = [...data, item]; // Re-renders each time
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
✅ data = [...data, ...items]; // Single re-render
|
|
251
|
+
|
|
252
|
+
EXPAND/COLLAPSE OPTIMIZATION
|
|
253
|
+
----------------------------
|
|
254
|
+
For large trees, be specific:
|
|
255
|
+
|
|
256
|
+
// Slow: expands ALL nodes
|
|
257
|
+
treeRef.expandAll();
|
|
258
|
+
|
|
259
|
+
// Fast: expand specific subtree
|
|
260
|
+
treeRef.expandAll('1.2'); // Only under 1.2
|
|
261
|
+
|
|
262
|
+
// Fast: expand to level
|
|
263
|
+
<Tree expandLevel={2} />
|
|
264
|
+
|
|
265
|
+
SEARCH OPTIMIZATION
|
|
266
|
+
-------------------
|
|
267
|
+
Debounce search input:
|
|
268
|
+
|
|
269
|
+
let inputValue = $state('');
|
|
270
|
+
let searchText = $state('');
|
|
271
|
+
let timer;
|
|
272
|
+
|
|
273
|
+
function handleInput(e) {
|
|
274
|
+
inputValue = e.target.value;
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
timer = setTimeout(() => {
|
|
277
|
+
searchText = inputValue;
|
|
278
|
+
}, 300);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Simplify search value callback:
|
|
282
|
+
|
|
283
|
+
// Avoid heavy computation
|
|
284
|
+
❌ getSearchValueCallback={(node) => {
|
|
285
|
+
return expensiveOperation(node.data);
|
|
286
|
+
}}
|
|
287
|
+
|
|
288
|
+
// Keep it simple
|
|
289
|
+
✅ getSearchValueCallback={(node) => {
|
|
290
|
+
return `${node.data.name} ${node.data.email}`;
|
|
291
|
+
}}
|
|
292
|
+
|
|
293
|
+
DEBUG MODE OVERHEAD
|
|
294
|
+
-------------------
|
|
295
|
+
Disable in production:
|
|
296
|
+
|
|
297
|
+
<Tree shouldDisplayDebugInformation={false} />
|
|
298
|
+
|
|
299
|
+
Debug mode adds:
|
|
300
|
+
- Statistics calculations
|
|
301
|
+
- Console logging
|
|
302
|
+
- Debug panel rendering
|
|
303
|
+
|
|
304
|
+
COMMON PERFORMANCE ISSUES
|
|
305
|
+
-------------------------
|
|
306
|
+
Issue: Tree takes forever to render
|
|
307
|
+
Fix: Use $state.raw() instead of $state()
|
|
308
|
+
|
|
309
|
+
Issue: UI freezes during load
|
|
310
|
+
Fix: Enable progressiveRender={true}
|
|
311
|
+
|
|
312
|
+
Issue: Slow search
|
|
313
|
+
Fix: Simplify getSearchValueCallback, use debouncing
|
|
314
|
+
|
|
315
|
+
Issue: Slow sorting
|
|
316
|
+
Fix: Avoid O(n²) patterns, use isSorted={true} for pre-sorted data
|
|
317
|
+
|
|
318
|
+
Issue: Memory issues with huge datasets
|
|
319
|
+
Fix: Limit expandLevel, use virtualScroll={true}
|
|
320
|
+
|
|
321
|
+
PROFILING
|
|
322
|
+
---------
|
|
323
|
+
Use browser DevTools:
|
|
324
|
+
|
|
325
|
+
1. Performance tab → Record
|
|
326
|
+
2. Load tree or perform operation
|
|
327
|
+
3. Look for long tasks
|
|
328
|
+
|
|
329
|
+
Common bottlenecks:
|
|
330
|
+
- Proxy access (use $state.raw())
|
|
331
|
+
- Recursive component creation (use flat mode)
|
|
332
|
+
- Search indexing (runs async, wait for completion)
|
|
333
|
+
|
|
334
|
+
BEST PRACTICES
|
|
335
|
+
--------------
|
|
336
|
+
✅ Use $state.raw() for 1000+ items
|
|
337
|
+
✅ Keep progressiveRender={true}
|
|
338
|
+
✅ Keep useFlatRendering={true}
|
|
339
|
+
✅ Limit expandLevel for large trees
|
|
340
|
+
✅ Debounce search input
|
|
341
|
+
✅ Use isSorted={true} for pre-sorted data
|
|
342
|
+
✅ Use virtualScroll={true} for 10K+ nodes
|
|
343
|
+
✅ Profile before optimizing
|
|
344
|
+
|
|
345
|
+
❌ Don't use $state() for large datasets
|
|
346
|
+
❌ Don't disable progressive rendering
|
|
347
|
+
❌ Don't expand all levels for large trees
|
|
348
|
+
❌ Don't do heavy computation in callbacks
|
|
349
|
+
❌ Don't block UI with sync operations
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
SEARCH FEATURES
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
CRITICAL: Search requires explicit configuration
|
|
5
|
+
- Set shouldUseInternalSearchIndex={true}
|
|
6
|
+
- Provide searchValueMember OR getSearchValueCallback
|
|
7
|
+
- Optional: FlexSearch for advanced search
|
|
8
|
+
|
|
9
|
+
BASIC SEARCH SETUP
|
|
10
|
+
------------------
|
|
11
|
+
<script>
|
|
12
|
+
let searchText = $state('');
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<input bind:value={searchText} placeholder="Search..." />
|
|
16
|
+
|
|
17
|
+
<Tree
|
|
18
|
+
{data}
|
|
19
|
+
idMember="id"
|
|
20
|
+
pathMember="path"
|
|
21
|
+
bind:searchText
|
|
22
|
+
shouldUseInternalSearchIndex={true}
|
|
23
|
+
searchValueMember="name"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
SEARCH VALUE CONFIGURATION
|
|
27
|
+
--------------------------
|
|
28
|
+
What text gets indexed for search:
|
|
29
|
+
|
|
30
|
+
// Simple: use a property
|
|
31
|
+
<Tree searchValueMember="name" />
|
|
32
|
+
|
|
33
|
+
// Complex: use callback for computed values
|
|
34
|
+
<Tree
|
|
35
|
+
getSearchValueCallback={(node) => {
|
|
36
|
+
const item = node.data;
|
|
37
|
+
return `${item.name} ${item.description} ${item.tags.join(' ')}`;
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
ASYNC SEARCH INDEXING
|
|
42
|
+
---------------------
|
|
43
|
+
IMPORTANT: Search indexing happens asynchronously
|
|
44
|
+
|
|
45
|
+
- Tree renders immediately
|
|
46
|
+
- Indexing runs during browser idle time
|
|
47
|
+
- Uses requestIdleCallback (or setTimeout fallback)
|
|
48
|
+
- Large datasets won't freeze UI
|
|
49
|
+
|
|
50
|
+
Check indexing status:
|
|
51
|
+
const stats = treeRef.statistics;
|
|
52
|
+
console.log('Is indexing:', stats.isIndexing);
|
|
53
|
+
console.log('Pending:', stats.pendingIndexCount);
|
|
54
|
+
|
|
55
|
+
INDEXER CONFIGURATION
|
|
56
|
+
---------------------
|
|
57
|
+
<Tree
|
|
58
|
+
indexerBatchSize={25} <!-- Nodes per batch (default: 25) -->
|
|
59
|
+
indexerTimeout={50} <!-- Max wait time ms (default: 50) -->
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
indexerBatchSize:
|
|
63
|
+
- Lower (10-25): Smoother UI, slower indexing
|
|
64
|
+
- Higher (50-100): Faster indexing, brief UI pauses
|
|
65
|
+
|
|
66
|
+
indexerTimeout:
|
|
67
|
+
- Lower (25-50ms): More responsive indexing
|
|
68
|
+
- Higher (100-200ms): More genuine idle periods
|
|
69
|
+
|
|
70
|
+
FILTER VS SEARCH
|
|
71
|
+
----------------
|
|
72
|
+
filterNodes() - Hides non-matching nodes in tree display
|
|
73
|
+
|
|
74
|
+
searchText binding uses filterNodes internally:
|
|
75
|
+
<Tree bind:searchText ... />
|
|
76
|
+
|
|
77
|
+
searchNodes() - Returns matching nodes without filtering
|
|
78
|
+
|
|
79
|
+
<script>
|
|
80
|
+
function search(query) {
|
|
81
|
+
const results = treeRef.searchNodes(query);
|
|
82
|
+
console.log('Found:', results.length, 'nodes');
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
FLEXSEARCH OPTIONS
|
|
88
|
+
------------------
|
|
89
|
+
Both methods accept FlexSearch options:
|
|
90
|
+
|
|
91
|
+
// Search with options
|
|
92
|
+
const results = treeRef.searchNodes('query', {
|
|
93
|
+
suggest: true, // Enable suggestions for typos
|
|
94
|
+
limit: 10, // Max results
|
|
95
|
+
threshold: 0.8, // Similarity threshold
|
|
96
|
+
bool: 'and' // AND logic for multiple terms
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Filter with options
|
|
100
|
+
treeRef.filterNodes('query', {
|
|
101
|
+
suggest: true,
|
|
102
|
+
limit: 50
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
SEARCH RESULTS NAVIGATION
|
|
106
|
+
-------------------------
|
|
107
|
+
<script>
|
|
108
|
+
let searchText = $state('');
|
|
109
|
+
let results = $state([]);
|
|
110
|
+
let currentIndex = $state(0);
|
|
111
|
+
let treeRef;
|
|
112
|
+
|
|
113
|
+
$effect(() => {
|
|
114
|
+
if (searchText) {
|
|
115
|
+
results = treeRef.searchNodes(searchText);
|
|
116
|
+
currentIndex = 0;
|
|
117
|
+
if (results.length > 0) {
|
|
118
|
+
goToResult(0);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
async function goToResult(index) {
|
|
124
|
+
currentIndex = index;
|
|
125
|
+
await treeRef.scrollToPath(results[index].path, {
|
|
126
|
+
expand: true,
|
|
127
|
+
highlight: true,
|
|
128
|
+
containerScroll: true
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function next() {
|
|
133
|
+
const nextIndex = (currentIndex + 1) % results.length;
|
|
134
|
+
goToResult(nextIndex);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function prev() {
|
|
138
|
+
const prevIndex = (currentIndex - 1 + results.length) % results.length;
|
|
139
|
+
goToResult(prevIndex);
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<input bind:value={searchText} />
|
|
144
|
+
<span>{currentIndex + 1} of {results.length}</span>
|
|
145
|
+
<button onclick={prev}>Prev</button>
|
|
146
|
+
<button onclick={next}>Next</button>
|
|
147
|
+
|
|
148
|
+
CLEAR SEARCH
|
|
149
|
+
------------
|
|
150
|
+
// Clear search text (shows all nodes)
|
|
151
|
+
searchText = '';
|
|
152
|
+
|
|
153
|
+
// Or programmatically
|
|
154
|
+
treeRef.filterNodes('');
|
|
155
|
+
|
|
156
|
+
CUSTOM SEARCH INDEX
|
|
157
|
+
-------------------
|
|
158
|
+
Initialize your own FlexSearch index:
|
|
159
|
+
|
|
160
|
+
<script>
|
|
161
|
+
import { Index } from 'flexsearch';
|
|
162
|
+
|
|
163
|
+
function initializeIndex() {
|
|
164
|
+
return new Index({
|
|
165
|
+
tokenize: 'forward',
|
|
166
|
+
resolution: 9,
|
|
167
|
+
cache: true
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<Tree
|
|
173
|
+
initializeIndexCallback={initializeIndex}
|
|
174
|
+
shouldUseInternalSearchIndex={true}
|
|
175
|
+
searchValueMember="name"
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
SEARCH WITHOUT INTERNAL INDEX
|
|
179
|
+
-----------------------------
|
|
180
|
+
Handle search externally:
|
|
181
|
+
|
|
182
|
+
<script>
|
|
183
|
+
let data = $state.raw([...]);
|
|
184
|
+
let filteredData = $state.raw([...]);
|
|
185
|
+
let searchText = $state('');
|
|
186
|
+
|
|
187
|
+
$effect(() => {
|
|
188
|
+
if (searchText) {
|
|
189
|
+
filteredData = data.filter(item =>
|
|
190
|
+
item.name.toLowerCase().includes(searchText.toLowerCase())
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
filteredData = data;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<input bind:value={searchText} />
|
|
199
|
+
<Tree data={filteredData} ... />
|
|
200
|
+
|
|
201
|
+
Note: This approach loses the tree hierarchy when filtering
|
|
202
|
+
|
|
203
|
+
SEARCH HIGHLIGHT
|
|
204
|
+
----------------
|
|
205
|
+
Highlight matching text in node template:
|
|
206
|
+
|
|
207
|
+
<Tree {data} bind:searchText>
|
|
208
|
+
{#snippet nodeTemplate(node)}
|
|
209
|
+
{@html highlightMatch(node.data.name, searchText)}
|
|
210
|
+
{/snippet}
|
|
211
|
+
</Tree>
|
|
212
|
+
|
|
213
|
+
<script>
|
|
214
|
+
function highlightMatch(text, query) {
|
|
215
|
+
if (!query) return text;
|
|
216
|
+
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
|
|
217
|
+
return text.replace(regex, '<mark>$1</mark>');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function escapeRegex(string) {
|
|
221
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
222
|
+
}
|
|
223
|
+
</script>
|
|
224
|
+
|
|
225
|
+
SEARCH DEBOUNCING
|
|
226
|
+
-----------------
|
|
227
|
+
Debounce search input for better performance:
|
|
228
|
+
|
|
229
|
+
<script>
|
|
230
|
+
let inputValue = $state('');
|
|
231
|
+
let searchText = $state('');
|
|
232
|
+
let debounceTimer;
|
|
233
|
+
|
|
234
|
+
function handleInput(e) {
|
|
235
|
+
inputValue = e.target.value;
|
|
236
|
+
clearTimeout(debounceTimer);
|
|
237
|
+
debounceTimer = setTimeout(() => {
|
|
238
|
+
searchText = inputValue;
|
|
239
|
+
}, 300);
|
|
240
|
+
}
|
|
241
|
+
</script>
|
|
242
|
+
|
|
243
|
+
<input value={inputValue} oninput={handleInput} />
|
|
244
|
+
<Tree bind:searchText ... />
|
|
245
|
+
|
|
246
|
+
SEARCH STATISTICS
|
|
247
|
+
-----------------
|
|
248
|
+
Track search results:
|
|
249
|
+
|
|
250
|
+
const stats = treeRef.statistics;
|
|
251
|
+
console.log('Total nodes:', stats.nodeCount);
|
|
252
|
+
console.log('Filtered visible:', stats.filteredNodeCount);
|
|
253
|
+
|
|
254
|
+
// After search
|
|
255
|
+
$effect(() => {
|
|
256
|
+
if (searchText) {
|
|
257
|
+
console.log(`Showing ${stats.filteredNodeCount} of ${stats.nodeCount}`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
MULTIPLE SEARCH FIELDS
|
|
262
|
+
----------------------
|
|
263
|
+
Index multiple properties:
|
|
264
|
+
|
|
265
|
+
<Tree
|
|
266
|
+
getSearchValueCallback={(node) => {
|
|
267
|
+
const item = node.data;
|
|
268
|
+
return [
|
|
269
|
+
item.name,
|
|
270
|
+
item.email,
|
|
271
|
+
item.department,
|
|
272
|
+
item.tags.join(' ')
|
|
273
|
+
].join(' ');
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
|
|
277
|
+
SEARCH WITH EMPTY TREE
|
|
278
|
+
----------------------
|
|
279
|
+
Handle no results state:
|
|
280
|
+
|
|
281
|
+
<Tree {data} bind:searchText>
|
|
282
|
+
{#snippet noDataFound()}
|
|
283
|
+
{#if searchText}
|
|
284
|
+
<p>No results for "{searchText}"</p>
|
|
285
|
+
{:else}
|
|
286
|
+
<p>No items available</p>
|
|
287
|
+
{/if}
|
|
288
|
+
{/snippet}
|
|
289
|
+
</Tree>
|
|
290
|
+
|
|
291
|
+
COMMON SEARCH PATTERNS
|
|
292
|
+
----------------------
|
|
293
|
+
File search:
|
|
294
|
+
<Tree
|
|
295
|
+
searchValueMember="name"
|
|
296
|
+
getSearchValueCallback={(node) =>
|
|
297
|
+
`${node.data.name} ${node.data.path} ${node.data.extension}`
|
|
298
|
+
}
|
|
299
|
+
/>
|
|
300
|
+
|
|
301
|
+
User search:
|
|
302
|
+
<Tree
|
|
303
|
+
getSearchValueCallback={(node) =>
|
|
304
|
+
`${node.data.firstName} ${node.data.lastName} ${node.data.email}`
|
|
305
|
+
}
|
|
306
|
+
/>
|
|
307
|
+
|
|
308
|
+
Product search:
|
|
309
|
+
<Tree
|
|
310
|
+
getSearchValueCallback={(node) =>
|
|
311
|
+
`${node.data.name} ${node.data.sku} ${node.data.category} ${node.data.description}`
|
|
312
|
+
}
|
|
313
|
+
/>
|
|
314
|
+
|
|
315
|
+
DEBUGGING SEARCH
|
|
316
|
+
----------------
|
|
317
|
+
Enable search indexing logs:
|
|
318
|
+
|
|
319
|
+
import { setCategoryLevel } from '@keenmate/svelte-treeview';
|
|
320
|
+
setCategoryLevel('LTREE:INDEX', 'debug');
|
|
321
|
+
|
|
322
|
+
// Console shows:
|
|
323
|
+
// - Indexer initialization
|
|
324
|
+
// - Queue management
|
|
325
|
+
// - Batch processing
|
|
326
|
+
// - Indexing completion
|
|
327
|
+
|
|
328
|
+
Check index status:
|
|
329
|
+
console.log('Stats:', treeRef.statistics);
|
|
330
|
+
|
|
331
|
+
BEST PRACTICES
|
|
332
|
+
--------------
|
|
333
|
+
✅ Set shouldUseInternalSearchIndex={true}
|
|
334
|
+
✅ Provide searchValueMember or getSearchValueCallback
|
|
335
|
+
✅ Use debouncing for search input
|
|
336
|
+
✅ Handle loading state during indexing
|
|
337
|
+
✅ Provide feedback for no results
|
|
338
|
+
|
|
339
|
+
❌ Don't search before indexing completes
|
|
340
|
+
❌ Don't use heavy callbacks (slows indexing)
|
|
341
|
+
❌ Don't forget to clear search
|
|
342
|
+
❌ Don't rely on immediate search after data load
|
|
343
|
+
|
|
344
|
+
TROUBLESHOOTING
|
|
345
|
+
---------------
|
|
346
|
+
Search not working:
|
|
347
|
+
✅ Check shouldUseInternalSearchIndex={true}
|
|
348
|
+
✅ Check searchValueMember or getSearchValueCallback is set
|
|
349
|
+
✅ Wait for indexing to complete (check statistics.isIndexing)
|
|
350
|
+
|
|
351
|
+
Search slow:
|
|
352
|
+
✅ Reduce indexerBatchSize
|
|
353
|
+
✅ Simplify getSearchValueCallback
|
|
354
|
+
✅ Use $state.raw() for large datasets
|
|
355
|
+
|
|
356
|
+
No results found:
|
|
357
|
+
✅ Check searchValueMember matches data property
|
|
358
|
+
✅ Check getSearchValueCallback returns string
|
|
359
|
+
✅ Check data actually contains search term
|