@keenmate/svelte-treeview 5.0.0-rc07 → 5.0.0-rc09
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/CHANGELOG.md +51 -0
- package/README.md +35 -44
- package/ai/basic-setup.txt +336 -339
- package/ai/import-patterns.txt +290 -271
- package/ai/styling-theming.txt +355 -354
- package/component-variables.manifest.json +142 -0
- package/dist/components/Node.svelte +36 -26
- package/dist/components/Tree.svelte +87 -15
- package/dist/components/Tree.svelte.d.ts +100 -17
- package/dist/constants.generated.d.ts +1 -1
- package/dist/constants.generated.js +1 -1
- package/dist/core/TreeController.svelte.d.ts +84 -18
- package/dist/core/TreeController.svelte.js +310 -96
- package/dist/index.d.ts +1 -1
- package/dist/ltree/ltree.svelte.d.ts +1 -1
- package/dist/ltree/ltree.svelte.js +180 -48
- package/dist/ltree/types.d.ts +18 -4
- package/dist/styles/_base.css +41 -0
- package/dist/styles/_checkbox.css +83 -0
- package/dist/styles/_context-menu.css +134 -0
- package/dist/styles/_debug.css +45 -0
- package/dist/styles/_drag-drop.css +174 -0
- package/dist/styles/_drop-zones.css +270 -0
- package/dist/styles/_loading.css +40 -0
- package/dist/styles/_node.css +60 -0
- package/dist/styles/_states.css +92 -0
- package/dist/styles/_toggle-icons.css +97 -0
- package/dist/styles/_variables.css +189 -0
- package/dist/styles/main.css +37 -0
- package/dist/styles.css +651 -470
- package/package.json +103 -102
- package/dist/styles/main.scss +0 -1074
- package/dist/styles.css.map +0 -1
- package/dist/styles.scss +0 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/keenmate/schemas/main/component-variables.schema.json",
|
|
3
|
+
"component": "@keenmate/svelte-treeview",
|
|
4
|
+
"prefix": "ltree",
|
|
5
|
+
"baseVariables": [
|
|
6
|
+
{ "name": "base-accent-color", "required": true, "usage": "Primary accent — selection, drop-zones, focus rings, debug panel. Tints derived via color-mix()." },
|
|
7
|
+
{ "name": "base-success-color", "required": false, "usage": "Drop-valid border and dragover highlight" },
|
|
8
|
+
{ "name": "base-danger-color", "required": false, "usage": "Drop-invalid border, danger context-menu items, scroll-highlight arrows" },
|
|
9
|
+
{ "name": "base-main-bg", "required": false, "usage": "Used for --ltree-light (context-menu hover tint, debug stat chips)" },
|
|
10
|
+
{ "name": "base-border-color", "required": true, "usage": "Default border color (checkbox, context-menu, spinner track)" },
|
|
11
|
+
{ "name": "base-hover-bg", "required": false, "usage": "Node row hover background" },
|
|
12
|
+
{ "name": "base-text-color-1", "required": true, "usage": "Primary body text color (node labels, context-menu items)" },
|
|
13
|
+
{ "name": "base-text-color-3", "required": false, "usage": "Muted text (path, toggle icon, loading 'more', node-path)" },
|
|
14
|
+
{ "name": "base-text-color-4", "required": false, "usage": "Subtle text (context-menu shortcut, submenu arrow, divider label)" },
|
|
15
|
+
{ "name": "base-text-color-on-accent", "required": false, "usage": "Text on accent backgrounds — checkbox checkmark color" },
|
|
16
|
+
{ "name": "base-input-bg", "required": false, "usage": "Checkbox background (unchecked)" },
|
|
17
|
+
{ "name": "base-dropdown-bg", "required": false, "usage": "Context-menu and submenu background" },
|
|
18
|
+
{ "name": "base-dropdown-box-shadow", "required": false, "usage": "Context-menu shadow" },
|
|
19
|
+
{ "name": "base-font-family", "required": false, "usage": "All text in component" },
|
|
20
|
+
{ "name": "base-font-size-xs", "required": false, "usage": "Small text — node path, context-menu shortcut, icon font-size (multiplier × --ltree-rem)" },
|
|
21
|
+
{ "name": "base-font-size-sm", "required": false, "usage": "Default node text and context-menu items (multiplier × --ltree-rem)" },
|
|
22
|
+
{ "name": "base-font-weight-medium", "required": false, "usage": "Node label weight" },
|
|
23
|
+
{ "name": "base-border-radius-sm", "required": false, "usage": "Checkbox, context-menu, node-content border radius (multiplier × --ltree-rem)" }
|
|
24
|
+
],
|
|
25
|
+
"componentVariables": [
|
|
26
|
+
{ "name": "ltree-rem", "category": "sizing", "usage": "Base sizing unit for proportional scaling (default 10px). Set to 1rem to scale with document font-size." },
|
|
27
|
+
|
|
28
|
+
{ "name": "ltree-primary", "category": "color", "usage": "Primary accent color (selection, focus, links). Tints derived via color-mix()." },
|
|
29
|
+
{ "name": "ltree-success", "category": "color", "usage": "Success color (drop-valid border, dragover-highlight)" },
|
|
30
|
+
{ "name": "ltree-danger", "category": "color", "usage": "Danger color (drop-invalid, scroll-highlight arrows, danger menu items)" },
|
|
31
|
+
{ "name": "ltree-light", "category": "color", "usage": "Light surface (context-menu hover, debug stat chips)" },
|
|
32
|
+
{ "name": "ltree-border", "category": "color", "usage": "Default border color" },
|
|
33
|
+
{ "name": "ltree-body-color", "category": "color", "usage": "Default body text color" },
|
|
34
|
+
|
|
35
|
+
{ "name": "ltree-font-family", "category": "typography", "usage": "Font family for all tree text" },
|
|
36
|
+
{ "name": "ltree-node-font-size", "category": "typography", "usage": "Node row text size" },
|
|
37
|
+
{ "name": "ltree-node-icon-font-size", "category": "typography", "usage": "Per-node icon font size (emoji, etc.)" },
|
|
38
|
+
{ "name": "ltree-node-icon-margin-right", "category": "spacing", "usage": "Gap between node icon and label" },
|
|
39
|
+
{ "name": "ltree-node-label-font-weight", "category": "typography", "usage": "Node label weight" },
|
|
40
|
+
{ "name": "ltree-node-label-margin-right", "category": "spacing", "usage": "Gap after node label" },
|
|
41
|
+
{ "name": "ltree-node-path-font-size", "category": "typography", "usage": "Path / debug muted text size" },
|
|
42
|
+
{ "name": "ltree-node-path-color", "category": "color", "usage": "Path / debug muted text color" },
|
|
43
|
+
|
|
44
|
+
{ "name": "ltree-node-indent-per-level", "category": "spacing", "usage": "Indentation added per nesting level" },
|
|
45
|
+
{ "name": "ltree-node-content-padding", "category": "spacing", "usage": "Inner padding of a node row" },
|
|
46
|
+
{ "name": "ltree-node-content-border-radius", "category": "border", "usage": "Node row corner rounding" },
|
|
47
|
+
{ "name": "ltree-node-hover-bg", "category": "surface", "usage": "Node row hover background" },
|
|
48
|
+
{ "name": "ltree-children-margin-top", "category": "spacing", "usage": "Top margin of nested child list" },
|
|
49
|
+
|
|
50
|
+
{ "name": "ltree-toggle-icon-size", "category": "sizing", "usage": "Toggle icon (chevron / +/- / arrow / leaf) SVG size" },
|
|
51
|
+
{ "name": "ltree-toggle-icon-width", "category": "sizing", "usage": "Width reserved for the toggle column" },
|
|
52
|
+
{ "name": "ltree-toggle-icon-color", "category": "color", "usage": "Toggle icon color (via currentColor mask)" },
|
|
53
|
+
{ "name": "ltree-toggle-icon-margin-right", "category": "spacing", "usage": "Gap between toggle and node content" },
|
|
54
|
+
{ "name": "ltree-toggle-icon-transition", "category": "animation", "usage": "Rotation animation timing" },
|
|
55
|
+
|
|
56
|
+
{ "name": "ltree-checkbox-size", "category": "sizing", "usage": "Checkbox square dimension" },
|
|
57
|
+
{ "name": "ltree-checkbox-border-width", "category": "border", "usage": "Checkbox border thickness (hairline, does not scale)" },
|
|
58
|
+
{ "name": "ltree-checkbox-border-color", "category": "color", "usage": "Unchecked checkbox border" },
|
|
59
|
+
{ "name": "ltree-checkbox-border-radius", "category": "border", "usage": "Checkbox corner rounding" },
|
|
60
|
+
{ "name": "ltree-checkbox-bg", "category": "surface", "usage": "Unchecked checkbox background" },
|
|
61
|
+
{ "name": "ltree-checkbox-checked-bg", "category": "surface", "usage": "Checked / indeterminate checkbox background" },
|
|
62
|
+
{ "name": "ltree-checkbox-checked-border-color", "category": "color", "usage": "Checked checkbox border" },
|
|
63
|
+
{ "name": "ltree-checkbox-checkmark-color", "category": "color", "usage": "Tick / dash glyph color" },
|
|
64
|
+
{ "name": "ltree-checkbox-focus-ring-width", "category": "border", "usage": "Focus-visible ring thickness" },
|
|
65
|
+
{ "name": "ltree-checkbox-focus-ring-color", "category": "color", "usage": "Focus-visible ring color (defaults to a 25% tint of --ltree-primary)" },
|
|
66
|
+
{ "name": "ltree-checkbox-focus-ring", "category": "border", "usage": "Composed focus-visible box-shadow (uses the two parts above; override directly for full control)" },
|
|
67
|
+
|
|
68
|
+
{ "name": "ltree-highlight-bg", "category": "state", "usage": "Explorer-style highlighted node background" },
|
|
69
|
+
{ "name": "ltree-highlight-color", "category": "state", "usage": "Highlighted node text color" },
|
|
70
|
+
{ "name": "ltree-multi-selected-bg", "category": "state", "usage": "Multi-select tint background" },
|
|
71
|
+
{ "name": "ltree-multi-selected-outline", "category": "state", "usage": "Multi-select outline" },
|
|
72
|
+
|
|
73
|
+
{ "name": "ltree-dragover-bg", "category": "state", "usage": "Drag-over background tint" },
|
|
74
|
+
{ "name": "ltree-dragover-shadow", "category": "state", "usage": "Drag-over glow shadow" },
|
|
75
|
+
{ "name": "ltree-drop-placeholder-bg", "category": "state", "usage": "Empty-tree drop placeholder background" },
|
|
76
|
+
{ "name": "ltree-drop-placeholder-color", "category": "state", "usage": "Placeholder text color" },
|
|
77
|
+
{ "name": "ltree-drop-placeholder-border-radius", "category": "border", "usage": "Placeholder corner rounding" },
|
|
78
|
+
{ "name": "ltree-drop-placeholder-min-height", "category": "sizing", "usage": "Placeholder min height" },
|
|
79
|
+
|
|
80
|
+
{ "name": "ltree-drop-zone-border-radius", "category": "border", "usage": "Drop zone corner rounding" },
|
|
81
|
+
{ "name": "ltree-drop-zone-before-bg", "category": "drop-zone", "usage": "Before zone resting background (sage)" },
|
|
82
|
+
{ "name": "ltree-drop-zone-before-color", "category": "drop-zone", "usage": "Before zone resting text" },
|
|
83
|
+
{ "name": "ltree-drop-zone-before-active-bg", "category": "drop-zone", "usage": "Before zone active background" },
|
|
84
|
+
{ "name": "ltree-drop-zone-before-active-color", "category": "drop-zone", "usage": "Before zone active text" },
|
|
85
|
+
{ "name": "ltree-drop-zone-before-active-shadow", "category": "drop-zone", "usage": "Before zone active shadow" },
|
|
86
|
+
{ "name": "ltree-drop-zone-after-bg", "category": "drop-zone", "usage": "After zone resting background (coral)" },
|
|
87
|
+
{ "name": "ltree-drop-zone-after-color", "category": "drop-zone", "usage": "After zone resting text" },
|
|
88
|
+
{ "name": "ltree-drop-zone-after-active-bg", "category": "drop-zone", "usage": "After zone active background" },
|
|
89
|
+
{ "name": "ltree-drop-zone-after-active-color", "category": "drop-zone", "usage": "After zone active text" },
|
|
90
|
+
{ "name": "ltree-drop-zone-after-active-shadow", "category": "drop-zone", "usage": "After zone active shadow" },
|
|
91
|
+
{ "name": "ltree-drop-zone-child-bg", "category": "drop-zone", "usage": "Child zone resting background (lavender)" },
|
|
92
|
+
{ "name": "ltree-drop-zone-child-color", "category": "drop-zone", "usage": "Child zone resting text" },
|
|
93
|
+
{ "name": "ltree-drop-zone-child-active-bg", "category": "drop-zone", "usage": "Child zone active background" },
|
|
94
|
+
{ "name": "ltree-drop-zone-child-active-color", "category": "drop-zone", "usage": "Child zone active text" },
|
|
95
|
+
{ "name": "ltree-drop-zone-child-active-shadow", "category": "drop-zone", "usage": "Child zone active shadow" },
|
|
96
|
+
|
|
97
|
+
{ "name": "ltree-drop-glow-before-color", "category": "drop-zone", "usage": "Border glow color for 'before' position" },
|
|
98
|
+
{ "name": "ltree-drop-glow-after-color", "category": "drop-zone", "usage": "Border glow color for 'after' position" },
|
|
99
|
+
{ "name": "ltree-drop-glow-child-color", "category": "drop-zone", "usage": "Border glow color for 'child' position" },
|
|
100
|
+
{ "name": "ltree-drop-glow-child-bg", "category": "drop-zone", "usage": "Child zone glow background tint" },
|
|
101
|
+
{ "name": "ltree-drop-glow-size", "category": "border", "usage": "Glow border thickness (hairline, does not scale)" },
|
|
102
|
+
{ "name": "ltree-drop-arrow-size", "category": "sizing", "usage": "Direction arrow size in glow mode" },
|
|
103
|
+
{ "name": "ltree-drop-arrow-position", "category": "spacing", "usage": "Horizontal arrow position (% of node width)" },
|
|
104
|
+
{ "name": "ltree-drop-arrow-before-rotation", "category": "animation", "usage": "Arrow rotation for 'before' position" },
|
|
105
|
+
{ "name": "ltree-drop-arrow-after-rotation", "category": "animation", "usage": "Arrow rotation for 'after' position" },
|
|
106
|
+
{ "name": "ltree-drop-arrow-child-rotation", "category": "animation", "usage": "Arrow rotation for 'child' position (45deg by default)" },
|
|
107
|
+
{ "name": "ltree-drop-arrow-before-image", "category": "icon", "usage": "SVG data URL for the 'before' direction arrow" },
|
|
108
|
+
{ "name": "ltree-drop-arrow-after-image", "category": "icon", "usage": "SVG data URL for the 'after' direction arrow" },
|
|
109
|
+
{ "name": "ltree-drop-arrow-child-image", "category": "icon", "usage": "SVG data URL for the 'child' direction arrow" },
|
|
110
|
+
|
|
111
|
+
{ "name": "tree-ghost-bg", "category": "state", "usage": "Touch drag ghost background (legacy prefix, kept for backward compat)" },
|
|
112
|
+
{ "name": "tree-ghost-color", "category": "state", "usage": "Touch drag ghost text color (legacy prefix)" },
|
|
113
|
+
|
|
114
|
+
{ "name": "ltree-context-menu-bg", "category": "context-menu", "usage": "Menu background" },
|
|
115
|
+
{ "name": "ltree-context-menu-border-color", "category": "context-menu", "usage": "Menu border color" },
|
|
116
|
+
{ "name": "ltree-context-menu-border-radius", "category": "border", "usage": "Menu corner rounding" },
|
|
117
|
+
{ "name": "ltree-context-menu-shadow", "category": "context-menu", "usage": "Menu drop shadow" },
|
|
118
|
+
{ "name": "ltree-context-menu-min-width", "category": "sizing", "usage": "Menu minimum width" },
|
|
119
|
+
{ "name": "ltree-context-menu-padding", "category": "spacing", "usage": "Menu inner padding" },
|
|
120
|
+
{ "name": "ltree-context-menu-item-padding", "category": "spacing", "usage": "Menu item padding" },
|
|
121
|
+
{ "name": "ltree-context-menu-item-font-size", "category": "typography", "usage": "Menu item font size" },
|
|
122
|
+
{ "name": "ltree-context-menu-item-color", "category": "color", "usage": "Menu item text color" },
|
|
123
|
+
{ "name": "ltree-context-menu-item-hover-bg", "category": "surface", "usage": "Menu item hover background" },
|
|
124
|
+
{ "name": "ltree-context-menu-icon-size", "category": "sizing", "usage": "Menu icon column width" },
|
|
125
|
+
{ "name": "ltree-context-menu-icon-font-size", "category": "typography", "usage": "Menu icon font size" },
|
|
126
|
+
{ "name": "ltree-context-menu-shortcut-color", "category": "color", "usage": "Keyboard shortcut hint color" },
|
|
127
|
+
{ "name": "ltree-context-menu-shortcut-font-size", "category": "typography", "usage": "Keyboard shortcut hint font size" },
|
|
128
|
+
{ "name": "ltree-context-menu-arrow-color", "category": "color", "usage": "Submenu arrow color" },
|
|
129
|
+
{ "name": "ltree-context-menu-divider-color", "category": "color", "usage": "Divider line color" },
|
|
130
|
+
{ "name": "ltree-context-menu-divider-label-color", "category": "color", "usage": "Named divider label color" },
|
|
131
|
+
|
|
132
|
+
{ "name": "ltree-loading-bg", "category": "surface", "usage": "Loading overlay background" },
|
|
133
|
+
{ "name": "ltree-loading-color", "category": "color", "usage": "'Loading more…' text color" },
|
|
134
|
+
{ "name": "ltree-spinner-size", "category": "sizing", "usage": "Spinner diameter" },
|
|
135
|
+
{ "name": "ltree-spinner-track", "category": "color", "usage": "Spinner background ring color" },
|
|
136
|
+
{ "name": "ltree-spinner-color", "category": "color", "usage": "Spinner active color" },
|
|
137
|
+
|
|
138
|
+
{ "name": "ltree-scroll-highlight-bg", "category": "state", "usage": "scrollToPath highlight background flash" },
|
|
139
|
+
{ "name": "ltree-scroll-highlight-shadow", "category": "state", "usage": "scrollToPath highlight glow" },
|
|
140
|
+
{ "name": "ltree-scroll-highlight-arrow-color", "category": "state", "usage": "scrollToPath direction arrow color" }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
@@ -54,21 +54,21 @@
|
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} = config;
|
|
57
|
+
// Read every config field through $derived so updates from controller.update()
|
|
58
|
+
// or the controller's runtime $effect syncs propagate into this Node's render.
|
|
59
|
+
// Plain destructuring would snapshot primitives once and never react.
|
|
60
|
+
const expandIconClass = $derived(config.expandIconClass);
|
|
61
|
+
const collapseIconClass = $derived(config.collapseIconClass);
|
|
62
|
+
const leafIconClass = $derived(config.leafIconClass);
|
|
63
|
+
const highlightedNodeClass = $derived(config.highlightedNodeClass);
|
|
64
|
+
const focusedNodeClass = $derived(config.focusedNodeClass);
|
|
65
|
+
// dragOverNodeClass is applied to the DOM directly by the controller
|
|
66
|
+
// (see hoveredNodeForDrop $effect in TreeController) — no per-Node binding.
|
|
67
|
+
const allowCopy = $derived(config.allowCopy);
|
|
69
68
|
const clickBehavior = $derived(config.clickBehavior);
|
|
70
69
|
const showCheckboxes = $derived(config.showCheckboxes);
|
|
71
70
|
const checkboxMode = $derived(config.checkboxMode);
|
|
71
|
+
const clickTogglesCheckbox = $derived(config.clickTogglesCheckbox);
|
|
72
72
|
|
|
73
73
|
// Indeterminate state: driven by controller's _updateAncestorVisualStates
|
|
74
74
|
const isIndeterminate = $derived(checkboxMode === 'cascade' && node.visualState === 'indeterminate');
|
|
@@ -81,15 +81,15 @@
|
|
|
81
81
|
const accordionExpand = $derived(config.accordionExpand);
|
|
82
82
|
const toggleIconMode = $derived(config.toggleIconMode);
|
|
83
83
|
|
|
84
|
-
// Compute if THIS node is the one being hovered for drop
|
|
84
|
+
// Compute if THIS node is the one being hovered for drop.
|
|
85
|
+
// Single source of truth from the controller — guarantees exactly one highlighted
|
|
86
|
+
// row at a time, unlike the per-node local flag we used before (HTML5 dragleave
|
|
87
|
+
// is unreliable and leaked stale highlights when moving fast between rows).
|
|
85
88
|
const isHoveredForDrop = $derived(hoveredNodeForDropPath === node.path);
|
|
86
89
|
|
|
87
90
|
const tree = getContext<Ltree<T>>("Ltree")
|
|
88
91
|
const renderCoordinator = getContext<RenderCoordinator | null>("RenderCoordinator")
|
|
89
92
|
|
|
90
|
-
// Drag over state
|
|
91
|
-
let isDraggedOver = $state(false);
|
|
92
|
-
|
|
93
93
|
// Track glow position for glow mode
|
|
94
94
|
let glowPosition = $state<'before' | 'after' | 'child' | null>(null);
|
|
95
95
|
|
|
@@ -177,8 +177,8 @@
|
|
|
177
177
|
// — only applied before first-child nodes (where level > previous node's level).
|
|
178
178
|
const indentStyle = $derived(
|
|
179
179
|
flatMode
|
|
180
|
-
? `margin-left: calc(${node?.level || 1} * var(--
|
|
181
|
-
: `margin-left: var(--
|
|
180
|
+
? `margin-left: calc(${node?.level || 1} * var(--ltree-node-indent-per-level, 0.5rem))${flatGap ? '; margin-top: 2px' : ''}`
|
|
181
|
+
: `margin-left: var(--ltree-node-indent-per-level, 0.5rem)`,
|
|
182
182
|
)
|
|
183
183
|
|
|
184
184
|
// Progressive rendering state - only used in recursive mode
|
|
@@ -296,14 +296,27 @@
|
|
|
296
296
|
function _onNodeClicked(event?: MouseEvent) {
|
|
297
297
|
uiLogger.debug(`Node clicked: ${node.path}`, { id: node.id, hasChildren: node.hasChildren })
|
|
298
298
|
const modifiers = event ? { ctrl: event.ctrlKey || event.metaKey, shift: event.shiftKey } : undefined;
|
|
299
|
+
const hasModifiers = !!(modifiers?.ctrl || modifiers?.shift);
|
|
300
|
+
|
|
301
|
+
// Plain click on a selectable node with checkboxes shown → toggle the checkbox
|
|
302
|
+
// instead of focusing/highlighting. Expand still happens if clickBehavior asks for it.
|
|
303
|
+
// Modified clicks (Ctrl/Shift) fall through to the normal multi-highlight path.
|
|
304
|
+
if (clickTogglesCheckbox && showCheckboxes && node.isSelectable && !hasModifiers) {
|
|
305
|
+
callbacks.onCheckboxToggle(node, { skipFocus: true });
|
|
306
|
+
if (clickBehavior !== 'select') toggleExpanded();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
299
309
|
|
|
300
310
|
if (clickBehavior === 'expand') {
|
|
301
|
-
// Expand only — no selection callback
|
|
302
|
-
|
|
311
|
+
// Expand only — no selection callback. Ctrl/Shift have no meaning here
|
|
312
|
+
// (no selection to extend), so they shouldn't toggle expand either.
|
|
313
|
+
if (!hasModifiers) toggleExpanded()
|
|
303
314
|
} else if (clickBehavior === 'expand-and-focus') {
|
|
304
|
-
// Select + expand on single click
|
|
315
|
+
// Select + expand on single click. Modified clicks are reserved for
|
|
316
|
+
// highlight management — don't also toggle expand (matches OS file
|
|
317
|
+
// explorers where Ctrl/Shift+click manages selection without opening).
|
|
305
318
|
callbacks.onNodeClicked(node, modifiers)
|
|
306
|
-
toggleExpanded()
|
|
319
|
+
if (!hasModifiers) toggleExpanded()
|
|
307
320
|
} else {
|
|
308
321
|
// 'select' — single click selects only
|
|
309
322
|
callbacks.onNodeClicked(node, modifiers)
|
|
@@ -365,7 +378,7 @@
|
|
|
365
378
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
366
379
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
367
380
|
<div
|
|
368
|
-
class="ltree-node-content {node.isHighlighted ? highlightedNodeClass : ''} {node.isFocused && focusedNodeClass ? focusedNodeClass : ''}
|
|
381
|
+
class="ltree-node-content {node.isHighlighted ? highlightedNodeClass : ''} {node.isFocused && focusedNodeClass ? focusedNodeClass : ''}"
|
|
369
382
|
class:ltree-clickable={node.isSelectable}
|
|
370
383
|
class:ltree-dragged={isDraggedNode}
|
|
371
384
|
class:ltree-draggable={node?.isDraggable}
|
|
@@ -403,7 +416,6 @@
|
|
|
403
416
|
if (e.dataTransfer) {
|
|
404
417
|
e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move';
|
|
405
418
|
}
|
|
406
|
-
isDraggedOver = true;
|
|
407
419
|
// In glow mode, calculate and update the glow position
|
|
408
420
|
if (dropZoneMode === 'glow') {
|
|
409
421
|
glowPosition = calculateGlowPosition(e, e.currentTarget as HTMLElement);
|
|
@@ -417,7 +429,6 @@
|
|
|
417
429
|
const y = e.clientY;
|
|
418
430
|
|
|
419
431
|
if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
|
|
420
|
-
isDraggedOver = false;
|
|
421
432
|
glowPosition = null;
|
|
422
433
|
callbacks.onNodeDragLeave(node, e);
|
|
423
434
|
}
|
|
@@ -428,7 +439,6 @@
|
|
|
428
439
|
if (e.dataTransfer) {
|
|
429
440
|
e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move';
|
|
430
441
|
}
|
|
431
|
-
isDraggedOver = false;
|
|
432
442
|
// In glow mode, use the calculated glowPosition for the drop
|
|
433
443
|
if (dropZoneMode === 'glow' && glowPosition) {
|
|
434
444
|
callbacks.onZoneDrop(node, glowPosition, e);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
type DropOperation,
|
|
13
13
|
type ClickBehavior,
|
|
14
14
|
type CheckboxMode,
|
|
15
|
+
type SelectionMode,
|
|
15
16
|
type TreeChange,
|
|
16
17
|
type ApplyChangesResult
|
|
17
18
|
} from '../ltree/types.js';
|
|
@@ -31,8 +32,11 @@
|
|
|
31
32
|
parentPathMember?: string | null | undefined;
|
|
32
33
|
levelMember?: string | null | undefined;
|
|
33
34
|
isExpandedMember?: string | null | undefined;
|
|
35
|
+
getIsExpandedCallback?: (node: LTreeNode<T>) => boolean;
|
|
34
36
|
isSelectableMember?: string | null | undefined;
|
|
37
|
+
getIsSelectableCallback?: (node: LTreeNode<T>) => boolean;
|
|
35
38
|
isSelectedMember?: string | null | undefined;
|
|
39
|
+
getIsSelectedCallback?: (node: LTreeNode<T>) => boolean;
|
|
36
40
|
isDraggableMember?: string | null | undefined;
|
|
37
41
|
getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
|
|
38
42
|
isDropAllowedMember?: string | null | undefined;
|
|
@@ -76,8 +80,16 @@
|
|
|
76
80
|
// BEHAVIOUR
|
|
77
81
|
expandLevel?: number | null | undefined;
|
|
78
82
|
clickBehavior?: ClickBehavior | null | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* `'single'` (default): plain click highlights one node; Ctrl/Shift+click
|
|
85
|
+
* degrade to plain click; Shift+Arrow / Enter are no-ops.
|
|
86
|
+
* `'multi'`: Ctrl-toggle, Shift-range, Shift+Arrow extend, Enter toggles
|
|
87
|
+
* highlight on the focused node.
|
|
88
|
+
*/
|
|
89
|
+
selectionMode?: SelectionMode | null | undefined;
|
|
79
90
|
showCheckboxes?: boolean | null | undefined;
|
|
80
91
|
checkboxMode?: CheckboxMode | null | undefined;
|
|
92
|
+
clickTogglesCheckbox?: boolean | null | undefined;
|
|
81
93
|
beforeCheckboxToggleCallback?: (node: LTreeNode<T>, checked: boolean, affectedPaths: string[]) => boolean | string[] | void;
|
|
82
94
|
rangeSelectionMode?: 'visual' | 'logical';
|
|
83
95
|
initializeIndexCallback?: () => Index;
|
|
@@ -185,8 +197,11 @@
|
|
|
185
197
|
hasChildrenMember,
|
|
186
198
|
|
|
187
199
|
isExpandedMember,
|
|
200
|
+
getIsExpandedCallback,
|
|
188
201
|
isSelectableMember,
|
|
202
|
+
getIsSelectableCallback,
|
|
189
203
|
isSelectedMember,
|
|
204
|
+
getIsSelectedCallback,
|
|
190
205
|
isDraggableMember,
|
|
191
206
|
getIsDraggableCallback,
|
|
192
207
|
isDropAllowedMember,
|
|
@@ -223,8 +238,10 @@
|
|
|
223
238
|
expandLevel = 2,
|
|
224
239
|
|
|
225
240
|
clickBehavior = 'expand-and-focus',
|
|
241
|
+
selectionMode = 'single',
|
|
226
242
|
showCheckboxes = false,
|
|
227
243
|
checkboxMode = 'independent',
|
|
244
|
+
clickTogglesCheckbox = false,
|
|
228
245
|
beforeCheckboxToggleCallback,
|
|
229
246
|
rangeSelectionMode = 'visual',
|
|
230
247
|
shouldUseInternalSearchIndex = true,
|
|
@@ -306,8 +323,11 @@
|
|
|
306
323
|
levelMember,
|
|
307
324
|
hasChildrenMember,
|
|
308
325
|
isExpandedMember,
|
|
326
|
+
getIsExpandedCallback,
|
|
309
327
|
isSelectableMember,
|
|
328
|
+
getIsSelectableCallback,
|
|
310
329
|
isSelectedMember,
|
|
330
|
+
getIsSelectedCallback,
|
|
311
331
|
isDraggableMember,
|
|
312
332
|
getIsDraggableCallback,
|
|
313
333
|
isDropAllowedMember,
|
|
@@ -330,8 +350,10 @@
|
|
|
330
350
|
selectedPaths,
|
|
331
351
|
expandLevel,
|
|
332
352
|
clickBehavior,
|
|
353
|
+
selectionMode,
|
|
333
354
|
showCheckboxes,
|
|
334
355
|
checkboxMode,
|
|
356
|
+
clickTogglesCheckbox,
|
|
335
357
|
beforeCheckboxToggleCallback,
|
|
336
358
|
rangeSelectionMode,
|
|
337
359
|
shouldUseInternalSearchIndex,
|
|
@@ -440,8 +462,10 @@
|
|
|
440
462
|
|
|
441
463
|
// Visual config sync (drives nodeConfig update via controller's internal effect)
|
|
442
464
|
$effect(() => { controller.clickBehavior = clickBehavior ?? 'expand-and-focus'; });
|
|
465
|
+
$effect(() => { controller.selectionMode = selectionMode ?? 'single'; });
|
|
443
466
|
$effect(() => { controller.showCheckboxes = showCheckboxes ?? false; });
|
|
444
467
|
$effect(() => { controller.checkboxMode = checkboxMode ?? 'independent'; });
|
|
468
|
+
$effect(() => { controller.clickTogglesCheckbox = clickTogglesCheckbox ?? false; });
|
|
445
469
|
$effect(() => { controller.beforeCheckboxToggleHandler = beforeCheckboxToggleCallback; });
|
|
446
470
|
$effect(() => { controller.rangeSelectionMode = rangeSelectionMode ?? 'visual'; });
|
|
447
471
|
$effect(() => { controller.expandIconClass = expandIconClass ?? 'ltree-icon-expand'; });
|
|
@@ -507,20 +531,32 @@
|
|
|
507
531
|
);
|
|
508
532
|
|
|
509
533
|
// ── Export public methods (thin proxies) ────────────────────────────
|
|
510
|
-
export async function expandNodes(
|
|
511
|
-
|
|
534
|
+
export async function expandNodes(
|
|
535
|
+
nodePath: string | string[],
|
|
536
|
+
options?: { exclusive?: boolean; noEmit?: boolean }
|
|
537
|
+
) {
|
|
538
|
+
controller.expandNodes(nodePath, options);
|
|
512
539
|
}
|
|
513
540
|
|
|
514
|
-
export async function collapseNodes(
|
|
515
|
-
|
|
541
|
+
export async function collapseNodes(
|
|
542
|
+
nodePath: string | string[],
|
|
543
|
+
options?: { noEmit?: boolean }
|
|
544
|
+
) {
|
|
545
|
+
controller.collapseNodes(nodePath, options);
|
|
516
546
|
}
|
|
517
547
|
|
|
518
|
-
export function expandAll(
|
|
519
|
-
|
|
548
|
+
export function expandAll(
|
|
549
|
+
nodePath?: string | string[] | null | undefined,
|
|
550
|
+
options?: { exclusive?: boolean; noEmit?: boolean }
|
|
551
|
+
) {
|
|
552
|
+
controller.expandAll(nodePath, options);
|
|
520
553
|
}
|
|
521
554
|
|
|
522
|
-
export function collapseAll(
|
|
523
|
-
|
|
555
|
+
export function collapseAll(
|
|
556
|
+
nodePath?: string | string[] | null | undefined,
|
|
557
|
+
options?: { noEmit?: boolean }
|
|
558
|
+
) {
|
|
559
|
+
controller.collapseAll(nodePath, options);
|
|
524
560
|
}
|
|
525
561
|
|
|
526
562
|
export function filterNodes(searchTextVal: string, searchOptions?: SearchOptions): void {
|
|
@@ -613,16 +649,28 @@
|
|
|
613
649
|
}
|
|
614
650
|
|
|
615
651
|
// Multi-select methods
|
|
616
|
-
export function selectNode(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace') {
|
|
617
|
-
controller.selectNode(path, mode);
|
|
652
|
+
export function selectNode(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace', options?: { silent?: boolean }) {
|
|
653
|
+
controller.selectNode(path, mode, options);
|
|
618
654
|
}
|
|
619
655
|
|
|
620
|
-
export function selectNodes(paths: string[]) {
|
|
621
|
-
controller.selectNodes(paths);
|
|
656
|
+
export function selectNodes(paths: string[], options?: { silent?: boolean }) {
|
|
657
|
+
controller.selectNodes(paths, options);
|
|
622
658
|
}
|
|
623
659
|
|
|
624
|
-
export function
|
|
625
|
-
controller.
|
|
660
|
+
export function highlightNode(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace', options?: { silent?: boolean }) {
|
|
661
|
+
controller.highlightNode(path, mode, options);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function highlightNodes(paths: string[], options?: { silent?: boolean }) {
|
|
665
|
+
controller.highlightNodes(paths, options);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function clearHighlight(options?: { silent?: boolean }) {
|
|
669
|
+
controller.clearHighlight(options);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function deselectAll(options?: { silent?: boolean }) {
|
|
673
|
+
controller.deselectAll(options);
|
|
626
674
|
}
|
|
627
675
|
|
|
628
676
|
export function getSelectedNodes(): LTreeNode<T>[] {
|
|
@@ -659,8 +707,11 @@
|
|
|
659
707
|
| "levelMember"
|
|
660
708
|
| "hasChildrenMember"
|
|
661
709
|
| "isExpandedMember"
|
|
710
|
+
| "getIsExpandedCallback"
|
|
662
711
|
| "isSelectableMember"
|
|
712
|
+
| "getIsSelectableCallback"
|
|
663
713
|
| "isSelectedMember"
|
|
714
|
+
| "getIsSelectedCallback"
|
|
664
715
|
| "isDraggableMember"
|
|
665
716
|
| "getIsDraggableCallback"
|
|
666
717
|
| "isDropAllowedMember"
|
|
@@ -679,8 +730,10 @@
|
|
|
679
730
|
| "selectedPaths"
|
|
680
731
|
| "expandLevel"
|
|
681
732
|
| "clickBehavior"
|
|
733
|
+
| "selectionMode"
|
|
682
734
|
| "showCheckboxes"
|
|
683
735
|
| "checkboxMode"
|
|
736
|
+
| "clickTogglesCheckbox"
|
|
684
737
|
| "beforeCheckboxToggleCallback"
|
|
685
738
|
| "rangeSelectionMode"
|
|
686
739
|
| "shouldUseInternalSearchIndex"
|
|
@@ -732,8 +785,11 @@
|
|
|
732
785
|
if (updates.levelMember !== undefined) levelMember = updates.levelMember;
|
|
733
786
|
if (updates.hasChildrenMember !== undefined) hasChildrenMember = updates.hasChildrenMember;
|
|
734
787
|
if (updates.isExpandedMember !== undefined) isExpandedMember = updates.isExpandedMember;
|
|
788
|
+
if (updates.getIsExpandedCallback !== undefined) getIsExpandedCallback = updates.getIsExpandedCallback;
|
|
735
789
|
if (updates.isSelectableMember !== undefined) isSelectableMember = updates.isSelectableMember;
|
|
790
|
+
if (updates.getIsSelectableCallback !== undefined) getIsSelectableCallback = updates.getIsSelectableCallback;
|
|
736
791
|
if (updates.isSelectedMember !== undefined) isSelectedMember = updates.isSelectedMember;
|
|
792
|
+
if (updates.getIsSelectedCallback !== undefined) getIsSelectedCallback = updates.getIsSelectedCallback;
|
|
737
793
|
if (updates.isDraggableMember !== undefined) isDraggableMember = updates.isDraggableMember;
|
|
738
794
|
if (updates.getIsDraggableCallback !== undefined) getIsDraggableCallback = updates.getIsDraggableCallback;
|
|
739
795
|
if (updates.isDropAllowedMember !== undefined) isDropAllowedMember = updates.isDropAllowedMember;
|
|
@@ -752,8 +808,10 @@
|
|
|
752
808
|
if (updates.selectedPaths !== undefined) selectedPaths = updates.selectedPaths;
|
|
753
809
|
if (updates.expandLevel !== undefined) expandLevel = updates.expandLevel;
|
|
754
810
|
if (updates.clickBehavior !== undefined) clickBehavior = updates.clickBehavior;
|
|
811
|
+
if (updates.selectionMode !== undefined) selectionMode = updates.selectionMode;
|
|
755
812
|
if (updates.showCheckboxes !== undefined) showCheckboxes = updates.showCheckboxes;
|
|
756
813
|
if (updates.checkboxMode !== undefined) checkboxMode = updates.checkboxMode;
|
|
814
|
+
if (updates.clickTogglesCheckbox !== undefined) clickTogglesCheckbox = updates.clickTogglesCheckbox;
|
|
757
815
|
if (updates.beforeCheckboxToggleCallback !== undefined) beforeCheckboxToggleCallback = updates.beforeCheckboxToggleCallback;
|
|
758
816
|
if (updates.rangeSelectionMode !== undefined) rangeSelectionMode = updates.rangeSelectionMode;
|
|
759
817
|
if (updates.shouldUseInternalSearchIndex !== undefined) shouldUseInternalSearchIndex = updates.shouldUseInternalSearchIndex;
|
|
@@ -813,6 +871,8 @@
|
|
|
813
871
|
|
|
814
872
|
let handled = true;
|
|
815
873
|
|
|
874
|
+
// Shift+nav extends the highlight range. In single mode the controller
|
|
875
|
+
// short-circuits these to no-ops (see _navHighlightTo).
|
|
816
876
|
switch (event.key) {
|
|
817
877
|
case 'ArrowDown': event.shiftKey ? controller.navHighlightNext() : controller.navNextSibling(); break;
|
|
818
878
|
case 'ArrowUp': event.shiftKey ? controller.navHighlightPrev() : controller.navPrevSibling(); break;
|
|
@@ -824,7 +884,19 @@
|
|
|
824
884
|
case 'PageDown': event.shiftKey ? controller.navHighlightPageDown() : controller.navPageDown(); break;
|
|
825
885
|
case 'PageUp': event.shiftKey ? controller.navHighlightPageUp() : controller.navPageUp(); break;
|
|
826
886
|
case 'Enter':
|
|
827
|
-
|
|
887
|
+
// single: no-op (per spec); multi: toggle highlight on focused node.
|
|
888
|
+
if (controller.selectionMode === 'multi') controller.toggleFocusedHighlight();
|
|
889
|
+
else handled = false;
|
|
890
|
+
break;
|
|
891
|
+
case ' ':
|
|
892
|
+
// With checkboxes: toggle the focused node's checkbox.
|
|
893
|
+
// Without: keep the legacy expand/collapse behaviour as a useful fallback.
|
|
894
|
+
if (controller.showCheckboxes && controller.focusedNode?.isSelectable) {
|
|
895
|
+
controller.nodeCallbacks.onCheckboxToggle(controller.focusedNode);
|
|
896
|
+
} else {
|
|
897
|
+
controller.navToggle();
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
828
900
|
default: handled = false;
|
|
829
901
|
}
|
|
830
902
|
|