@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,336 @@
|
|
|
1
|
+
BASIC SETUP
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
CRITICAL: This is a Svelte 5 component using runes
|
|
5
|
+
- Requires Svelte 5.0.0 or higher
|
|
6
|
+
- Uses $state, $derived, $effect runes
|
|
7
|
+
- NOT compatible with Svelte 4 or earlier
|
|
8
|
+
- Works with SvelteKit, Vite, or any Svelte 5 setup
|
|
9
|
+
|
|
10
|
+
INSTALLATION
|
|
11
|
+
------------
|
|
12
|
+
npm install @keenmate/svelte-treeview
|
|
13
|
+
|
|
14
|
+
# Or yarn
|
|
15
|
+
yarn add @keenmate/svelte-treeview
|
|
16
|
+
|
|
17
|
+
# Or pnpm
|
|
18
|
+
pnpm add @keenmate/svelte-treeview
|
|
19
|
+
|
|
20
|
+
IMPORT STYLES
|
|
21
|
+
-------------
|
|
22
|
+
The component requires CSS to display correctly:
|
|
23
|
+
|
|
24
|
+
// In your main.js/main.ts or +layout.svelte
|
|
25
|
+
import '@keenmate/svelte-treeview/styles.css';
|
|
26
|
+
|
|
27
|
+
// Or SCSS (if you have sass configured)
|
|
28
|
+
import '@keenmate/svelte-treeview/styles.scss';
|
|
29
|
+
|
|
30
|
+
MINIMAL EXAMPLE
|
|
31
|
+
---------------
|
|
32
|
+
<script lang="ts">
|
|
33
|
+
import { Tree } from '@keenmate/svelte-treeview';
|
|
34
|
+
|
|
35
|
+
const data = [
|
|
36
|
+
{ id: '1', path: '1', name: 'Documents' },
|
|
37
|
+
{ id: '1.1', path: '1.1', name: 'Projects' },
|
|
38
|
+
{ id: '1.1.1', path: '1.1.1', name: 'Project A' },
|
|
39
|
+
{ id: '2', path: '2', name: 'Pictures' }
|
|
40
|
+
];
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<Tree
|
|
44
|
+
{data}
|
|
45
|
+
idMember="id"
|
|
46
|
+
pathMember="path"
|
|
47
|
+
displayValueMember="name"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
REQUIRED PROPS
|
|
51
|
+
--------------
|
|
52
|
+
These props are required for the tree to work:
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Description |
|
|
55
|
+
|------|------|-------------|
|
|
56
|
+
| data | T[] | Array of data objects |
|
|
57
|
+
| idMember | string | Property name for unique ID |
|
|
58
|
+
| pathMember | string | Property name for hierarchical path |
|
|
59
|
+
|
|
60
|
+
RECOMMENDED PROPS
|
|
61
|
+
-----------------
|
|
62
|
+
| Prop | Type | Default | Description |
|
|
63
|
+
|------|------|---------|-------------|
|
|
64
|
+
| displayValueMember | string | null | Property for display text |
|
|
65
|
+
| sortCallback | function | built-in | Custom sort function |
|
|
66
|
+
| expandLevel | number | 2 | Auto-expand nodes to this depth |
|
|
67
|
+
|
|
68
|
+
PATH STRUCTURE
|
|
69
|
+
--------------
|
|
70
|
+
CRITICAL: Data must use path-based hierarchy
|
|
71
|
+
|
|
72
|
+
The path defines the tree structure:
|
|
73
|
+
- Root level: "1", "2", "3"
|
|
74
|
+
- Second level: "1.1", "1.2", "2.1"
|
|
75
|
+
- Third level: "1.1.1", "1.2.1"
|
|
76
|
+
|
|
77
|
+
Parent path is derived from child path:
|
|
78
|
+
- "1.2.3" → parent is "1.2"
|
|
79
|
+
- "1.2" → parent is "1"
|
|
80
|
+
- "1" → no parent (root)
|
|
81
|
+
|
|
82
|
+
Custom separators:
|
|
83
|
+
<Tree treePathSeparator="/" ... />
|
|
84
|
+
// Paths: "1/2/3", "files/docs/readme"
|
|
85
|
+
|
|
86
|
+
BINDING SELECTED NODE
|
|
87
|
+
---------------------
|
|
88
|
+
<script>
|
|
89
|
+
let selectedNode = $state(null);
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<Tree
|
|
93
|
+
{data}
|
|
94
|
+
idMember="id"
|
|
95
|
+
pathMember="path"
|
|
96
|
+
bind:selectedNode
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{#if selectedNode}
|
|
100
|
+
<p>Selected: {selectedNode.data.name}</p>
|
|
101
|
+
{/if}
|
|
102
|
+
|
|
103
|
+
BINDING SEARCH TEXT
|
|
104
|
+
-------------------
|
|
105
|
+
<script>
|
|
106
|
+
let searchText = $state('');
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<input bind:value={searchText} placeholder="Search..." />
|
|
110
|
+
|
|
111
|
+
<Tree
|
|
112
|
+
{data}
|
|
113
|
+
idMember="id"
|
|
114
|
+
pathMember="path"
|
|
115
|
+
bind:searchText
|
|
116
|
+
shouldUseInternalSearchIndex={true}
|
|
117
|
+
searchValueMember="name"
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
CUSTOM NODE TEMPLATE
|
|
121
|
+
--------------------
|
|
122
|
+
<Tree
|
|
123
|
+
{data}
|
|
124
|
+
idMember="id"
|
|
125
|
+
pathMember="path"
|
|
126
|
+
>
|
|
127
|
+
{#snippet nodeTemplate(node)}
|
|
128
|
+
<div class="custom-node">
|
|
129
|
+
<span class="icon">{node.data.icon}</span>
|
|
130
|
+
<span class="name">{node.data.name}</span>
|
|
131
|
+
{#if node.data.count}
|
|
132
|
+
<span class="badge">{node.data.count}</span>
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
135
|
+
{/snippet}
|
|
136
|
+
</Tree>
|
|
137
|
+
|
|
138
|
+
IMPORTANT: The snippet name is nodeTemplate (not nodeContent)
|
|
139
|
+
|
|
140
|
+
EXPAND/COLLAPSE CONTROL
|
|
141
|
+
-----------------------
|
|
142
|
+
Control initial expansion:
|
|
143
|
+
|
|
144
|
+
<Tree
|
|
145
|
+
{data}
|
|
146
|
+
expandLevel={3} <!-- Expand first 3 levels -->
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
Programmatic control:
|
|
150
|
+
|
|
151
|
+
<script>
|
|
152
|
+
let treeRef;
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<Tree bind:this={treeRef} {data} ... />
|
|
156
|
+
|
|
157
|
+
<button onclick={() => treeRef.expandAll()}>Expand All</button>
|
|
158
|
+
<button onclick={() => treeRef.collapseAll()}>Collapse All</button>
|
|
159
|
+
<button onclick={() => treeRef.expandNodes('1.2')}>Expand 1.2</button>
|
|
160
|
+
|
|
161
|
+
CLICK HANDLING
|
|
162
|
+
--------------
|
|
163
|
+
<Tree
|
|
164
|
+
{data}
|
|
165
|
+
idMember="id"
|
|
166
|
+
pathMember="path"
|
|
167
|
+
onNodeClicked={(node) => {
|
|
168
|
+
console.log('Clicked:', node.data.name);
|
|
169
|
+
console.log('Path:', node.path);
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
Toggle on click (default true):
|
|
174
|
+
<Tree shouldToggleOnNodeClick={false} ... />
|
|
175
|
+
|
|
176
|
+
SCROLL TO NODE
|
|
177
|
+
--------------
|
|
178
|
+
<script>
|
|
179
|
+
let treeRef;
|
|
180
|
+
|
|
181
|
+
async function goToNode() {
|
|
182
|
+
await treeRef.scrollToPath('1.2.3', {
|
|
183
|
+
expand: true, // Expand parents first
|
|
184
|
+
highlight: true, // Highlight the node
|
|
185
|
+
containerScroll: true // Scroll only within container
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
</script>
|
|
189
|
+
|
|
190
|
+
<Tree bind:this={treeRef} {data} ... />
|
|
191
|
+
<button onclick={goToNode}>Go to 1.2.3</button>
|
|
192
|
+
|
|
193
|
+
TREE STATISTICS
|
|
194
|
+
---------------
|
|
195
|
+
<script>
|
|
196
|
+
let treeRef;
|
|
197
|
+
|
|
198
|
+
function showStats() {
|
|
199
|
+
const stats = treeRef.statistics;
|
|
200
|
+
console.log('Total nodes:', stats.nodeCount);
|
|
201
|
+
console.log('Max depth:', stats.maxLevel);
|
|
202
|
+
console.log('Filtered:', stats.filteredNodeCount);
|
|
203
|
+
console.log('Indexing:', stats.isIndexing);
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
DEBUG MODE
|
|
208
|
+
----------
|
|
209
|
+
Enable debug panel and console logging:
|
|
210
|
+
|
|
211
|
+
<Tree
|
|
212
|
+
{data}
|
|
213
|
+
shouldDisplayDebugInformation={true}
|
|
214
|
+
/>
|
|
215
|
+
|
|
216
|
+
Shows:
|
|
217
|
+
- Tree ID
|
|
218
|
+
- Node count
|
|
219
|
+
- Max depth
|
|
220
|
+
- Filtered count
|
|
221
|
+
- Indexing status
|
|
222
|
+
- Currently dragged node
|
|
223
|
+
|
|
224
|
+
EMPTY STATE
|
|
225
|
+
-----------
|
|
226
|
+
Custom message when tree is empty:
|
|
227
|
+
|
|
228
|
+
<Tree {data} ...>
|
|
229
|
+
{#snippet noDataFound()}
|
|
230
|
+
<div class="empty-state">
|
|
231
|
+
<p>No items found</p>
|
|
232
|
+
<button>Add First Item</button>
|
|
233
|
+
</div>
|
|
234
|
+
{/snippet}
|
|
235
|
+
</Tree>
|
|
236
|
+
|
|
237
|
+
LOADING STATE
|
|
238
|
+
-------------
|
|
239
|
+
Show a loading placeholder while data is being fetched:
|
|
240
|
+
|
|
241
|
+
<Tree {data} isLoading={isLoading}>
|
|
242
|
+
{#snippet loadingPlaceholder()}
|
|
243
|
+
<div class="loading">Loading tree data...</div>
|
|
244
|
+
{/snippet}
|
|
245
|
+
</Tree>
|
|
246
|
+
|
|
247
|
+
DROP PLACEHOLDER
|
|
248
|
+
----------------
|
|
249
|
+
Custom placeholder for empty trees that accept drops:
|
|
250
|
+
|
|
251
|
+
<Tree {data} dragDropMode="both">
|
|
252
|
+
{#snippet dropPlaceholder()}
|
|
253
|
+
<div class="drop-here">Drop items here to start</div>
|
|
254
|
+
{/snippet}
|
|
255
|
+
</Tree>
|
|
256
|
+
|
|
257
|
+
FRAMEWORK INTEGRATION
|
|
258
|
+
---------------------
|
|
259
|
+
SvelteKit:
|
|
260
|
+
// +page.svelte
|
|
261
|
+
<script>
|
|
262
|
+
import { Tree } from '@keenmate/svelte-treeview';
|
|
263
|
+
import '@keenmate/svelte-treeview/styles.css';
|
|
264
|
+
</script>
|
|
265
|
+
|
|
266
|
+
Vite + Svelte:
|
|
267
|
+
// App.svelte
|
|
268
|
+
<script>
|
|
269
|
+
import { Tree } from '@keenmate/svelte-treeview';
|
|
270
|
+
</script>
|
|
271
|
+
|
|
272
|
+
// main.js
|
|
273
|
+
import '@keenmate/svelte-treeview/styles.css';
|
|
274
|
+
|
|
275
|
+
COMMON SETUP PATTERNS
|
|
276
|
+
---------------------
|
|
277
|
+
File explorer:
|
|
278
|
+
<Tree
|
|
279
|
+
{data}
|
|
280
|
+
idMember="id"
|
|
281
|
+
pathMember="path"
|
|
282
|
+
displayValueMember="name"
|
|
283
|
+
expandLevel={1}
|
|
284
|
+
treePathSeparator="/"
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
Organization tree:
|
|
288
|
+
<Tree
|
|
289
|
+
{data}
|
|
290
|
+
idMember="employeeId"
|
|
291
|
+
pathMember="orgPath"
|
|
292
|
+
displayValueMember="fullName"
|
|
293
|
+
expandLevel={3}
|
|
294
|
+
/>
|
|
295
|
+
|
|
296
|
+
With search and drag-drop:
|
|
297
|
+
<Tree
|
|
298
|
+
{data}
|
|
299
|
+
idMember="id"
|
|
300
|
+
pathMember="path"
|
|
301
|
+
bind:searchText
|
|
302
|
+
shouldUseInternalSearchIndex={true}
|
|
303
|
+
searchValueMember="name"
|
|
304
|
+
dragOverNodeClass="ltree-dragover-glow"
|
|
305
|
+
onNodeDrop={handleDrop}
|
|
306
|
+
/>
|
|
307
|
+
|
|
308
|
+
BROWSER SUPPORT
|
|
309
|
+
---------------
|
|
310
|
+
Requires modern browsers:
|
|
311
|
+
- Chrome/Edge 88+
|
|
312
|
+
- Firefox 78+
|
|
313
|
+
- Safari 14+
|
|
314
|
+
|
|
315
|
+
No IE11 support (Svelte 5 requirement)
|
|
316
|
+
|
|
317
|
+
CRITICAL REMINDERS
|
|
318
|
+
------------------
|
|
319
|
+
✅ Import styles (CSS or SCSS)
|
|
320
|
+
✅ Use Svelte 5.0.0 or higher
|
|
321
|
+
✅ Use path-based hierarchical data
|
|
322
|
+
✅ Set idMember and pathMember
|
|
323
|
+
✅ Use $state.raw() for large datasets (1000+ items)
|
|
324
|
+
|
|
325
|
+
❌ Don't use with Svelte 4 or earlier
|
|
326
|
+
❌ Don't forget to import styles
|
|
327
|
+
❌ Don't use non-path-based data structures
|
|
328
|
+
❌ Don't use $state() for large datasets (causes slowdown)
|
|
329
|
+
|
|
330
|
+
NEXT STEPS
|
|
331
|
+
----------
|
|
332
|
+
- Custom data structure → data-handling.txt
|
|
333
|
+
- Add drag and drop → drag-drop.txt
|
|
334
|
+
- Add search → search-features.txt
|
|
335
|
+
- Add context menu → context-menu.txt
|
|
336
|
+
- Optimize performance → performance.txt
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
CONTEXT MENU
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
CRITICAL: Two approaches for context menus
|
|
5
|
+
- Callback-based (recommended): contextMenuCallback prop
|
|
6
|
+
- Snippet-based: contextMenu snippet
|
|
7
|
+
- Both support dynamic items, icons, disabled states
|
|
8
|
+
|
|
9
|
+
CALLBACK-BASED CONTEXT MENU
|
|
10
|
+
---------------------------
|
|
11
|
+
<script>
|
|
12
|
+
import type { ContextMenuItem, LTreeNode } from '@keenmate/svelte-treeview';
|
|
13
|
+
|
|
14
|
+
function createContextMenu(node: LTreeNode<MyItem>, closeMenuCallback: () => void): ContextMenuItem[] {
|
|
15
|
+
const items: ContextMenuItem[] = [];
|
|
16
|
+
|
|
17
|
+
// Basic item
|
|
18
|
+
items.push({
|
|
19
|
+
icon: '📂',
|
|
20
|
+
title: 'Open',
|
|
21
|
+
callback: () => alert(`Opening ${node.data.name}`)
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Conditional item
|
|
25
|
+
if (node.data.canEdit) {
|
|
26
|
+
items.push({
|
|
27
|
+
icon: '✏️',
|
|
28
|
+
title: 'Edit',
|
|
29
|
+
callback: () => editItem(node.data)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Divider
|
|
34
|
+
items.push({ isDivider: true });
|
|
35
|
+
|
|
36
|
+
// Disabled item
|
|
37
|
+
items.push({
|
|
38
|
+
icon: '🗑️',
|
|
39
|
+
title: 'Delete',
|
|
40
|
+
isDisabled: !node.data.canDelete,
|
|
41
|
+
callback: () => deleteItem(node.data)
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return items;
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<Tree
|
|
49
|
+
{data}
|
|
50
|
+
contextMenuCallback={createContextMenu}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
CONTEXT MENU ITEM INTERFACE
|
|
54
|
+
---------------------------
|
|
55
|
+
interface ContextMenuItem {
|
|
56
|
+
icon?: string; // Optional icon (emoji or text)
|
|
57
|
+
title: string; // Menu item text
|
|
58
|
+
isDisabled?: boolean; // Disable the item
|
|
59
|
+
callback: () => void | Promise<void>; // Action
|
|
60
|
+
isDivider?: boolean; // Render as divider
|
|
61
|
+
className?: string; // Custom CSS class
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
SNIPPET-BASED CONTEXT MENU
|
|
65
|
+
--------------------------
|
|
66
|
+
<Tree {data}>
|
|
67
|
+
{#snippet contextMenu(node, closeMenu)}
|
|
68
|
+
<div class="ltree-context-menu-item" onclick={() => {
|
|
69
|
+
openItem(node.data);
|
|
70
|
+
closeMenu();
|
|
71
|
+
}}>
|
|
72
|
+
📂 Open
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="ltree-context-menu-divider"></div>
|
|
76
|
+
|
|
77
|
+
<div
|
|
78
|
+
class="ltree-context-menu-item"
|
|
79
|
+
class:ltree-context-menu-item-disabled={!node.data.canDelete}
|
|
80
|
+
onclick={() => {
|
|
81
|
+
if (node.data.canDelete) {
|
|
82
|
+
deleteItem(node.data);
|
|
83
|
+
closeMenu();
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
🗑️ Delete
|
|
88
|
+
</div>
|
|
89
|
+
{/snippet}
|
|
90
|
+
</Tree>
|
|
91
|
+
|
|
92
|
+
POSITION OFFSET
|
|
93
|
+
---------------
|
|
94
|
+
Adjust menu position relative to cursor:
|
|
95
|
+
|
|
96
|
+
<Tree
|
|
97
|
+
contextMenuCallback={createMenu}
|
|
98
|
+
contextMenuXOffset={8} <!-- Horizontal offset (default: 8px) -->
|
|
99
|
+
contextMenuYOffset={0} <!-- Vertical offset (default: 0px) -->
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
CLOSE MENU CALLBACK
|
|
103
|
+
-------------------
|
|
104
|
+
Callback receives closeMenu function for programmatic control:
|
|
105
|
+
|
|
106
|
+
function createContextMenu(node, closeMenuCallback) {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
title: 'Action',
|
|
110
|
+
callback: async () => {
|
|
111
|
+
const success = await doSomething();
|
|
112
|
+
if (success) {
|
|
113
|
+
closeMenuCallback(); // Close only on success
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
<Tree contextMenuCallback={createContextMenu} />
|
|
121
|
+
|
|
122
|
+
ASYNC CALLBACKS
|
|
123
|
+
---------------
|
|
124
|
+
Menu item callbacks can be async:
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
title: 'Save to Server',
|
|
128
|
+
callback: async () => {
|
|
129
|
+
try {
|
|
130
|
+
await saveToServer(node.data);
|
|
131
|
+
showSuccess('Saved!');
|
|
132
|
+
} catch (error) {
|
|
133
|
+
showError('Failed to save');
|
|
134
|
+
// Menu stays open on error
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
CLOSE CONTEXT MENU PROGRAMMATICALLY
|
|
140
|
+
-----------------------------------
|
|
141
|
+
<script>
|
|
142
|
+
let treeRef;
|
|
143
|
+
|
|
144
|
+
function closeMenuExternally() {
|
|
145
|
+
treeRef.closeContextMenu();
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<Tree bind:this={treeRef} ... />
|
|
150
|
+
|
|
151
|
+
AUTO-CLOSE BEHAVIOR
|
|
152
|
+
-------------------
|
|
153
|
+
Context menu automatically closes on:
|
|
154
|
+
- Click outside the menu
|
|
155
|
+
- Scroll events (mouse wheel, scrollbar, touch)
|
|
156
|
+
- Another right-click
|
|
157
|
+
- Pressing Escape
|
|
158
|
+
- Window resize
|
|
159
|
+
|
|
160
|
+
CUSTOM ITEM STYLING
|
|
161
|
+
-------------------
|
|
162
|
+
Add custom classes to items:
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
title: 'Delete',
|
|
166
|
+
className: 'text-danger', // Your CSS class
|
|
167
|
+
callback: () => deleteItem()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Built-in CSS classes:
|
|
171
|
+
- .ltree-context-menu - Menu container
|
|
172
|
+
- .ltree-context-menu-item - Menu item
|
|
173
|
+
- .ltree-context-menu-item-disabled - Disabled item
|
|
174
|
+
- .ltree-context-menu-icon - Icon container
|
|
175
|
+
- .ltree-context-menu-divider - Divider line
|
|
176
|
+
|
|
177
|
+
CONDITIONAL MENUS
|
|
178
|
+
-----------------
|
|
179
|
+
Show different menus based on node type:
|
|
180
|
+
|
|
181
|
+
function createContextMenu(node, closeMenuCallback) {
|
|
182
|
+
if (node.data?.type === 'folder') {
|
|
183
|
+
return [
|
|
184
|
+
{ title: 'New File', callback: () => newFile(node) },
|
|
185
|
+
{ title: 'New Folder', callback: () => newFolder(node) },
|
|
186
|
+
{ isDivider: true },
|
|
187
|
+
{ title: 'Rename', callback: () => rename(node) },
|
|
188
|
+
{ title: 'Delete', callback: () => deleteFolder(node) }
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (node.data?.type === 'file') {
|
|
193
|
+
return [
|
|
194
|
+
{ title: 'Open', callback: () => openFile(node) },
|
|
195
|
+
{ title: 'Download', callback: () => downloadFile(node) },
|
|
196
|
+
{ isDivider: true },
|
|
197
|
+
{ title: 'Rename', callback: () => rename(node) },
|
|
198
|
+
{ title: 'Delete', callback: () => deleteFile(node) }
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return []; // No menu
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
ICONS
|
|
206
|
+
-----
|
|
207
|
+
Icons can be:
|
|
208
|
+
- Emoji: '📁', '✏️', '🗑️'
|
|
209
|
+
- Text: 'Edit', '[X]'
|
|
210
|
+
- Unicode: '\u{1F4C1}'
|
|
211
|
+
|
|
212
|
+
Example with emojis:
|
|
213
|
+
{
|
|
214
|
+
icon: '📂',
|
|
215
|
+
title: 'Open Folder',
|
|
216
|
+
callback: () => openFolder()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
DIVIDERS
|
|
220
|
+
--------
|
|
221
|
+
Add visual separation:
|
|
222
|
+
|
|
223
|
+
[
|
|
224
|
+
{ title: 'Cut', callback: () => cut() },
|
|
225
|
+
{ title: 'Copy', callback: () => copy() },
|
|
226
|
+
{ title: 'Paste', callback: () => paste() },
|
|
227
|
+
{ isDivider: true }, // <-- Divider here
|
|
228
|
+
{ title: 'Delete', callback: () => del() }
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
DISABLED ITEMS
|
|
232
|
+
--------------
|
|
233
|
+
Disable based on conditions:
|
|
234
|
+
|
|
235
|
+
{
|
|
236
|
+
title: 'Paste',
|
|
237
|
+
isDisabled: clipboard.isEmpty(),
|
|
238
|
+
callback: () => paste()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
title: 'Delete',
|
|
243
|
+
isDisabled: node.data.isReadOnly,
|
|
244
|
+
callback: () => deleteItem()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
DEBUG MODE
|
|
248
|
+
----------
|
|
249
|
+
For CSS development, show menu persistently:
|
|
250
|
+
|
|
251
|
+
<Tree
|
|
252
|
+
contextMenuCallback={createMenu}
|
|
253
|
+
shouldDisplayContextMenuInDebugMode={true}
|
|
254
|
+
shouldDisplayDebugInformation={true}
|
|
255
|
+
/>
|
|
256
|
+
|
|
257
|
+
Debug mode:
|
|
258
|
+
- Shows menu at fixed position (200px right, 100px down from tree)
|
|
259
|
+
- Uses second node (or first if only one)
|
|
260
|
+
- Menu stays visible for styling
|
|
261
|
+
|
|
262
|
+
COMMON PATTERNS
|
|
263
|
+
---------------
|
|
264
|
+
File explorer menu:
|
|
265
|
+
function createMenu(node, closeMenuCallback) {
|
|
266
|
+
const items = [];
|
|
267
|
+
|
|
268
|
+
if (node.data?.type === 'folder') {
|
|
269
|
+
items.push(
|
|
270
|
+
{ icon: '📄', title: 'New File', callback: () => newFile(node) },
|
|
271
|
+
{ icon: '📁', title: 'New Folder', callback: () => newFolder(node) }
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
items.push(
|
|
276
|
+
{ isDivider: true },
|
|
277
|
+
{ icon: '✏️', title: 'Rename', callback: () => rename(node) },
|
|
278
|
+
{ icon: '📋', title: 'Copy Path', callback: () => copyPath(node) },
|
|
279
|
+
{ isDivider: true },
|
|
280
|
+
{
|
|
281
|
+
icon: '🗑️',
|
|
282
|
+
title: 'Delete',
|
|
283
|
+
isDisabled: node.data.isSystem,
|
|
284
|
+
callback: () => deleteItem(node)
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return items;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
User management menu:
|
|
292
|
+
function createMenu(node, closeMenuCallback) {
|
|
293
|
+
const user = node.data;
|
|
294
|
+
return [
|
|
295
|
+
{ icon: '👤', title: 'View Profile', callback: () => viewProfile(user) },
|
|
296
|
+
{ icon: '✉️', title: 'Send Email', callback: () => sendEmail(user) },
|
|
297
|
+
{ isDivider: true },
|
|
298
|
+
{
|
|
299
|
+
icon: '🔒',
|
|
300
|
+
title: user.isActive ? 'Deactivate' : 'Activate',
|
|
301
|
+
callback: () => toggleActive(user)
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
icon: '🗑️',
|
|
305
|
+
title: 'Remove',
|
|
306
|
+
isDisabled: user.isAdmin,
|
|
307
|
+
callback: () => removeUser(user)
|
|
308
|
+
}
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
STYLING CONTEXT MENU
|
|
313
|
+
--------------------
|
|
314
|
+
Override default styles:
|
|
315
|
+
|
|
316
|
+
.ltree-context-menu {
|
|
317
|
+
background: #1a1a1a;
|
|
318
|
+
border: 1px solid #333;
|
|
319
|
+
border-radius: 8px;
|
|
320
|
+
padding: 4px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.ltree-context-menu-item {
|
|
324
|
+
padding: 8px 12px;
|
|
325
|
+
border-radius: 4px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.ltree-context-menu-item:hover {
|
|
329
|
+
background: #2a2a2a;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.ltree-context-menu-item-disabled {
|
|
333
|
+
opacity: 0.5;
|
|
334
|
+
cursor: not-allowed;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
BEST PRACTICES
|
|
338
|
+
--------------
|
|
339
|
+
✅ Use callback-based approach for complex menus
|
|
340
|
+
✅ Show icons for quick recognition
|
|
341
|
+
✅ Use dividers to group related actions
|
|
342
|
+
✅ Disable dangerous actions when not safe
|
|
343
|
+
✅ Close menu after successful action
|
|
344
|
+
✅ Handle async operations gracefully
|
|
345
|
+
|
|
346
|
+
❌ Don't create menus with too many items (>10)
|
|
347
|
+
❌ Don't forget to close menu after action
|
|
348
|
+
❌ Don't use blocking operations in callbacks
|
|
349
|
+
❌ Don't make all items look the same (use icons)
|