@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,357 @@
|
|
|
1
|
+
TYPESCRIPT TYPES
|
|
2
|
+
================
|
|
3
|
+
|
|
4
|
+
CRITICAL: Full TypeScript support
|
|
5
|
+
- Generic types for your data
|
|
6
|
+
- All props and methods typed
|
|
7
|
+
- Type exports from package
|
|
8
|
+
|
|
9
|
+
BASIC TYPED USAGE
|
|
10
|
+
-----------------
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { Tree } from '@keenmate/svelte-treeview';
|
|
13
|
+
import type { LTreeNode } from '@keenmate/svelte-treeview';
|
|
14
|
+
|
|
15
|
+
interface MyItem {
|
|
16
|
+
id: string;
|
|
17
|
+
path: string;
|
|
18
|
+
name: string;
|
|
19
|
+
type: 'file' | 'folder';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let data: MyItem[] = $state([]);
|
|
23
|
+
let selectedNode: LTreeNode<MyItem> | null = $state(null);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<Tree
|
|
27
|
+
{data}
|
|
28
|
+
idMember="id"
|
|
29
|
+
pathMember="path"
|
|
30
|
+
bind:selectedNode
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
TYPE EXPORTS
|
|
34
|
+
------------
|
|
35
|
+
import type {
|
|
36
|
+
// Core types
|
|
37
|
+
LTreeNode, // Tree node interface
|
|
38
|
+
Ltree, // Tree instance interface
|
|
39
|
+
DropPosition, // 'before' | 'after' | 'child'
|
|
40
|
+
|
|
41
|
+
// Context menu
|
|
42
|
+
ContextMenuItem, // Menu item interface
|
|
43
|
+
|
|
44
|
+
// Results
|
|
45
|
+
InsertArrayResult, // Insert operation result
|
|
46
|
+
|
|
47
|
+
// Internal (advanced)
|
|
48
|
+
NodeCallbacks, // Event callback types
|
|
49
|
+
NodeConfig // Node configuration
|
|
50
|
+
} from '@keenmate/svelte-treeview';
|
|
51
|
+
|
|
52
|
+
LTREENODE INTERFACE
|
|
53
|
+
-------------------
|
|
54
|
+
interface LTreeNode<T> {
|
|
55
|
+
// Path information
|
|
56
|
+
path: string; // Full path "1.2.3"
|
|
57
|
+
pathSegment: string; // Last segment "3"
|
|
58
|
+
parentPath: string | null; // Parent "1.2" or null
|
|
59
|
+
|
|
60
|
+
// Structure
|
|
61
|
+
level: number | null; // Depth (0 = root)
|
|
62
|
+
children: Record<string, LTreeNode<T>>;
|
|
63
|
+
hasChildren: boolean;
|
|
64
|
+
|
|
65
|
+
// Your data
|
|
66
|
+
data: T | null; // Original data object
|
|
67
|
+
|
|
68
|
+
// State
|
|
69
|
+
isExpanded: boolean;
|
|
70
|
+
isSelected: boolean;
|
|
71
|
+
|
|
72
|
+
// Drag and drop
|
|
73
|
+
isDraggable: boolean;
|
|
74
|
+
isDropAllowed: boolean;
|
|
75
|
+
allowedDropPositions: DropPosition[] | null | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
DROPPOSITION TYPE
|
|
79
|
+
-----------------
|
|
80
|
+
type DropPosition = 'before' | 'after' | 'child';
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
import type { DropPosition } from '@keenmate/svelte-treeview';
|
|
84
|
+
|
|
85
|
+
function getAllowedPositions(node: LTreeNode<MyItem>): DropPosition[] | null {
|
|
86
|
+
if (node.data?.type === 'file') {
|
|
87
|
+
return ['before', 'after'];
|
|
88
|
+
}
|
|
89
|
+
return null; // all positions
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
CONTEXTMENUITEM INTERFACE
|
|
93
|
+
-------------------------
|
|
94
|
+
interface ContextMenuItem {
|
|
95
|
+
icon?: string;
|
|
96
|
+
title: string;
|
|
97
|
+
isDisabled?: boolean;
|
|
98
|
+
callback: () => void | Promise<void>;
|
|
99
|
+
isDivider?: boolean;
|
|
100
|
+
className?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Usage:
|
|
104
|
+
function createMenu(node: LTreeNode<MyItem>, closeMenu: () => void): ContextMenuItem[] {
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
icon: '📂',
|
|
108
|
+
title: 'Open',
|
|
109
|
+
callback: () => openItem(node.data)
|
|
110
|
+
},
|
|
111
|
+
{ isDivider: true },
|
|
112
|
+
{
|
|
113
|
+
icon: '🗑️',
|
|
114
|
+
title: 'Delete',
|
|
115
|
+
isDisabled: node.data?.isProtected,
|
|
116
|
+
callback: () => deleteItem(node.data)
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
INSERTARRAYRESULT INTERFACE
|
|
122
|
+
---------------------------
|
|
123
|
+
interface InsertArrayResult<T> {
|
|
124
|
+
successful: number;
|
|
125
|
+
failed: Array<{
|
|
126
|
+
node: LTreeNode<T>;
|
|
127
|
+
originalData: T;
|
|
128
|
+
error: string;
|
|
129
|
+
}>;
|
|
130
|
+
total: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Usage:
|
|
134
|
+
let insertResult: InsertArrayResult<MyItem> | undefined = $state();
|
|
135
|
+
|
|
136
|
+
$effect(() => {
|
|
137
|
+
if (insertResult) {
|
|
138
|
+
console.log(`${insertResult.successful} succeeded`);
|
|
139
|
+
console.log(`${insertResult.failed.length} failed`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
LTREE INTERFACE
|
|
144
|
+
---------------
|
|
145
|
+
interface Ltree<T> {
|
|
146
|
+
// Properties
|
|
147
|
+
root: LTreeNode<T>;
|
|
148
|
+
pathSeparator: string;
|
|
149
|
+
flatTreeNodes: LTreeNode<T>[];
|
|
150
|
+
nodeCount: number;
|
|
151
|
+
maxLevel: number;
|
|
152
|
+
filteredNodeCount: number;
|
|
153
|
+
|
|
154
|
+
// Methods
|
|
155
|
+
insertArray(data: T[]): InsertArrayResult<T>;
|
|
156
|
+
getNodeByPath(path: string): LTreeNode<T> | null;
|
|
157
|
+
expandNodes(path: string): Ltree<T>;
|
|
158
|
+
collapseNodes(path: string): Ltree<T>;
|
|
159
|
+
expandAll(path?: string): void;
|
|
160
|
+
collapseAll(path?: string): void;
|
|
161
|
+
filterNodes(searchText: string): void;
|
|
162
|
+
searchNodes(searchText: string): LTreeNode<T>[];
|
|
163
|
+
|
|
164
|
+
// Callbacks
|
|
165
|
+
getNodeAllowedDropPositions(node: LTreeNode<T>): DropPosition[] | null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
CALLBACK TYPES
|
|
169
|
+
--------------
|
|
170
|
+
// Node click
|
|
171
|
+
onNodeClicked?: (node: LTreeNode<T>) => void;
|
|
172
|
+
|
|
173
|
+
// Drag events
|
|
174
|
+
onNodeDragStart?: (node: LTreeNode<T>, event: DragEvent) => void;
|
|
175
|
+
onNodeDragOver?: (node: LTreeNode<T>, event: DragEvent) => void;
|
|
176
|
+
onNodeDrop?: (
|
|
177
|
+
dropNode: LTreeNode<T> | null,
|
|
178
|
+
draggedNode: LTreeNode<T>,
|
|
179
|
+
position: DropPosition,
|
|
180
|
+
event: DragEvent,
|
|
181
|
+
operation: 'move' | 'copy'
|
|
182
|
+
) => void;
|
|
183
|
+
|
|
184
|
+
// Validation
|
|
185
|
+
beforeDropCallback?: (
|
|
186
|
+
dropNode: LTreeNode<T> | null,
|
|
187
|
+
draggedNode: LTreeNode<T>,
|
|
188
|
+
position: DropPosition,
|
|
189
|
+
event: DragEvent,
|
|
190
|
+
operation: 'move' | 'copy'
|
|
191
|
+
) => boolean | { position?: DropPosition; operation?: 'move' | 'copy' } | Promise<...>;
|
|
192
|
+
|
|
193
|
+
// Value getters
|
|
194
|
+
getDisplayValueCallback?: (node: LTreeNode<T>) => string;
|
|
195
|
+
getSearchValueCallback?: (node: LTreeNode<T>) => string;
|
|
196
|
+
getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null;
|
|
197
|
+
|
|
198
|
+
// Context menu
|
|
199
|
+
contextMenuCallback?: (
|
|
200
|
+
node: LTreeNode<T>,
|
|
201
|
+
closeMenu: () => void
|
|
202
|
+
) => ContextMenuItem[];
|
|
203
|
+
|
|
204
|
+
// Sort
|
|
205
|
+
sortCallback?: (nodes: LTreeNode<T>[]) => LTreeNode<T>[];
|
|
206
|
+
|
|
207
|
+
GENERIC TREE REFERENCE
|
|
208
|
+
----------------------
|
|
209
|
+
<script lang="ts">
|
|
210
|
+
import { Tree } from '@keenmate/svelte-treeview';
|
|
211
|
+
|
|
212
|
+
interface MyItem {
|
|
213
|
+
id: string;
|
|
214
|
+
path: string;
|
|
215
|
+
name: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let treeRef: Tree<MyItem>;
|
|
219
|
+
</script>
|
|
220
|
+
|
|
221
|
+
<Tree bind:this={treeRef} {data} ... />
|
|
222
|
+
|
|
223
|
+
<button onclick={() => {
|
|
224
|
+
// Fully typed methods
|
|
225
|
+
const node = treeRef.getNodeByPath('1.2');
|
|
226
|
+
if (node) {
|
|
227
|
+
console.log(node.data?.name); // string | undefined
|
|
228
|
+
}
|
|
229
|
+
}}>
|
|
230
|
+
Get Node
|
|
231
|
+
</button>
|
|
232
|
+
|
|
233
|
+
STATISTICS TYPE
|
|
234
|
+
---------------
|
|
235
|
+
interface Statistics {
|
|
236
|
+
nodeCount: number;
|
|
237
|
+
maxLevel: number;
|
|
238
|
+
filteredNodeCount: number;
|
|
239
|
+
isIndexing: boolean;
|
|
240
|
+
pendingIndexCount: number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const stats: Statistics = treeRef.statistics;
|
|
244
|
+
|
|
245
|
+
COMPLEX DATA TYPES
|
|
246
|
+
------------------
|
|
247
|
+
// Union types
|
|
248
|
+
interface FileItem {
|
|
249
|
+
id: string;
|
|
250
|
+
path: string;
|
|
251
|
+
name: string;
|
|
252
|
+
type: 'file';
|
|
253
|
+
size: number;
|
|
254
|
+
extension: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface FolderItem {
|
|
258
|
+
id: string;
|
|
259
|
+
path: string;
|
|
260
|
+
name: string;
|
|
261
|
+
type: 'folder';
|
|
262
|
+
childCount: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type TreeItem = FileItem | FolderItem;
|
|
266
|
+
|
|
267
|
+
// Type guards
|
|
268
|
+
function isFile(item: TreeItem): item is FileItem {
|
|
269
|
+
return item.type === 'file';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isFolder(item: TreeItem): item is FolderItem {
|
|
273
|
+
return item.type === 'folder';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Usage in callbacks
|
|
277
|
+
<Tree
|
|
278
|
+
data={items as TreeItem[]}
|
|
279
|
+
getDisplayValueCallback={(node) => {
|
|
280
|
+
if (!node.data) return '';
|
|
281
|
+
if (isFile(node.data)) {
|
|
282
|
+
return `${node.data.name} (${node.data.size}KB)`;
|
|
283
|
+
}
|
|
284
|
+
return `${node.data.name} (${node.data.childCount} items)`;
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
|
|
288
|
+
STRICT NULL CHECKS
|
|
289
|
+
------------------
|
|
290
|
+
Handle nullable data:
|
|
291
|
+
|
|
292
|
+
onNodeClicked={(node) => {
|
|
293
|
+
// node.data can be null
|
|
294
|
+
if (node.data) {
|
|
295
|
+
console.log(node.data.name);
|
|
296
|
+
}
|
|
297
|
+
}}
|
|
298
|
+
|
|
299
|
+
// Or with optional chaining
|
|
300
|
+
onNodeClicked={(node) => {
|
|
301
|
+
console.log(node.data?.name ?? 'Unknown');
|
|
302
|
+
}}
|
|
303
|
+
|
|
304
|
+
TYPE NARROWING IN TEMPLATES
|
|
305
|
+
---------------------------
|
|
306
|
+
<Tree {data}>
|
|
307
|
+
{#snippet nodeTemplate(node)}
|
|
308
|
+
{#if node.data}
|
|
309
|
+
<span>{node.data.name}</span>
|
|
310
|
+
{#if isFolder(node.data)}
|
|
311
|
+
<span class="count">({node.data.childCount})</span>
|
|
312
|
+
{/if}
|
|
313
|
+
{/if}
|
|
314
|
+
{/snippet}
|
|
315
|
+
</Tree>
|
|
316
|
+
|
|
317
|
+
EXTENDING TYPES
|
|
318
|
+
---------------
|
|
319
|
+
// Extend base node data
|
|
320
|
+
interface BaseItem {
|
|
321
|
+
id: string;
|
|
322
|
+
path: string;
|
|
323
|
+
name: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface ExtendedItem extends BaseItem {
|
|
327
|
+
createdAt: Date;
|
|
328
|
+
updatedAt: Date;
|
|
329
|
+
metadata: Record<string, unknown>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Use extended type
|
|
333
|
+
let data: ExtendedItem[] = $state.raw([]);
|
|
334
|
+
|
|
335
|
+
<Tree
|
|
336
|
+
{data}
|
|
337
|
+
idMember="id"
|
|
338
|
+
pathMember="path"
|
|
339
|
+
getDisplayValueCallback={(node) => {
|
|
340
|
+
const item = node.data;
|
|
341
|
+
if (!item) return '';
|
|
342
|
+
return `${item.name} (${item.createdAt.toLocaleDateString()})`;
|
|
343
|
+
}}
|
|
344
|
+
/>
|
|
345
|
+
|
|
346
|
+
BEST PRACTICES
|
|
347
|
+
--------------
|
|
348
|
+
✅ Define interface for your data type
|
|
349
|
+
✅ Use type imports (import type { ... })
|
|
350
|
+
✅ Handle null/undefined in callbacks
|
|
351
|
+
✅ Use type guards for union types
|
|
352
|
+
✅ Type tree reference with generic
|
|
353
|
+
|
|
354
|
+
❌ Don't use `any` type
|
|
355
|
+
❌ Don't ignore null checks on node.data
|
|
356
|
+
❌ Don't cast without type guards
|
|
357
|
+
❌ Don't forget generic parameter on Tree ref
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
import {getContext, onDestroy, type Snippet} from "svelte"
|
|
5
5
|
import type {Ltree, DropPosition, DropOperation} from "../ltree/types.js"
|
|
6
6
|
import type {RenderCoordinator} from "./RenderCoordinator.svelte.js"
|
|
7
|
-
import type {NodeCallbacks, NodeConfig} from "
|
|
7
|
+
import type {NodeCallbacks, NodeConfig} from "../core/TreeController.svelte.js"
|
|
8
8
|
import { uiLogger } from "../logger.js"
|
|
9
9
|
|
|
10
10
|
// Define component props interface
|
|
11
11
|
// Callbacks and config come from context, drag state comes as props
|
|
12
12
|
interface Props {
|
|
13
13
|
node: LTreeNode<T>;
|
|
14
|
-
children?: Snippet<[T]>; // Keep the general children slot for backward compatibility
|
|
14
|
+
children?: Snippet<[LTreeNode<T>]>; // Keep the general children slot for backward compatibility
|
|
15
15
|
|
|
16
16
|
// Progressive rendering
|
|
17
17
|
progressiveRender?: boolean;
|
|
@@ -54,17 +54,24 @@
|
|
|
54
54
|
const callbacks = getContext<NodeCallbacks<T>>('NodeCallbacks');
|
|
55
55
|
const config = getContext<NodeConfig>('NodeConfig');
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
// Destructure config for convenience.
|
|
58
|
+
// Works reactively because nodeConfig uses $state() (not .raw()) and is mutated
|
|
59
|
+
// in-place via Object.assign, so the proxy reference stays the same.
|
|
60
|
+
const {
|
|
61
|
+
shouldToggleOnNodeClick,
|
|
62
|
+
expandIconClass,
|
|
63
|
+
collapseIconClass,
|
|
64
|
+
leafIconClass,
|
|
65
|
+
selectedNodeClass,
|
|
66
|
+
dragOverNodeClass,
|
|
67
|
+
allowCopy,
|
|
68
|
+
} = config;
|
|
69
|
+
|
|
70
|
+
// Read dropZoneMode and dropZoneStart through the proxy each time (not
|
|
71
|
+
// destructured) so they stay reactive in flat mode where nodes are NOT
|
|
72
|
+
// recreated on config change.
|
|
65
73
|
const dropZoneMode = $derived(config.dropZoneMode);
|
|
66
74
|
const dropZoneStart = $derived(config.dropZoneStart);
|
|
67
|
-
const allowCopy = $derived(config.allowCopy);
|
|
68
75
|
|
|
69
76
|
// Compute if THIS node is the one being hovered for drop
|
|
70
77
|
const isHoveredForDrop = $derived(hoveredNodeForDropPath === node.path);
|
|
@@ -72,16 +79,11 @@
|
|
|
72
79
|
const tree = getContext<Ltree<T>>("Ltree")
|
|
73
80
|
const renderCoordinator = getContext<RenderCoordinator | null>("RenderCoordinator")
|
|
74
81
|
|
|
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
|
-
|
|
80
82
|
// Drag over state
|
|
81
83
|
let isDraggedOver = $state(false);
|
|
82
84
|
|
|
83
85
|
// Track glow position for glow mode
|
|
84
|
-
let glowPosition = $state<'
|
|
86
|
+
let glowPosition = $state<'before' | 'after' | 'child' | null>(null);
|
|
85
87
|
|
|
86
88
|
// Get allowed drop positions for this node (empty/undefined = all allowed)
|
|
87
89
|
// Uses tree.getNodeAllowedDropPositions which checks callback > member > node property
|
|
@@ -97,28 +99,32 @@
|
|
|
97
99
|
|
|
98
100
|
// Calculate glow position based on mouse position in the node row
|
|
99
101
|
// Respects allowedDropPositions - snaps to nearest allowed position
|
|
100
|
-
|
|
102
|
+
// Uses dropZoneStart to determine the child zone threshold
|
|
103
|
+
function calculateGlowPosition(event: DragEvent, element: HTMLElement): 'before' | 'after' | 'child' | null {
|
|
101
104
|
const rect = element.getBoundingClientRect();
|
|
102
105
|
const x = event.clientX - rect.left;
|
|
103
106
|
const y = event.clientY - rect.top;
|
|
104
107
|
const width = rect.width;
|
|
105
108
|
const height = rect.height;
|
|
106
109
|
|
|
107
|
-
// Calculate the ideal position based on mouse position
|
|
108
|
-
let idealPosition: DropPosition;
|
|
109
110
|
// Convert dropZoneStart to pixels: number = percentage, string = as-is (px or %)
|
|
110
111
|
const startPx = typeof dropZoneStart === 'number'
|
|
111
112
|
? (dropZoneStart / 100) * width
|
|
112
|
-
: dropZoneStart.endsWith('px')
|
|
113
|
+
: typeof dropZoneStart === 'string' && dropZoneStart.endsWith('px')
|
|
113
114
|
? parseFloat(dropZoneStart)
|
|
114
|
-
:
|
|
115
|
+
: typeof dropZoneStart === 'string'
|
|
116
|
+
? (parseFloat(dropZoneStart) / 100) * width
|
|
117
|
+
: width / 2;
|
|
118
|
+
const childThreshold = isNaN(startPx) ? width / 2 : startPx;
|
|
115
119
|
|
|
116
|
-
|
|
120
|
+
// Calculate the ideal position based on mouse position
|
|
121
|
+
let idealPosition: DropPosition;
|
|
122
|
+
if (x > childThreshold) {
|
|
117
123
|
idealPosition = 'child';
|
|
118
124
|
} else if (y < height / 2) {
|
|
119
|
-
idealPosition = '
|
|
125
|
+
idealPosition = 'before';
|
|
120
126
|
} else {
|
|
121
|
-
idealPosition = '
|
|
127
|
+
idealPosition = 'after';
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
// If no restrictions, return the ideal position
|
|
@@ -138,17 +144,20 @@
|
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
// Multiple positions allowed but not the ideal one
|
|
141
|
-
// For
|
|
147
|
+
// For before/after: pick based on Y position
|
|
142
148
|
// For child: pick based on what's available
|
|
143
|
-
if (allowedPositions.includes('
|
|
144
|
-
// Both
|
|
145
|
-
return y < height / 2 ? '
|
|
149
|
+
if (allowedPositions.includes('before') && allowedPositions.includes('after')) {
|
|
150
|
+
// Both before and after allowed, pick based on Y
|
|
151
|
+
return y < height / 2 ? 'before' : 'after';
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
// Return the first allowed position
|
|
149
155
|
return allowedPositions[0];
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
// Resolve isCollapsible via tree's resolution method (callback > member > node property)
|
|
159
|
+
const isCollapsible = $derived(tree.getNodeIsCollapsible(node));
|
|
160
|
+
|
|
152
161
|
// Convert reactive statements to derived values
|
|
153
162
|
// In flat mode, children rendering is handled by Tree.svelte, so we skip these computations
|
|
154
163
|
const childrenArray = $derived(!flatMode ? Object.values(node?.children || []) : [])
|
|
@@ -242,12 +251,11 @@
|
|
|
242
251
|
});
|
|
243
252
|
|
|
244
253
|
function toggleExpanded() {
|
|
245
|
-
if (node.hasChildren) {
|
|
254
|
+
if (node.hasChildren && isCollapsible) {
|
|
246
255
|
const newState = !node.isExpanded
|
|
247
256
|
uiLogger.debug(`${newState ? 'Expanding' : 'Collapsing'} node: ${node.path}`)
|
|
248
257
|
node.isExpanded = newState
|
|
249
|
-
tree.
|
|
250
|
-
tree.refresh() // structural: recompute visibleFlatNodes
|
|
258
|
+
tree.refresh()
|
|
251
259
|
}
|
|
252
260
|
}
|
|
253
261
|
|
|
@@ -260,7 +268,6 @@
|
|
|
260
268
|
}
|
|
261
269
|
</script>
|
|
262
270
|
|
|
263
|
-
{#key nodeRev}
|
|
264
271
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
265
272
|
<div
|
|
266
273
|
class="ltree-node"
|
|
@@ -271,7 +278,7 @@
|
|
|
271
278
|
<div class="ltree-node-row">
|
|
272
279
|
<!-- Toggle icon with its own click handler -->
|
|
273
280
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
274
|
-
{#if hasChildren}
|
|
281
|
+
{#if hasChildren && isCollapsible}
|
|
275
282
|
<span
|
|
276
283
|
class="ltree-toggle-icon ltree-clickable {node.isExpanded
|
|
277
284
|
? collapseIconClass
|
|
@@ -290,12 +297,12 @@
|
|
|
290
297
|
class="ltree-node-content {node.isSelected ? selectedNodeClass : ''} {isDraggedOver && dragOverNodeClass ? dragOverNodeClass : ''}"
|
|
291
298
|
class:ltree-clickable={node.isSelectable}
|
|
292
299
|
class:ltree-dragged={isDraggedNode}
|
|
293
|
-
class:ltree-draggable={node?.isDraggable
|
|
294
|
-
class:ltree-glow-
|
|
295
|
-
class:ltree-glow-
|
|
300
|
+
class:ltree-draggable={node?.isDraggable}
|
|
301
|
+
class:ltree-glow-before={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'before' && isPositionAllowed('before')}
|
|
302
|
+
class:ltree-glow-after={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'after' && isPositionAllowed('after')}
|
|
296
303
|
class:ltree-glow-child={dropZoneMode === 'glow' && isDragInProgress && isHoveredForDrop && glowPosition === 'child' && isPositionAllowed('child')}
|
|
297
304
|
class:ltree-drop-copy={isDragInProgress && isHoveredForDrop && dropOperation === 'copy'}
|
|
298
|
-
draggable={node?.isDraggable
|
|
305
|
+
draggable={node?.isDraggable}
|
|
299
306
|
onclick={(e) => {
|
|
300
307
|
e.stopPropagation();
|
|
301
308
|
_onNodeClicked();
|
|
@@ -305,7 +312,7 @@
|
|
|
305
312
|
callbacks.onNodeRightClicked(node, e);
|
|
306
313
|
}}
|
|
307
314
|
ondragstart={(e) => {
|
|
308
|
-
if (node?.isDraggable &&
|
|
315
|
+
if (node?.isDraggable && e.dataTransfer) {
|
|
309
316
|
e.dataTransfer.effectAllowed = allowCopy ? "copyMove" : "move";
|
|
310
317
|
e.dataTransfer.setData(
|
|
311
318
|
"application/svelte-treeview",
|
|
@@ -365,6 +372,7 @@
|
|
|
365
372
|
{tree.getNodeDisplayValue(node)}
|
|
366
373
|
{/if}
|
|
367
374
|
</div>
|
|
375
|
+
|
|
368
376
|
</div>
|
|
369
377
|
|
|
370
378
|
<!-- In flat mode, children are rendered by Tree.svelte, not recursively here -->
|
|
@@ -391,4 +399,3 @@
|
|
|
391
399
|
</div>
|
|
392
400
|
{/if}
|
|
393
401
|
</div>
|
|
394
|
-
{/key}
|
|
@@ -5,7 +5,7 @@ import type { DropPosition, DropOperation } from "../ltree/types.js";
|
|
|
5
5
|
declare function $$render<T>(): {
|
|
6
6
|
props: {
|
|
7
7
|
node: LTreeNode<T>;
|
|
8
|
-
children?: Snippet<[T]>;
|
|
8
|
+
children?: Snippet<[LTreeNode<T>]>;
|
|
9
9
|
progressiveRender?: boolean;
|
|
10
10
|
renderBatchSize?: number;
|
|
11
11
|
isDraggedNode?: boolean;
|