@keenmate/svelte-treeview 4.0.0 → 4.1.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 +26 -1
- package/dist/components/Tree.svelte +10 -7
- package/dist/helpers/ltree-helpers.d.ts +3 -3
- package/dist/helpers/ltree-helpers.js +8 -13
- package/dist/ltree/ltree.svelte.js +45 -11
- package/dist/styles.scss +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A high-performance, feature-rich hierarchical tree view component for Svelte 5 with drag & drop support, search functionality, and flexible data structures using LTree.
|
|
4
4
|
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> **Looking for a framework-agnostic solution?** There's also a web component version that can be used standalone or in other frameworks at https://github.com/KeenMate/web-treeview/
|
|
7
|
+
|
|
5
8
|
## 🚀 Features
|
|
6
9
|
|
|
7
10
|
- **Svelte 5 Native**: Built specifically for Svelte 5 with full support for runes and modern Svelte patterns
|
|
@@ -622,6 +625,28 @@ interface NodeData {
|
|
|
622
625
|
- Second level: `"1.1"`, `"1.2"`, `"2.1"`
|
|
623
626
|
- Third level: `"1.1.1"`, `"1.2.1"`, `"2.1.1"`
|
|
624
627
|
|
|
628
|
+
### Sorting Requirements
|
|
629
|
+
|
|
630
|
+
**Important:** For proper tree construction, your `sortCallback` must sort by **level first** to ensure parent nodes are inserted before their children:
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
const sortCallback = (items: LTreeNode<T>[]) => {
|
|
634
|
+
return items.sort((a, b) => {
|
|
635
|
+
// First, sort by level (shallower levels first)
|
|
636
|
+
const aLevel = a.path.split('.').length;
|
|
637
|
+
const bLevel = b.path.split('.').length;
|
|
638
|
+
if (aLevel !== bLevel) {
|
|
639
|
+
return aLevel - bLevel;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Then sort by your custom criteria
|
|
643
|
+
return a.data.name.localeCompare(b.data.name);
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
**Why this matters:** If deeper level nodes are processed before their parents, you'll get "Could not find parent node" errors during tree construction. Level-first sorting ensures hierarchical integrity and enables progressive rendering for large datasets.
|
|
649
|
+
|
|
625
650
|
### Insert Result Information
|
|
626
651
|
|
|
627
652
|
The tree provides detailed information about data insertion through the `insertResult` bindable property:
|
|
@@ -709,4 +734,4 @@ MIT License - see LICENSE file for details.
|
|
|
709
734
|
|
|
710
735
|
---
|
|
711
736
|
|
|
712
|
-
Built with ❤️ by [KeenMate](https://github.com/keenmate)
|
|
737
|
+
Built with ❤️ by [KeenMate](https://github.com/keenmate)
|
|
@@ -222,8 +222,9 @@
|
|
|
222
222
|
|
|
223
223
|
treeId = treeId || generateTreeId();
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
if (shouldDisplayDebugInformation)
|
|
226
|
+
console.log("Tree treePathSeparator:", treePathSeparator)
|
|
227
|
+
|
|
227
228
|
// svelte-ignore non_reactive_update
|
|
228
229
|
const tree: Ltree<T> = createLTree<T>(
|
|
229
230
|
idMember,
|
|
@@ -257,6 +258,11 @@
|
|
|
257
258
|
}
|
|
258
259
|
);
|
|
259
260
|
|
|
261
|
+
// Update tree separator when prop changes
|
|
262
|
+
$effect(() => {
|
|
263
|
+
tree.treePathSeparator = treePathSeparator;
|
|
264
|
+
});
|
|
265
|
+
|
|
260
266
|
setContext('Ltree', tree);
|
|
261
267
|
|
|
262
268
|
$effect(() => {
|
|
@@ -284,7 +290,6 @@
|
|
|
284
290
|
if (selectedNode) {
|
|
285
291
|
const previousNode = tree.getNodeByPath(selectedNode.path);
|
|
286
292
|
if (previousNode) {
|
|
287
|
-
console.log('🚀 ~ _onNodeClicked ~ previousNode:', previousNode);
|
|
288
293
|
previousNode.isSelected = false;
|
|
289
294
|
} else selectedNode = null;
|
|
290
295
|
}
|
|
@@ -325,8 +330,6 @@
|
|
|
325
330
|
// event.dataTransfer.effectAllowed = "move";
|
|
326
331
|
// event.dataTransfer.setData("text/plain", node.path);
|
|
327
332
|
// }
|
|
328
|
-
|
|
329
|
-
console.log('🚀 ~ _onNodeDragStart ~ draggedNode:', draggedNode, event);
|
|
330
333
|
}
|
|
331
334
|
|
|
332
335
|
function _onNodeDragOver(node: LTreeNode<T>, event: DragEvent) {
|
|
@@ -464,8 +467,8 @@
|
|
|
464
467
|
</div>
|
|
465
468
|
{/if}
|
|
466
469
|
</div>
|
|
467
|
-
|
|
468
|
-
|
|
470
|
+
|
|
471
|
+
{@render treeFooter?.()}
|
|
469
472
|
|
|
470
473
|
<!-- Context Menu -->
|
|
471
474
|
{#if contextMenuVisible && contextMenu && contextMenuNode}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function getParentPath(path: string): string | null;
|
|
2
|
-
export declare function getRelativePath(path: string, parentPath: string): string;
|
|
3
|
-
export declare function getPathSegments(path: string, start?: number, count?: number): string;
|
|
1
|
+
export declare function getParentPath(path: string, pathSeparator?: string): string | null;
|
|
2
|
+
export declare function getRelativePath(path: string, parentPath: string, pathSeparator?: string): string;
|
|
3
|
+
export declare function getPathSegments(path: string, start?: number, count?: number, pathSeparator?: string): string;
|
|
4
4
|
export declare function getLevel(path: string, pathSeparator: string): number;
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import { isEmptyString } from './string-helpers.js';
|
|
2
|
-
export function getParentPath(path) {
|
|
2
|
+
export function getParentPath(path, pathSeparator = '.') {
|
|
3
3
|
if (!path || typeof path !== 'string')
|
|
4
4
|
return null;
|
|
5
|
-
const lastDotIndex = path.lastIndexOf(
|
|
5
|
+
const lastDotIndex = path.lastIndexOf(pathSeparator);
|
|
6
6
|
return lastDotIndex === -1 ? '' : path.substring(0, lastDotIndex);
|
|
7
7
|
}
|
|
8
|
-
export function getRelativePath(path, parentPath) {
|
|
8
|
+
export function getRelativePath(path, parentPath, pathSeparator = '.') {
|
|
9
9
|
if (isEmptyString(parentPath))
|
|
10
10
|
return path;
|
|
11
|
-
return path.startsWith(parentPath +
|
|
11
|
+
return path.startsWith(parentPath + pathSeparator) ? path.substring(parentPath.length + pathSeparator.length) : path;
|
|
12
12
|
}
|
|
13
|
-
export function getPathSegments(path, start = 0, count = 1) {
|
|
14
|
-
const segments = path.split(
|
|
13
|
+
export function getPathSegments(path, start = 0, count = 1, pathSeparator = '.') {
|
|
14
|
+
const segments = path.split(pathSeparator);
|
|
15
15
|
const taken = segments.slice(start, start + count);
|
|
16
|
-
return taken.join(
|
|
16
|
+
return taken.join(pathSeparator);
|
|
17
17
|
}
|
|
18
18
|
export function getLevel(path, pathSeparator) {
|
|
19
|
-
|
|
20
|
-
for (let i = 0; i < path.length; i++) {
|
|
21
|
-
if (path[i] === pathSeparator)
|
|
22
|
-
count++;
|
|
23
|
-
}
|
|
24
|
-
return count;
|
|
19
|
+
return path.split(pathSeparator).length;
|
|
25
20
|
}
|
|
@@ -95,11 +95,11 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
95
95
|
node.id = _idMember ? row[_idMember] : undefined;
|
|
96
96
|
node.path = _pathMember ? row[_pathMember] : undefined;
|
|
97
97
|
if (shouldCalculateParentPath) {
|
|
98
|
-
node.parentPath = getParentPath(node.path);
|
|
98
|
+
node.parentPath = getParentPath(node.path, this.treePathSeparator);
|
|
99
99
|
}
|
|
100
100
|
else
|
|
101
101
|
node.parentPath = row[_parentPathMember];
|
|
102
|
-
node.pathSegment = getPathSegments(getRelativePath(node.path, node.parentPath));
|
|
102
|
+
node.pathSegment = getPathSegments(getRelativePath(node.path, node.parentPath, this.treePathSeparator), 0, 1, this.treePathSeparator);
|
|
103
103
|
if (!shouldCalculateLevel)
|
|
104
104
|
node.level = row[_levelMember];
|
|
105
105
|
else
|
|
@@ -137,6 +137,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
137
137
|
const itemsToIndex = [];
|
|
138
138
|
let realIndex = 0; // this is used to avoid scenario, when node cannot found a parent
|
|
139
139
|
let successfulCount = 0;
|
|
140
|
+
let hasRenderedExpandLevel = false;
|
|
140
141
|
mappedData.forEach((node, index) => {
|
|
141
142
|
const result = this.insertTreeNode(node.parentPath, node, true);
|
|
142
143
|
if (result) {
|
|
@@ -154,6 +155,23 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
154
155
|
itemsToIndex.push({ node, index: realIndex });
|
|
155
156
|
realIndex++;
|
|
156
157
|
}
|
|
158
|
+
// Progressive rendering: emit changes when we complete expandLevel
|
|
159
|
+
if (!noEmitChanges && !hasRenderedExpandLevel && _expandLevel && node.level && node.level <= _expandLevel) {
|
|
160
|
+
// Check if this might be the last node at expandLevel by looking ahead
|
|
161
|
+
const remainingNodes = mappedData.slice(index + 1);
|
|
162
|
+
const hasMoreAtExpandLevel = remainingNodes.some(futureNode => {
|
|
163
|
+
const futureLevel = futureNode.level || getLevel(futureNode.path, this.treePathSeparator);
|
|
164
|
+
return futureLevel <= _expandLevel;
|
|
165
|
+
});
|
|
166
|
+
if (!hasMoreAtExpandLevel) {
|
|
167
|
+
// We've processed all nodes up to expandLevel - render now!
|
|
168
|
+
hasRenderedExpandLevel = true;
|
|
169
|
+
this._emitTreeChanged();
|
|
170
|
+
if (this.shouldDisplayDebugInformation) {
|
|
171
|
+
console.log(`[Tree ${_treeId}] Progressive render: Displayed levels 1-${_expandLevel} (${successfulCount} nodes processed so far)`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
157
175
|
}
|
|
158
176
|
});
|
|
159
177
|
// Log errors for backward compatibility and debugging
|
|
@@ -172,8 +190,22 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
172
190
|
});
|
|
173
191
|
indexer.addToQueue(itemsToIndex);
|
|
174
192
|
}
|
|
193
|
+
// Final render (only if we haven't already rendered progressively)
|
|
175
194
|
if (!noEmitChanges) {
|
|
176
|
-
|
|
195
|
+
if (hasRenderedExpandLevel) {
|
|
196
|
+
// We already rendered expandLevel, now render the complete tree
|
|
197
|
+
this._emitTreeChanged();
|
|
198
|
+
if (this.shouldDisplayDebugInformation) {
|
|
199
|
+
console.log(`[Tree ${_treeId}] Final render: Complete tree with all ${successfulCount} nodes`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// No progressive rendering occurred, render everything at once
|
|
204
|
+
this._emitTreeChanged();
|
|
205
|
+
if (this.shouldDisplayDebugInformation) {
|
|
206
|
+
console.log(`[Tree ${_treeId}] Single render: All ${successfulCount} nodes (no expandLevel or progressive render conditions met)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
177
209
|
}
|
|
178
210
|
performance.mark('insert-end');
|
|
179
211
|
performance.measure('sort-duration', 'sort-start', 'sort-end');
|
|
@@ -199,7 +231,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
199
231
|
if (shouldCalculateLevel) {
|
|
200
232
|
newNode.level = (parentNode.level || 0) + 1;
|
|
201
233
|
}
|
|
202
|
-
const newSegment = segmentPrefix + getPathSegments(getRelativePath(newNode?.path, parentPath));
|
|
234
|
+
const newSegment = segmentPrefix + getPathSegments(getRelativePath(newNode?.path, parentPath, this.treePathSeparator), 0, 1, this.treePathSeparator);
|
|
203
235
|
if (!parentNode.children.hasOwnProperty(newSegment)) {
|
|
204
236
|
parentNode.children[newSegment] = newNode;
|
|
205
237
|
if (shouldCalculateHasChildren && !parentNode.hasChildren) {
|
|
@@ -425,6 +457,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
425
457
|
},
|
|
426
458
|
_defaultSort: function (self, items) {
|
|
427
459
|
return items.sort((a, b) => {
|
|
460
|
+
// First, sort by level (shallower levels first)
|
|
461
|
+
const aLevel = a.level || 0;
|
|
462
|
+
const bLevel = b.level || 0;
|
|
463
|
+
if (aLevel !== bLevel) {
|
|
464
|
+
return aLevel - bLevel;
|
|
465
|
+
}
|
|
466
|
+
// Then sort by parent path
|
|
428
467
|
if (a.parentPath !== b.parentPath) {
|
|
429
468
|
if (a.parentPath === '')
|
|
430
469
|
return -1;
|
|
@@ -432,13 +471,8 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
432
471
|
return 1;
|
|
433
472
|
return a.parentPath.localeCompare(b.parentPath);
|
|
434
473
|
}
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
// self.getNodeDisplayValue(a),
|
|
438
|
-
// self.getNodeDisplayValue(b),
|
|
439
|
-
// self.getNodeDisplayValue(a).localeCompare(this.getNodeDisplayValue(b))
|
|
440
|
-
// );
|
|
441
|
-
return self.getNodeDisplayValue(a).localeCompare(this.getNodeDisplayValue(b));
|
|
474
|
+
// Finally sort by display value
|
|
475
|
+
return self.getNodeDisplayValue(a).localeCompare(self.getNodeDisplayValue(b));
|
|
442
476
|
});
|
|
443
477
|
},
|
|
444
478
|
_emitTreeChanged: function () {
|
package/dist/styles.scss
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// CSS entry point for the library
|
|
2
|
-
@
|
|
2
|
+
@use './styles/main.scss';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keenmate/svelte-treeview",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev --port 17777",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"prepack": "svelte-kit sync && svelte-package && sass src/lib/styles.scss dist/styles.css && publint",
|
|
11
11
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
12
12
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
13
|
+
"test": "vitest",
|
|
14
|
+
"test:run": "vitest run",
|
|
13
15
|
"format": "prettier --write .",
|
|
14
16
|
"lint": "prettier --check . && eslint ."
|
|
15
17
|
},
|
|
@@ -59,7 +61,8 @@
|
|
|
59
61
|
"svelte-preprocess": "^6.0.3",
|
|
60
62
|
"typescript": "^5.0.0",
|
|
61
63
|
"typescript-eslint": "^8.20.0",
|
|
62
|
-
"vite": "^7.0.4"
|
|
64
|
+
"vite": "^7.0.4",
|
|
65
|
+
"vitest": "^3.2.4"
|
|
63
66
|
},
|
|
64
67
|
"optionalDependencies": {
|
|
65
68
|
"flexsearch": "^0.8.212"
|