@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.
@@ -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
- flatIndentSize?: string; // CSS value for per-level indentation in flat mode
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
- flatIndentSize = '1.5rem',
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
- // Destructure config for convenience (these are stable references)
58
- const {
59
- shouldToggleOnNodeClick,
60
- expandIconClass,
61
- collapseIconClass,
62
- leafIconClass,
63
- selectedNodeClass,
64
- dragOverNodeClass,
65
- dropZoneMode,
66
- dropZoneLayout,
67
- dropZoneStart,
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
- if (x > width / 2) {
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(${(node?.level || 1) - 1} * ${flatIndentSize})`
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.refresh()
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}
@@ -14,7 +14,7 @@ declare function $$render<T>(): {
14
14
  activeDropPosition?: DropPosition | null;
15
15
  dropOperation?: DropOperation;
16
16
  flatMode?: boolean;
17
- flatIndentSize?: string;
17
+ flatGap?: boolean;
18
18
  };
19
19
  exports: {};
20
20
  bindings: "";