@keenmate/svelte-treeview 4.7.0 → 4.8.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 +148 -205
- package/dist/components/Node.svelte +39 -67
- package/dist/components/Node.svelte.d.ts +1 -1
- package/dist/components/Tree.svelte +416 -171
- package/dist/components/Tree.svelte.d.ts +31 -13
- package/dist/constants.generated.d.ts +1 -1
- package/dist/constants.generated.js +1 -1
- package/dist/ltree/indexer.js +3 -1
- package/dist/ltree/ltree-node.svelte.d.ts +1 -0
- package/dist/ltree/ltree-node.svelte.js +1 -0
- package/dist/ltree/ltree.svelte.js +104 -72
- package/dist/ltree/types.d.ts +4 -0
- package/dist/styles/main.scss +53 -6
- package/dist/styles.css +43 -6
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
// Flat rendering mode
|
|
28
28
|
flatMode?: boolean; // When true, don't render children (Tree handles flat rendering)
|
|
29
|
-
|
|
29
|
+
flatGap?: boolean; // When true in flat mode, add margin-top to match recursive .ltree-children gap
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Destructure props using Svelte 5 syntax
|
|
@@ -47,45 +47,39 @@
|
|
|
47
47
|
|
|
48
48
|
// Flat rendering mode
|
|
49
49
|
flatMode = false,
|
|
50
|
-
|
|
50
|
+
flatGap = false,
|
|
51
51
|
}: Props = $props()
|
|
52
52
|
|
|
53
53
|
// Get stable references from context (avoids prop drilling and re-renders from inline functions)
|
|
54
54
|
const callbacks = getContext<NodeCallbacks<T>>('NodeCallbacks');
|
|
55
55
|
const config = getContext<NodeConfig>('NodeConfig');
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
dropZoneMaxWidth,
|
|
69
|
-
allowCopy,
|
|
70
|
-
} = config;
|
|
57
|
+
// Use $derived so values track mutations on the shared config proxy
|
|
58
|
+
const shouldToggleOnNodeClick = $derived(config.shouldToggleOnNodeClick);
|
|
59
|
+
const expandIconClass = $derived(config.expandIconClass);
|
|
60
|
+
const collapseIconClass = $derived(config.collapseIconClass);
|
|
61
|
+
const leafIconClass = $derived(config.leafIconClass);
|
|
62
|
+
const selectedNodeClass = $derived(config.selectedNodeClass);
|
|
63
|
+
const dragOverNodeClass = $derived(config.dragOverNodeClass);
|
|
64
|
+
const dragDropMode = $derived(config.dragDropMode);
|
|
65
|
+
const dropZoneMode = $derived(config.dropZoneMode);
|
|
66
|
+
const dropZoneStart = $derived(config.dropZoneStart);
|
|
67
|
+
const allowCopy = $derived(config.allowCopy);
|
|
71
68
|
|
|
72
69
|
// Compute if THIS node is the one being hovered for drop
|
|
73
70
|
const isHoveredForDrop = $derived(hoveredNodeForDropPath === node.path);
|
|
74
71
|
|
|
75
|
-
// Format dropZoneStart - number = percentage, string = as-is
|
|
76
|
-
const formattedDropZoneStart = $derived(
|
|
77
|
-
typeof dropZoneStart === 'number' ? `${dropZoneStart}%` : dropZoneStart
|
|
78
|
-
)
|
|
79
|
-
|
|
80
72
|
const tree = getContext<Ltree<T>>("Ltree")
|
|
81
73
|
const renderCoordinator = getContext<RenderCoordinator | null>("RenderCoordinator")
|
|
82
74
|
|
|
75
|
+
// Per-node reactive signal — each NodeSignal has its own $state, so
|
|
76
|
+
// bumping one signal only re-renders THIS Node, not all siblings.
|
|
77
|
+
const nodeSignal = tree.getNodeSignal(String(node.id));
|
|
78
|
+
const nodeRev = $derived(nodeSignal?.value ?? 0);
|
|
79
|
+
|
|
83
80
|
// Drag over state
|
|
84
81
|
let isDraggedOver = $state(false);
|
|
85
82
|
|
|
86
|
-
// Track which drop zone is being hovered during drag (for floating mode)
|
|
87
|
-
let hoveredZone = $state<'above' | 'below' | 'child' | null>(null);
|
|
88
|
-
|
|
89
83
|
// Track glow position for glow mode
|
|
90
84
|
let glowPosition = $state<'above' | 'below' | 'child' | null>(null);
|
|
91
85
|
|
|
@@ -112,7 +106,14 @@
|
|
|
112
106
|
|
|
113
107
|
// Calculate the ideal position based on mouse position
|
|
114
108
|
let idealPosition: DropPosition;
|
|
115
|
-
|
|
109
|
+
// Convert dropZoneStart to pixels: number = percentage, string = as-is (px or %)
|
|
110
|
+
const startPx = typeof dropZoneStart === 'number'
|
|
111
|
+
? (dropZoneStart / 100) * width
|
|
112
|
+
: dropZoneStart.endsWith('px')
|
|
113
|
+
? parseFloat(dropZoneStart)
|
|
114
|
+
: (parseFloat(dropZoneStart) / 100) * width;
|
|
115
|
+
|
|
116
|
+
if (x > startPx) {
|
|
116
117
|
idealPosition = 'child';
|
|
117
118
|
} else if (y < height / 2) {
|
|
118
119
|
idealPosition = 'above';
|
|
@@ -152,9 +153,14 @@
|
|
|
152
153
|
// In flat mode, children rendering is handled by Tree.svelte, so we skip these computations
|
|
153
154
|
const childrenArray = $derived(!flatMode ? Object.values(node?.children || []) : [])
|
|
154
155
|
const hasChildren = $derived(node?.hasChildren || false)
|
|
156
|
+
// In recursive mode, each nested Node compounds one level of margin-left.
|
|
157
|
+
// In flat mode, all nodes are siblings so we multiply level × indent explicitly.
|
|
158
|
+
// Both use the same CSS variable so theming works identically across modes.
|
|
159
|
+
// flatGap replicates the recursive .ltree-children { margin-top: 2px } gap
|
|
160
|
+
// — only applied before first-child nodes (where level > previous node's level).
|
|
155
161
|
const indentStyle = $derived(
|
|
156
162
|
flatMode
|
|
157
|
-
? `margin-left: calc(${
|
|
163
|
+
? `margin-left: calc(${node?.level || 1} * var(--tree-node-indent-per-level, 0.5rem))${flatGap ? '; margin-top: 2px' : ''}`
|
|
158
164
|
: `margin-left: var(--tree-node-indent-per-level, 0.5rem)`,
|
|
159
165
|
)
|
|
160
166
|
|
|
@@ -240,7 +246,8 @@
|
|
|
240
246
|
const newState = !node.isExpanded
|
|
241
247
|
uiLogger.debug(`${newState ? 'Expanding' : 'Collapsing'} node: ${node.path}`)
|
|
242
248
|
node.isExpanded = newState
|
|
243
|
-
tree.
|
|
249
|
+
tree.bumpNodeRev(node) // re-render expand/collapse icon via {#key nodeRev}
|
|
250
|
+
tree.refresh() // structural: recompute visibleFlatNodes
|
|
244
251
|
}
|
|
245
252
|
}
|
|
246
253
|
|
|
@@ -253,6 +260,7 @@
|
|
|
253
260
|
}
|
|
254
261
|
</script>
|
|
255
262
|
|
|
263
|
+
{#key nodeRev}
|
|
256
264
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
257
265
|
<div
|
|
258
266
|
class="ltree-node"
|
|
@@ -282,12 +290,12 @@
|
|
|
282
290
|
class="ltree-node-content {node.isSelected ? selectedNodeClass : ''} {isDraggedOver && dragOverNodeClass ? dragOverNodeClass : ''}"
|
|
283
291
|
class:ltree-clickable={node.isSelectable}
|
|
284
292
|
class:ltree-dragged={isDraggedNode}
|
|
285
|
-
class:ltree-draggable={node?.isDraggable}
|
|
293
|
+
class:ltree-draggable={node?.isDraggable && dragDropMode !== 'none'}
|
|
286
294
|
class:ltree-glow-above={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'above' && isPositionAllowed('above')}
|
|
287
295
|
class:ltree-glow-below={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'below' && isPositionAllowed('below')}
|
|
288
296
|
class:ltree-glow-child={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'child' && isPositionAllowed('child')}
|
|
289
297
|
class:ltree-drop-copy={isDragInProgress && isHoveredForDrop && dropOperation === 'copy'}
|
|
290
|
-
draggable={node?.isDraggable}
|
|
298
|
+
draggable={node?.isDraggable && dragDropMode !== 'none'}
|
|
291
299
|
onclick={(e) => {
|
|
292
300
|
e.stopPropagation();
|
|
293
301
|
_onNodeClicked();
|
|
@@ -297,7 +305,7 @@
|
|
|
297
305
|
callbacks.onNodeRightClicked(node, e);
|
|
298
306
|
}}
|
|
299
307
|
ondragstart={(e) => {
|
|
300
|
-
if (node?.isDraggable && e.dataTransfer) {
|
|
308
|
+
if (node?.isDraggable && dragDropMode !== 'none' && e.dataTransfer) {
|
|
301
309
|
e.dataTransfer.effectAllowed = allowCopy ? "copyMove" : "move";
|
|
302
310
|
e.dataTransfer.setData(
|
|
303
311
|
"application/svelte-treeview",
|
|
@@ -357,43 +365,6 @@
|
|
|
357
365
|
{tree.getNodeDisplayValue(node)}
|
|
358
366
|
{/if}
|
|
359
367
|
</div>
|
|
360
|
-
|
|
361
|
-
<!-- Drop zones: positioned relative to .ltree-node-row (outside content to avoid padding issues) -->
|
|
362
|
-
<!-- Only render floating drop zones when in 'floating' mode, filtered by allowedDropPositions -->
|
|
363
|
-
{#if dropZoneMode === 'floating' && isDragInProgress && isHoveredForDrop}
|
|
364
|
-
<div
|
|
365
|
-
class="ltree-drop-zones ltree-drop-zones-{dropZoneLayout}"
|
|
366
|
-
style="--drop-zone-start: {formattedDropZoneStart}; --drop-zone-max-width: {dropZoneMaxWidth}px;"
|
|
367
|
-
>
|
|
368
|
-
{#if isPositionAllowed('above')}
|
|
369
|
-
<div
|
|
370
|
-
class="ltree-drop-zone ltree-drop-above"
|
|
371
|
-
class:ltree-drop-zone-active={hoveredZone === 'above'}
|
|
372
|
-
ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'above'; callbacks.onNodeDragOver(node, e); }}
|
|
373
|
-
ondragleave={() => { hoveredZone = null; }}
|
|
374
|
-
ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'above', e); }}
|
|
375
|
-
>↑ Above</div>
|
|
376
|
-
{/if}
|
|
377
|
-
{#if isPositionAllowed('below')}
|
|
378
|
-
<div
|
|
379
|
-
class="ltree-drop-zone ltree-drop-below"
|
|
380
|
-
class:ltree-drop-zone-active={hoveredZone === 'below'}
|
|
381
|
-
ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'below'; callbacks.onNodeDragOver(node, e); }}
|
|
382
|
-
ondragleave={() => { hoveredZone = null; }}
|
|
383
|
-
ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'below', e); }}
|
|
384
|
-
>↓ Below</div>
|
|
385
|
-
{/if}
|
|
386
|
-
{#if isPositionAllowed('child')}
|
|
387
|
-
<div
|
|
388
|
-
class="ltree-drop-zone ltree-drop-child"
|
|
389
|
-
class:ltree-drop-zone-active={hoveredZone === 'child'}
|
|
390
|
-
ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = 'child'; callbacks.onNodeDragOver(node, e); }}
|
|
391
|
-
ondragleave={() => { hoveredZone = null; }}
|
|
392
|
-
ondrop={(e) => { e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move'; hoveredZone = null; callbacks.onZoneDrop(node, 'child', e); }}
|
|
393
|
-
>→ Child</div>
|
|
394
|
-
{/if}
|
|
395
|
-
</div>
|
|
396
|
-
{/if}
|
|
397
368
|
</div>
|
|
398
369
|
|
|
399
370
|
<!-- In flat mode, children are rendered by Tree.svelte, not recursively here -->
|
|
@@ -420,3 +391,4 @@
|
|
|
420
391
|
</div>
|
|
421
392
|
{/if}
|
|
422
393
|
</div>
|
|
394
|
+
{/key}
|