@keenmate/svelte-treeview 5.0.0-rc09 → 5.0.0-rc10
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 +23 -0
- package/README.md +10 -7
- package/component-variables.manifest.json +6 -3
- package/dist/components/Node.svelte +6 -10
- package/dist/components/Tree.svelte +24 -0
- package/dist/components/Tree.svelte.d.ts +20 -2
- package/dist/components/TreeProvider.svelte +1 -0
- package/dist/constants.generated.d.ts +1 -1
- package/dist/constants.generated.js +1 -1
- package/dist/core/TreeController.svelte.d.ts +11 -0
- package/dist/core/TreeController.svelte.js +61 -2
- package/dist/ltree/ltree-node.svelte.js +2 -2
- package/dist/ltree/ltree.svelte.d.ts +1 -1
- package/dist/ltree/ltree.svelte.js +21 -5
- package/dist/ltree/types.d.ts +2 -0
- package/dist/styles/animations.css +17 -0
- package/dist/styles/{_base.css → base.css} +2 -0
- package/dist/styles/controls.css +4 -0
- package/dist/styles/dark-mode.css +139 -0
- package/dist/styles/{_drag-drop.css → drag-drop.css} +2 -6
- package/dist/styles/floating.css +5 -0
- package/dist/styles/{_loading.css → loading.css} +1 -5
- package/dist/styles/main.css +44 -37
- package/dist/styles/{_variables.css → variables.css} +56 -30
- package/dist/styles.css +939 -869
- package/package.json +6 -3
- /package/dist/styles/{_checkbox.css → checkbox.css} +0 -0
- /package/dist/styles/{_context-menu.css → context-menu.css} +0 -0
- /package/dist/styles/{_debug.css → debug.css} +0 -0
- /package/dist/styles/{_drop-zones.css → drop-zones.css} +0 -0
- /package/dist/styles/{_node.css → node.css} +0 -0
- /package/dist/styles/{_states.css → states.css} +0 -0
- /package/dist/styles/{_toggle-icons.css → toggle-icons.css} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.0.0-rc10] - 2026-06-10
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **CSS custom properties rescoped from `:root` to `.ltree-container`**: All `--ltree-*` declarations now live on the tree's root element instead of the document root. This mirrors the `:host`-scoped pattern from `@keenmate/web-multiselect` / `web-daterangepicker` and is the only way `--base-*` theming actually works at subtree scope. Previously, a wrapper around the tree that set `--base-accent-color: red` had no effect because `--ltree-primary`'s `var(--base-accent-color, ...)` substitution was frozen at `:root`. With the new scope, the substitution recomputes on `.ltree-container` (a descendant of the wrapper), and `--base-*` set on any ancestor flows through correctly. Multiple trees on the same page can now be themed differently via subtree wrappers.
|
|
14
|
+
|
|
15
|
+
**Consumer migration**: setting `--ltree-*` on a wrapper element (e.g., `.my-theme { --ltree-primary: red }`) no longer cascades into the tree, because `.ltree-container` has its own direct rule for every `--ltree-*` token. To theme a subtree, prefer `--base-accent-color` (and the rest of the `--base-*` taxonomy) on the wrapper — every primary-derived tint follows via the existing `color-mix()` chains. To override an `--ltree-*` directly, target `.ltree-container`: `.my-wrap .ltree-container { --ltree-primary: red }`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`--ltree-bg` CSS variable**: The tree's `.ltree-container` now paints its own default background (`var(--base-main-bg, light-dark(#ffffff, #1a1a1a))`) so consumers don't need to wrap the tree in a colored container for a visible surface. The dark-mode CSS flips `--ltree-bg` alongside the other surface tokens. Set `--ltree-bg: transparent` to restore the pre-rc10 layered behavior.
|
|
19
|
+
- **Built-in dark mode with belt-and-suspenders coverage**: New `dark-mode.css` partial in an `@layer overrides;` cascade catches all four signals — OS preference via `@media (prefers-color-scheme: dark)`, framework theme classes (`[data-theme="dark"]`, `[data-bs-theme="dark"]`, `.dark`), per-instance opt-in via the new `theme` prop on `<Tree>`, and symmetric `light` selectors so a single tree can be forced back to light on a dark page. Every `--ltree-*` color variable's fallback now uses `light-dark(<light>, <dark>)` so consumers who declare `html { color-scheme: light dark }` get OS-aware behavior automatically with zero JavaScript.
|
|
20
|
+
- **`theme` prop on `<Tree>`**: `'dark' | 'light' | null | undefined`. Forwards to the root `.ltree-container` as `data-theme`, where the stylesheet's per-instance selectors (`.ltree-container[data-theme="dark"]`) take over. Leave `undefined` to inherit from the page (OS, framework class, etc.).
|
|
21
|
+
- **`--ltree-elevated-bg`, `--tree-ghost-shadow` CSS variables**: `elevated-bg` reads through the canonical `--base-elevated-bg` chain for surfaces above the main background (currently feeds the context-menu chain); `tree-ghost-shadow` replaces a hardcoded `rgba(0,0,0,0.3)` literal in the touch drag ghost so consumers can recolor or remove the shadow.
|
|
22
|
+
- **`getIsDropAllowedCallback` prop + `getNodeIsDropAllowed(node)` method**: Callback variant for the existing `isDropAllowedMember` data-field prop, matching the pattern rc09 introduced for `getIsExpandedCallback` / `getIsSelectableCallback` / `getIsSelectedCallback`. Seeded at `insertArray` time. Precedence: callback > member > default. Additionally, `getIsDraggableCallback` is now actually applied during the seed walk (the prop existed in rc09 but inert at insert-time — only consumed lazily in some paths). Precedence: callback > member > default for both.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **CSS file layout aligned with the Bliss web-component guidelines**: All `_<partial>.css` renamed to `<partial>.css` (the underscore was a legacy SASS convention and these are plain CSS modules). `main.css` now declares `@layer variables, component, overrides;` and wraps every `@import` in `layer(...)` — consumers' unlayered overrides automatically beat every rule in the library, no `!important` needed. New canonical Tier-2 stubs (`controls.css`, `floating.css`, `animations.css`) added even where empty, so the file set is identical across the KeenMate component suite. `@keyframes bounce` and `@keyframes ltree-spin` moved into `animations.css`.
|
|
26
|
+
- **Loading overlay background adapts to theme**: `--ltree-loading-bg` no longer hardcodes `rgba(255, 255, 255, 0.8)`; it now composes 80% of `--base-main-bg` against the page so the semi-transparent overlay reads correctly on both light and dark surfaces.
|
|
27
|
+
- **`component-variables.manifest.json` extended**: `base-elevated-bg` added to the `baseVariables` list; `ltree-elevated-bg` and `tree-ghost-shadow` added to `componentVariables`. The manifest now stays in sync with the new variable surface.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **Virtual scroll stuck at bottom after filter shrinks the tree**: When the user scrolled down and then typed a search filter, `visibleFlatNodes` collapsed and `vsTotalHeight` shrank below the previous scroll position. The browser silently clamped the container's actual `scrollTop`, but the controller's `vsScrollTop` state kept its stale large value — so `vsStartIndex` fell out of range, `flatNodesToRender` was empty, and the scrollbar appeared frozen near the bottom with no content rendered. A new `$effect` now reacts to `vsTotalHeight` and clamps both `vsScrollTop` and the container's `scrollTop` to the new max whenever content shrinks below the current scroll position.
|
|
31
|
+
- **Double-click expand silently broken in `clickBehavior='select'` mode**: The browser's native `dblclick` event never fired because the first click triggered focus → `_setFocusedNode` bumped `node._rev` → the flat-mode `{#each}` destroyed and recreated the row, so the second click landed on a different DOM element and the browser refused to synthesize a `dblclick`. The controller now detects double-clicks manually (`_lastSelectClickPath` / `_lastSelectClickTime`, 400 ms threshold) so a rapid second click on the same node toggles its expand state regardless of re-render. Node-level `_onNodeDblClicked` removed (it could double-toggle if the browser ever did fire `dblclick`).
|
|
32
|
+
|
|
10
33
|
## [5.0.0-rc09] - 2026-06-07
|
|
11
34
|
|
|
12
35
|
### Added
|
package/README.md
CHANGED
|
@@ -6,6 +6,16 @@ A high-performance, feature-rich hierarchical tree view component for Svelte 5 w
|
|
|
6
6
|
|
|
7
7
|
Browse interactive code examples and the full API reference at **[svelte-treeview.keenmate.dev](https://svelte-treeview.keenmate.dev)**
|
|
8
8
|
|
|
9
|
+
## What's New in v5.0.0-rc10
|
|
10
|
+
|
|
11
|
+
- **Built-in dark mode covering all four signals**: A new `dark-mode.css` partial flips the tree's surface, text, border, and accent palette when any of the canonical signals is active — OS preference (`prefers-color-scheme: dark`), page `<html style="color-scheme: dark">` resolved via `light-dark()`, framework theme classes (`[data-theme="dark"]`, `[data-bs-theme="dark"]`, `.dark`), and the new per-instance `theme` prop. Symmetric `light` selectors let a single tree force light on an otherwise-dark page. Zero JavaScript — pure CSS resolution.
|
|
12
|
+
- **`theme` prop on `<Tree>`**: `'dark' | 'light' | null | undefined`. Forwarded to the root `.ltree-container` as `data-theme`, where per-instance CSS selectors take over and beat ambient signals. Leave `undefined` to inherit from the page.
|
|
13
|
+
- **CSS variables rescoped from `:root` to `.ltree-container`**: Mirrors the `:host`-scoped pattern from sibling `@keenmate/*` components and is the only way `--base-*` theming actually works at subtree scope. A wrapper around the tree that sets `--base-accent-color: red` now re-tints the tree (it previously had no effect because the substitution was frozen at `:root`). Multiple trees on the same page can be themed differently via wrapper-scoped `--base-*` overrides. Consumer note: setting `--ltree-*` directly on a wrapper no longer cascades — use `--base-*` on the wrapper or target `.ltree-container` explicitly.
|
|
14
|
+
- **`--ltree-bg` — the tree paints its own surface**: `.ltree-container` gets a `background: var(--ltree-bg)` so consumers don't need to wrap the tree in a colored container for a visible surface. Override to `transparent` to restore the pre-rc10 layered behavior. Companion `--ltree-elevated-bg` reads through the `--base-elevated-bg` chain for floating chrome (context menu).
|
|
15
|
+
- **CSS file layout aligned with the Bliss web-component guidelines**: All `_xxx.css` partials renamed to `xxx.css`, `main.css` now declares `@layer variables, component, overrides;` and imports each partial into the matching layer. Consumers' unlayered overrides automatically beat every rule in the library — no `!important` needed. Note: if your app ships an unlayered universal CSS reset (`* { padding: 0 }` from normalize / Bootstrap / Tailwind preflight / etc.), wrap it in a low-priority `@layer reset` or the tree's defaults won't apply.
|
|
16
|
+
- **`getIsDropAllowedCallback` prop + `getIsDraggableCallback` seeded at insert-time**: Callback variant for `isDropAllowedMember`, matching the pattern rc09 introduced for `getIsExpandedCallback` / `getIsSelectableCallback` / `getIsSelectedCallback`. Also, the existing `getIsDraggableCallback` prop is now actually applied during the seed walk — previously it was only consumed lazily in some paths.
|
|
17
|
+
- **Bug fixes**: Virtual scroll no longer gets stuck at bottom after a filter shrinks the tree. Double-click to expand in `clickBehavior='select'` mode finally works (the browser's native `dblclick` event couldn't fire because the first click destroyed the row via the focus re-render; the controller now detects the double manually).
|
|
18
|
+
|
|
9
19
|
## What's New in v5.0.0-rc09
|
|
10
20
|
|
|
11
21
|
- **Three-level selection model**: New `selectionMode: 'single' | 'multi'` prop (default `'single'`) cleanly separates the **focused** node (cursor), the **highlighted** set (Ctrl/Shift+click range), and **selected** checkboxes. When `showCheckboxes` is off, highlight mirrors into `selectedPaths` automatically — so consumers always have a single "what's selected" set to read regardless of UI style. Programmatic `highlightNode` / `highlightNodes` / `clearHighlight` respect `{ silent: true }`.
|
|
@@ -18,13 +28,6 @@ Browse interactive code examples and the full API reference at **[svelte-treevie
|
|
|
18
28
|
|
|
19
29
|
> **Breaking changes** in this release include the `selectionMode='single'` default (Ctrl/Shift+click no longer auto-multi-selects without opt-in), `selectedPaths` becoming populated in no-checkbox trees (mirrored from highlight), removal of `lastHighlightedPath` / `isHighlightAnchor` / `--ltree-*-rgb` / SCSS variable overrides, and `--ltree-node-indent-per-level` now scaling with `--ltree-rem` instead of document `rem`. See CHANGELOG for migration notes.
|
|
20
30
|
|
|
21
|
-
## What's New in v5.0.0-rc08
|
|
22
|
-
|
|
23
|
-
- **`{ silent: true }` on highlight/selection methods**: Update tree state from URL params or other external sources without firing `onNodeClick` / `onHighlightChange` / `onSelectionChange` — perfect for deep links where you don't want the change callback to re-trigger your form loader. Applies to `highlightNode`, `highlightNodes`, `clearHighlight`, and `deselectAll`.
|
|
24
|
-
- **Array variants for expand/collapse**: `expandNodes`, `collapseNodes`, `expandAll`, and `collapseAll` now take `string | string[]`. Open or close several places in one call.
|
|
25
|
-
- **Exclusive focus mode**: Pass `{ exclusive: true }` to `expandNodes` / `expandAll` to open the target path and collapse everything else in a single pass — no two-step flicker compared to `collapseAll() + expandNodes(path)`.
|
|
26
|
-
- **`noEmit` option for batching**: Suppress the change emit on individual expand/collapse calls when chaining several operations; emit once at the end.
|
|
27
|
-
|
|
28
31
|
## v5.0: Core/Renderer Split + Virtual Scroll
|
|
29
32
|
|
|
30
33
|
> [!IMPORTANT]
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
{ "name": "base-accent-color", "required": true, "usage": "Primary accent — selection, drop-zones, focus rings, debug panel. Tints derived via color-mix()." },
|
|
7
7
|
{ "name": "base-success-color", "required": false, "usage": "Drop-valid border and dragover highlight" },
|
|
8
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)" },
|
|
9
|
+
{ "name": "base-main-bg", "required": false, "usage": "Used for --ltree-light (context-menu hover tint, debug stat chips). Light fallback uses light-dark() for OS-aware dark mode." },
|
|
10
|
+
{ "name": "base-elevated-bg", "required": false, "usage": "Elevated surface for context-menu (via dropdown chain) and --ltree-elevated-bg. Light fallback uses light-dark()." },
|
|
10
11
|
{ "name": "base-border-color", "required": true, "usage": "Default border color (checkbox, context-menu, spinner track)" },
|
|
11
12
|
{ "name": "base-hover-bg", "required": false, "usage": "Node row hover background" },
|
|
12
13
|
{ "name": "base-text-color-1", "required": true, "usage": "Primary body text color (node labels, context-menu items)" },
|
|
@@ -28,8 +29,9 @@
|
|
|
28
29
|
{ "name": "ltree-primary", "category": "color", "usage": "Primary accent color (selection, focus, links). Tints derived via color-mix()." },
|
|
29
30
|
{ "name": "ltree-success", "category": "color", "usage": "Success color (drop-valid border, dragover-highlight)" },
|
|
30
31
|
{ "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-
|
|
32
|
+
{ "name": "ltree-light", "category": "color", "usage": "Light surface (context-menu hover, debug stat chips). Light-dark-aware via fallback chain." },
|
|
33
|
+
{ "name": "ltree-elevated-bg", "category": "surface", "usage": "Elevated surface used as the default context-menu background (chains through --base-elevated-bg)." },
|
|
34
|
+
{ "name": "ltree-border", "category": "color", "usage": "Default border color. Light-dark-aware via fallback chain." },
|
|
33
35
|
{ "name": "ltree-body-color", "category": "color", "usage": "Default body text color" },
|
|
34
36
|
|
|
35
37
|
{ "name": "ltree-font-family", "category": "typography", "usage": "Font family for all tree text" },
|
|
@@ -110,6 +112,7 @@
|
|
|
110
112
|
|
|
111
113
|
{ "name": "tree-ghost-bg", "category": "state", "usage": "Touch drag ghost background (legacy prefix, kept for backward compat)" },
|
|
112
114
|
{ "name": "tree-ghost-color", "category": "state", "usage": "Touch drag ghost text color (legacy prefix)" },
|
|
115
|
+
{ "name": "tree-ghost-shadow", "category": "state", "usage": "Touch drag ghost drop shadow (legacy prefix)" },
|
|
113
116
|
|
|
114
117
|
{ "name": "ltree-context-menu-bg", "category": "context-menu", "usage": "Menu background" },
|
|
115
118
|
{ "name": "ltree-context-menu-border-color", "category": "context-menu", "usage": "Menu border color" },
|
|
@@ -323,12 +323,12 @@
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
326
|
+
// Note: double-click in clickBehavior='select' is detected on the controller
|
|
327
|
+
// side (see _lastSelectClickPath in TreeController). The browser's native
|
|
328
|
+
// dblclick event isn't reliable here: the first click bumps node._rev for
|
|
329
|
+
// the focus update, which destroys+recreates the row in flat-mode rendering,
|
|
330
|
+
// so the browser sees the two clicks on different elements and skips the
|
|
331
|
+
// dblclick. Manual detection on the (stable) controller avoids that.
|
|
332
332
|
</script>
|
|
333
333
|
|
|
334
334
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -391,10 +391,6 @@
|
|
|
391
391
|
e.stopPropagation();
|
|
392
392
|
_onNodeClicked(e);
|
|
393
393
|
}}
|
|
394
|
-
ondblclick={(e) => {
|
|
395
|
-
e.stopPropagation();
|
|
396
|
-
_onNodeDblClicked(e);
|
|
397
|
-
}}
|
|
398
394
|
oncontextmenu={(e) => {
|
|
399
395
|
e.stopPropagation();
|
|
400
396
|
callbacks.onNodeRightClicked(node, e);
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
isDraggableMember?: string | null | undefined;
|
|
41
41
|
getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
|
|
42
42
|
isDropAllowedMember?: string | null | undefined;
|
|
43
|
+
getIsDropAllowedCallback?: (node: LTreeNode<T>) => boolean;
|
|
43
44
|
allowedDropPositionsMember?: string | null | undefined;
|
|
44
45
|
getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
|
|
45
46
|
isCollapsibleMember?: string | null | undefined;
|
|
@@ -165,6 +166,11 @@
|
|
|
165
166
|
getContextMenuItemsCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void, selectedNodes?: LTreeNode<T>[]) => ContextMenuEntry[];
|
|
166
167
|
|
|
167
168
|
// VISUALS
|
|
169
|
+
/** Per-instance theme override. Forwarded to the root `.ltree-container`
|
|
170
|
+
* as `data-theme="dark"|"light"`, which the stylesheet uses to flip the
|
|
171
|
+
* tree's colors independently of the surrounding page. Leave undefined to
|
|
172
|
+
* inherit from the page (OS preference, framework classes, etc.). */
|
|
173
|
+
theme?: 'dark' | 'light' | null | undefined;
|
|
168
174
|
bodyClass?: string | null | undefined;
|
|
169
175
|
highlightedNodeClass?: string | null | undefined;
|
|
170
176
|
focusedNodeClass?: string | null | undefined;
|
|
@@ -205,6 +211,7 @@
|
|
|
205
211
|
isDraggableMember,
|
|
206
212
|
getIsDraggableCallback,
|
|
207
213
|
isDropAllowedMember,
|
|
214
|
+
getIsDropAllowedCallback,
|
|
208
215
|
allowedDropPositionsMember,
|
|
209
216
|
getAllowedDropPositionsCallback,
|
|
210
217
|
isCollapsibleMember,
|
|
@@ -299,6 +306,7 @@
|
|
|
299
306
|
getContextMenuItemsCallback,
|
|
300
307
|
|
|
301
308
|
// VISUALS
|
|
309
|
+
theme,
|
|
302
310
|
bodyClass,
|
|
303
311
|
expandIconClass = 'ltree-icon-expand',
|
|
304
312
|
collapseIconClass = 'ltree-icon-collapse',
|
|
@@ -316,6 +324,12 @@
|
|
|
316
324
|
}: Props = $props();
|
|
317
325
|
|
|
318
326
|
// ── Create controller ───────────────────────────────────────────────
|
|
327
|
+
// Each prop here captures its INITIAL value only — that's intentional. Every
|
|
328
|
+
// reactive prop is re-synced into the controller via $effect blocks below
|
|
329
|
+
// (search for "Sync props → controller"), so the controller stays in step
|
|
330
|
+
// with parent changes. The svelte-ignore suppresses the 80+ warnings the
|
|
331
|
+
// compiler would otherwise fire for each non-closure prop reference here.
|
|
332
|
+
// svelte-ignore state_referenced_locally
|
|
319
333
|
const controller = createTreeController<T>({
|
|
320
334
|
idMember,
|
|
321
335
|
pathMember,
|
|
@@ -331,6 +345,7 @@
|
|
|
331
345
|
isDraggableMember,
|
|
332
346
|
getIsDraggableCallback,
|
|
333
347
|
isDropAllowedMember,
|
|
348
|
+
getIsDropAllowedCallback,
|
|
334
349
|
allowedDropPositionsMember,
|
|
335
350
|
getAllowedDropPositionsCallback,
|
|
336
351
|
isCollapsibleMember,
|
|
@@ -412,7 +427,12 @@
|
|
|
412
427
|
});
|
|
413
428
|
|
|
414
429
|
// ── Apply navigation overrides if provided ─────────────────────────
|
|
430
|
+
// Snapshot at init only — navigation handlers aren't expected to swap mid-
|
|
431
|
+
// lifetime. Consumers passing a new object on rerender would not see it
|
|
432
|
+
// applied; if that becomes a need, hoist into an $effect.
|
|
433
|
+
// svelte-ignore state_referenced_locally
|
|
415
434
|
if (navigationOverrides) {
|
|
435
|
+
// svelte-ignore state_referenced_locally
|
|
416
436
|
controller.navigation = { ...controller.createDefaultNavigation(), ...navigationOverrides };
|
|
417
437
|
}
|
|
418
438
|
|
|
@@ -715,6 +735,7 @@
|
|
|
715
735
|
| "isDraggableMember"
|
|
716
736
|
| "getIsDraggableCallback"
|
|
717
737
|
| "isDropAllowedMember"
|
|
738
|
+
| "getIsDropAllowedCallback"
|
|
718
739
|
| "displayValueMember"
|
|
719
740
|
| "getDisplayValueCallback"
|
|
720
741
|
| "searchValueMember"
|
|
@@ -793,6 +814,7 @@
|
|
|
793
814
|
if (updates.isDraggableMember !== undefined) isDraggableMember = updates.isDraggableMember;
|
|
794
815
|
if (updates.getIsDraggableCallback !== undefined) getIsDraggableCallback = updates.getIsDraggableCallback;
|
|
795
816
|
if (updates.isDropAllowedMember !== undefined) isDropAllowedMember = updates.isDropAllowedMember;
|
|
817
|
+
if (updates.getIsDropAllowedCallback !== undefined) getIsDropAllowedCallback = updates.getIsDropAllowedCallback;
|
|
796
818
|
if (updates.displayValueMember !== undefined) displayValueMember = updates.displayValueMember;
|
|
797
819
|
if (updates.getDisplayValueCallback !== undefined) getDisplayValueCallback = updates.getDisplayValueCallback;
|
|
798
820
|
if (updates.searchValueMember !== undefined) searchValueMember = updates.searchValueMember;
|
|
@@ -959,9 +981,11 @@
|
|
|
959
981
|
<svelte:window onkeydown={handleContextMenuKeydown} />
|
|
960
982
|
|
|
961
983
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
984
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
962
985
|
<div
|
|
963
986
|
class="ltree-container"
|
|
964
987
|
tabindex="0"
|
|
988
|
+
data-theme={theme}
|
|
965
989
|
bind:this={treeContainerRef}
|
|
966
990
|
onkeydown={handleTreeKeydown}
|
|
967
991
|
ondragenter={controller.handleTreeDragEnter}
|
|
@@ -19,6 +19,7 @@ declare function $$render<T>(): {
|
|
|
19
19
|
isDraggableMember?: string | null | undefined;
|
|
20
20
|
getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
|
|
21
21
|
isDropAllowedMember?: string | null | undefined;
|
|
22
|
+
getIsDropAllowedCallback?: (node: LTreeNode<T>) => boolean;
|
|
22
23
|
allowedDropPositionsMember?: string | null | undefined;
|
|
23
24
|
getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
|
|
24
25
|
isCollapsibleMember?: string | null | undefined;
|
|
@@ -127,6 +128,11 @@ declare function $$render<T>(): {
|
|
|
127
128
|
position?: "child" | "before" | "after";
|
|
128
129
|
} | false | void;
|
|
129
130
|
getContextMenuItemsCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void, selectedNodes?: LTreeNode<T>[]) => ContextMenuEntry[];
|
|
131
|
+
/** Per-instance theme override. Forwarded to the root `.ltree-container`
|
|
132
|
+
* as `data-theme="dark"|"light"`, which the stylesheet uses to flip the
|
|
133
|
+
* tree's colors independently of the surrounding page. Leave undefined to
|
|
134
|
+
* inherit from the page (OS preference, framework classes, etc.). */
|
|
135
|
+
theme?: "dark" | "light" | null | undefined;
|
|
130
136
|
bodyClass?: string | null | undefined;
|
|
131
137
|
highlightedNodeClass?: string | null | undefined;
|
|
132
138
|
focusedNodeClass?: string | null | undefined;
|
|
@@ -240,6 +246,7 @@ declare function $$render<T>(): {
|
|
|
240
246
|
isDraggableMember?: string | null | undefined;
|
|
241
247
|
getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
|
|
242
248
|
isDropAllowedMember?: string | null | undefined;
|
|
249
|
+
getIsDropAllowedCallback?: (node: LTreeNode<T>) => boolean;
|
|
243
250
|
allowedDropPositionsMember?: string | null | undefined;
|
|
244
251
|
getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
|
|
245
252
|
isCollapsibleMember?: string | null | undefined;
|
|
@@ -348,6 +355,11 @@ declare function $$render<T>(): {
|
|
|
348
355
|
position?: "child" | "before" | "after";
|
|
349
356
|
} | false | void;
|
|
350
357
|
getContextMenuItemsCallback?: (node: LTreeNode<T>, closeMenuCallback: () => void, selectedNodes?: LTreeNode<T>[]) => ContextMenuEntry[];
|
|
358
|
+
/** Per-instance theme override. Forwarded to the root `.ltree-container`
|
|
359
|
+
* as `data-theme="dark"|"light"`, which the stylesheet uses to flip the
|
|
360
|
+
* tree's colors independently of the surrounding page. Leave undefined to
|
|
361
|
+
* inherit from the page (OS preference, framework classes, etc.). */
|
|
362
|
+
theme?: "dark" | "light" | null | undefined;
|
|
351
363
|
bodyClass?: string | null | undefined;
|
|
352
364
|
highlightedNodeClass?: string | null | undefined;
|
|
353
365
|
focusedNodeClass?: string | null | undefined;
|
|
@@ -364,7 +376,7 @@ declare function $$render<T>(): {
|
|
|
364
376
|
onTreeKeydown?: (event: KeyboardEvent, controller: TreeController<T>) => boolean | void;
|
|
365
377
|
/** Override individual navigation methods (e.g. for custom ArrowDown/Up behavior) */
|
|
366
378
|
navigationOverrides?: TreeNavigationOverrides<T>;
|
|
367
|
-
}, "treeId" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "getIsExpandedCallback" | "isSelectableMember" | "getIsSelectableCallback" | "isSelectedMember" | "getIsSelectedCallback" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "orderMember" | "isSorted" | "sortCallback" | "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "selectionMode" | "showCheckboxes" | "checkboxMode" | "clickTogglesCheckbox" | "beforeCheckboxToggleCallback" | "rangeSelectionMode" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayDebugInformation" | "shouldDisplayContextMenuInDebugMode" | "onNodeClick" | "onHighlightChange" | "onSelectionChange" | "onNodeDragStart" | "onNodeDragOver" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "getContextMenuItemsCallback" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "bodyClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset" | "accordionExpand">>) => void;
|
|
379
|
+
}, "treeId" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "getIsExpandedCallback" | "isSelectableMember" | "getIsSelectableCallback" | "isSelectedMember" | "getIsSelectedCallback" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "getIsDropAllowedCallback" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "orderMember" | "isSorted" | "sortCallback" | "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "selectionMode" | "showCheckboxes" | "checkboxMode" | "clickTogglesCheckbox" | "beforeCheckboxToggleCallback" | "rangeSelectionMode" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayDebugInformation" | "shouldDisplayContextMenuInDebugMode" | "onNodeClick" | "onHighlightChange" | "onSelectionChange" | "onNodeDragStart" | "onNodeDragOver" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "getContextMenuItemsCallback" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "bodyClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset" | "accordionExpand">>) => void;
|
|
368
380
|
};
|
|
369
381
|
bindings: "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "searchText" | "insertResult" | "isRendering";
|
|
370
382
|
slots: {};
|
|
@@ -471,6 +483,7 @@ declare class __sveltets_Render<T> {
|
|
|
471
483
|
isDraggableMember?: string | null | undefined;
|
|
472
484
|
getIsDraggableCallback?: ((node: LTreeNode<T>) => boolean) | undefined;
|
|
473
485
|
isDropAllowedMember?: string | null | undefined;
|
|
486
|
+
getIsDropAllowedCallback?: ((node: LTreeNode<T>) => boolean) | undefined;
|
|
474
487
|
allowedDropPositionsMember?: string | null | undefined;
|
|
475
488
|
getAllowedDropPositionsCallback?: ((node: LTreeNode<T>) => DropPosition[] | null | undefined) | undefined;
|
|
476
489
|
isCollapsibleMember?: string | null | undefined;
|
|
@@ -579,6 +592,11 @@ declare class __sveltets_Render<T> {
|
|
|
579
592
|
position?: "child" | "before" | "after";
|
|
580
593
|
}) | undefined;
|
|
581
594
|
getContextMenuItemsCallback?: ((node: LTreeNode<T>, closeMenuCallback: () => void, selectedNodes?: LTreeNode<T>[] | undefined) => ContextMenuEntry[]) | undefined;
|
|
595
|
+
/** Per-instance theme override. Forwarded to the root `.ltree-container`
|
|
596
|
+
* as `data-theme="dark"|"light"`, which the stylesheet uses to flip the
|
|
597
|
+
* tree's colors independently of the surrounding page. Leave undefined to
|
|
598
|
+
* inherit from the page (OS preference, framework classes, etc.). */
|
|
599
|
+
theme?: "dark" | "light" | null | undefined;
|
|
582
600
|
bodyClass?: string | null | undefined;
|
|
583
601
|
highlightedNodeClass?: string | null | undefined;
|
|
584
602
|
focusedNodeClass?: string | null | undefined;
|
|
@@ -595,7 +613,7 @@ declare class __sveltets_Render<T> {
|
|
|
595
613
|
onTreeKeydown?: ((event: KeyboardEvent, controller: TreeController<T>) => boolean | void) | undefined;
|
|
596
614
|
/** Override individual navigation methods (e.g. for custom ArrowDown/Up behavior) */
|
|
597
615
|
navigationOverrides?: Partial<import("../core/navigation.js").TreeNavigation<T>> | undefined;
|
|
598
|
-
}, "treeId" | "data" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "getIsExpandedCallback" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isSelectableMember" | "getIsSelectableCallback" | "isSelectedMember" | "getIsSelectedCallback" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "shouldDisplayDebugInformation" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "beforeCheckboxToggleCallback" | "getContextMenuItemsCallback" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "selectionMode" | "showCheckboxes" | "checkboxMode" | "clickTogglesCheckbox" | "rangeSelectionMode" | "initializeIndexCallback" | "searchText" | "shouldUseInternalSearchIndex" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "accordionExpand" | "onNodeClick" | "onNodeDragStart" | "onNodeDragOver" | "onHighlightChange" | "onSelectionChange" | "bodyClass" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
|
|
616
|
+
}, "treeId" | "data" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "getIsExpandedCallback" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isSelectableMember" | "getIsSelectableCallback" | "isSelectedMember" | "getIsSelectedCallback" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "getIsDropAllowedCallback" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "shouldDisplayDebugInformation" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "beforeCheckboxToggleCallback" | "getContextMenuItemsCallback" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "selectionMode" | "showCheckboxes" | "checkboxMode" | "clickTogglesCheckbox" | "rangeSelectionMode" | "initializeIndexCallback" | "searchText" | "shouldUseInternalSearchIndex" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "accordionExpand" | "onNodeClick" | "onNodeDragStart" | "onNodeDragOver" | "onHighlightChange" | "onSelectionChange" | "bodyClass" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
|
|
599
617
|
};
|
|
600
618
|
}
|
|
601
619
|
interface $$IsomorphicComponent {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated file - do not edit manually
|
|
2
2
|
// Generated by scripts/generate-constants.js
|
|
3
|
-
export const VERSION = "5.0.0-
|
|
3
|
+
export const VERSION = "5.0.0-rc10";
|
|
4
4
|
export const PACKAGE_NAME = "@keenmate/svelte-treeview";
|
|
5
5
|
export const AUTHOR = "KeenMate";
|
|
6
6
|
export const LICENSE = "MIT";
|
|
@@ -67,6 +67,7 @@ export interface TreeControllerProps<T> {
|
|
|
67
67
|
isDraggableMember?: string | null | undefined;
|
|
68
68
|
getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
|
|
69
69
|
isDropAllowedMember?: string | null | undefined;
|
|
70
|
+
getIsDropAllowedCallback?: (node: LTreeNode<T>) => boolean;
|
|
70
71
|
allowedDropPositionsMember?: string | null | undefined;
|
|
71
72
|
getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
|
|
72
73
|
isCollapsibleMember?: string | null | undefined;
|
|
@@ -199,6 +200,14 @@ export declare class TreeController<T> {
|
|
|
199
200
|
* from the focused node. Set on first Shift action, advances on subsequent
|
|
200
201
|
* Shift actions, cleared on any plain navigation. Not exposed via props. */
|
|
201
202
|
private _shiftCursor;
|
|
203
|
+
/** Manual double-click detection state for clickBehavior='select'. We can't
|
|
204
|
+
* rely on the browser's native dblclick event in this mode because the first
|
|
205
|
+
* click triggers focus → _setFocusedNode bumps node._rev → flat-mode {#each}
|
|
206
|
+
* destroys and recreates the row, so the second click lands on a different
|
|
207
|
+
* DOM element and the browser refuses to synthesize a dblclick. Tracking
|
|
208
|
+
* the click on the controller (which survives the re-render) sidesteps that. */
|
|
209
|
+
private _lastSelectClickPath;
|
|
210
|
+
private _lastSelectClickTime;
|
|
202
211
|
selectedPaths: Set<string>;
|
|
203
212
|
insertResult: InsertArrayResult<T> | null | undefined;
|
|
204
213
|
searchText: string | null | undefined;
|
|
@@ -430,6 +439,8 @@ export declare class TreeController<T> {
|
|
|
430
439
|
getNodeAllowedDropPositions(node: LTreeNode<T>): DropPosition[] | null;
|
|
431
440
|
/** Get whether a node is draggable (proxies LTree resolution: callback > member > node property). */
|
|
432
441
|
getNodeIsDraggable(node: LTreeNode<T>): boolean;
|
|
442
|
+
/** Get whether a node accepts drops (proxies LTree resolution: callback > member > node property). */
|
|
443
|
+
getNodeIsDropAllowed(node: LTreeNode<T>): boolean;
|
|
433
444
|
/** Get whether a node is collapsible (proxies LTree resolution: callback > member > node property). */
|
|
434
445
|
getNodeIsCollapsible(node: LTreeNode<T>): boolean;
|
|
435
446
|
/** Calculate drop position from cursor location within an element (before/after/child).
|
|
@@ -46,6 +46,14 @@ export class TreeController {
|
|
|
46
46
|
* from the focused node. Set on first Shift action, advances on subsequent
|
|
47
47
|
* Shift actions, cleared on any plain navigation. Not exposed via props. */
|
|
48
48
|
_shiftCursor = null;
|
|
49
|
+
/** Manual double-click detection state for clickBehavior='select'. We can't
|
|
50
|
+
* rely on the browser's native dblclick event in this mode because the first
|
|
51
|
+
* click triggers focus → _setFocusedNode bumps node._rev → flat-mode {#each}
|
|
52
|
+
* destroys and recreates the row, so the second click lands on a different
|
|
53
|
+
* DOM element and the browser refuses to synthesize a dblclick. Tracking
|
|
54
|
+
* the click on the controller (which survives the re-render) sidesteps that. */
|
|
55
|
+
_lastSelectClickPath = null;
|
|
56
|
+
_lastSelectClickTime = 0;
|
|
49
57
|
selectedPaths = $state.raw(new Set());
|
|
50
58
|
insertResult = $state.raw(null);
|
|
51
59
|
searchText = $state(undefined);
|
|
@@ -257,7 +265,7 @@ export class TreeController {
|
|
|
257
265
|
this.onRenderCompleteHandler = props.onRenderComplete;
|
|
258
266
|
// ── Create LTree ────────────────────────────────────────────────
|
|
259
267
|
// svelte-ignore non_reactive_update
|
|
260
|
-
this.tree = createLTree(props.idMember, props.pathMember, props.parentPathMember, props.levelMember, props.hasChildrenMember, props.isExpandedMember, props.getIsExpandedCallback, props.isSelectableMember, props.getIsSelectableCallback, props.isSelectedMember, props.getIsSelectedCallback, props.isDraggableMember, props.getIsDraggableCallback, props.isDropAllowedMember, props.allowedDropPositionsMember, props.displayValueMember, props.getDisplayValueCallback, props.searchValueMember, props.getSearchValueCallback, props.getAllowedDropPositionsCallback, props.isCollapsibleMember, props.getIsCollapsibleCallback, props.orderMember, this.treeId, this.treePathSeparator, props.expandLevel, props.shouldUseInternalSearchIndex, props.initializeIndexCallback, props.indexerBatchSize, props.indexerTimeout, {
|
|
268
|
+
this.tree = createLTree(props.idMember, props.pathMember, props.parentPathMember, props.levelMember, props.hasChildrenMember, props.isExpandedMember, props.getIsExpandedCallback, props.isSelectableMember, props.getIsSelectableCallback, props.isSelectedMember, props.getIsSelectedCallback, props.isDraggableMember, props.getIsDraggableCallback, props.isDropAllowedMember, props.getIsDropAllowedCallback, props.allowedDropPositionsMember, props.displayValueMember, props.getDisplayValueCallback, props.searchValueMember, props.getSearchValueCallback, props.getAllowedDropPositionsCallback, props.isCollapsibleMember, props.getIsCollapsibleCallback, props.orderMember, this.treeId, this.treePathSeparator, props.expandLevel, props.shouldUseInternalSearchIndex, props.initializeIndexCallback, props.indexerBatchSize, props.indexerTimeout, {
|
|
261
269
|
shouldDisplayDebugInformation: props.shouldDisplayDebugInformation,
|
|
262
270
|
isSorted: props.isSorted,
|
|
263
271
|
sortCallback: props.sortCallback
|
|
@@ -460,6 +468,22 @@ export class TreeController {
|
|
|
460
468
|
}
|
|
461
469
|
});
|
|
462
470
|
});
|
|
471
|
+
// Virtual scroll: clamp scroll position when content shrinks (e.g. after filter)
|
|
472
|
+
// Without this, vsScrollTop stays at its old (large) value while vsTotalHeight
|
|
473
|
+
// drops, so vsStartIndex falls out of range and the rendered slice is empty —
|
|
474
|
+
// the scroll appears stuck because the browser silently clamps the container's
|
|
475
|
+
// actual scrollTop but our derived state never re-reads it.
|
|
476
|
+
$effect(() => {
|
|
477
|
+
if (!this.vsActive || !this.vsContainerRef)
|
|
478
|
+
return;
|
|
479
|
+
const maxScrollTop = Math.max(0, this.vsTotalHeight - this.vsContainerRef.clientHeight);
|
|
480
|
+
if (this.vsScrollTop > maxScrollTop) {
|
|
481
|
+
this.vsScrollTop = maxScrollTop;
|
|
482
|
+
if (this.vsContainerRef.scrollTop > maxScrollTop) {
|
|
483
|
+
this.vsContainerRef.scrollTop = maxScrollTop;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
463
487
|
// Context menu global event listeners
|
|
464
488
|
$effect(() => {
|
|
465
489
|
if (this.contextMenuVisible) {
|
|
@@ -1093,7 +1117,11 @@ export class TreeController {
|
|
|
1093
1117
|
}
|
|
1094
1118
|
/** Get whether a node is draggable (proxies LTree resolution: callback > member > node property). */
|
|
1095
1119
|
getNodeIsDraggable(node) {
|
|
1096
|
-
return this.tree?.getNodeIsDraggable(node) ??
|
|
1120
|
+
return this.tree?.getNodeIsDraggable(node) ?? false;
|
|
1121
|
+
}
|
|
1122
|
+
/** Get whether a node accepts drops (proxies LTree resolution: callback > member > node property). */
|
|
1123
|
+
getNodeIsDropAllowed(node) {
|
|
1124
|
+
return this.tree?.getNodeIsDropAllowed(node) ?? false;
|
|
1097
1125
|
}
|
|
1098
1126
|
/** Get whether a node is collapsible (proxies LTree resolution: callback > member > node property). */
|
|
1099
1127
|
getNodeIsCollapsible(node) {
|
|
@@ -1365,6 +1393,32 @@ export class TreeController {
|
|
|
1365
1393
|
if (this.contextMenuVisible) {
|
|
1366
1394
|
this.closeContextMenu();
|
|
1367
1395
|
}
|
|
1396
|
+
// Manual dblclick detection for clickBehavior='select' — see the comment
|
|
1397
|
+
// on _lastSelectClickPath for why the browser's native dblclick can't be
|
|
1398
|
+
// trusted in this mode. Threshold matches Windows' default double-click
|
|
1399
|
+
// interval (500ms is the system default; we use a slightly tighter 400ms
|
|
1400
|
+
// to avoid coupling unrelated clicks).
|
|
1401
|
+
if (this.clickBehavior === 'select' && !modifiers?.ctrl && !modifiers?.shift) {
|
|
1402
|
+
const now = Date.now();
|
|
1403
|
+
const isDouble = this._lastSelectClickPath === node.path &&
|
|
1404
|
+
now - this._lastSelectClickTime < 400;
|
|
1405
|
+
if (isDouble) {
|
|
1406
|
+
this._lastSelectClickPath = null;
|
|
1407
|
+
this._lastSelectClickTime = 0;
|
|
1408
|
+
const canonical = this.tree.getNodeByPath(node.path) ?? node;
|
|
1409
|
+
if (canonical.hasChildren && canonical.isCollapsible !== false) {
|
|
1410
|
+
if (canonical.isExpanded) {
|
|
1411
|
+
this.collapseNodes(canonical.path);
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
this.expandNodes(canonical.path);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
this._lastSelectClickPath = node.path;
|
|
1420
|
+
this._lastSelectClickTime = now;
|
|
1421
|
+
}
|
|
1368
1422
|
// In single mode, mouse Ctrl/Shift+click degrade to plain click. Programmatic
|
|
1369
1423
|
// callers (highlightNode with mode='toggle'/'range') pass forceMultiSemantics
|
|
1370
1424
|
// to opt out of the gate — the API contract should not depend on selectionMode.
|
|
@@ -1943,6 +1997,11 @@ export class TreeController {
|
|
|
1943
1997
|
// selectable (preserves prior highlight state for unselectable rows).
|
|
1944
1998
|
if (node.isSelectable && !this.highlightedPaths.has(node.path)) {
|
|
1945
1999
|
requestAnimationFrame(() => {
|
|
2000
|
+
// Esc-cancel can fire dragend before this rAF runs, leaving the source
|
|
2001
|
+
// node "stuck" highlighted after a cancelled drag. Bail if the drag is
|
|
2002
|
+
// no longer in progress.
|
|
2003
|
+
if (!this.isDragInProgress)
|
|
2004
|
+
return;
|
|
1946
2005
|
this._clearAllHighlightFlags();
|
|
1947
2006
|
node.isHighlighted = true;
|
|
1948
2007
|
node._rev = (node._rev || 0) + 1;
|
|
@@ -15,9 +15,9 @@ export function createLTreeNode(data) {
|
|
|
15
15
|
children: {},
|
|
16
16
|
hasChildren: false,
|
|
17
17
|
data: undefined,
|
|
18
|
-
isDraggable:
|
|
18
|
+
isDraggable: false,
|
|
19
19
|
isCollapsible: true,
|
|
20
|
-
isDropAllowed:
|
|
20
|
+
isDropAllowed: false,
|
|
21
21
|
allowedDropPositions: undefined,
|
|
22
22
|
visualState: VisualState.notSelected,
|
|
23
23
|
isExpanded: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Index } from 'flexsearch';
|
|
2
2
|
import { type LTreeNode } from './ltree-node.svelte';
|
|
3
3
|
import type { Ltree } from './types.js';
|
|
4
|
-
export declare function createLTree<T>(_idMember: string, _pathMember: string, _parentPathMember?: string | null | undefined, _levelMember?: string | null | undefined, _hasChildrenMember?: string | null | undefined, _isExpandedMember?: string | null | undefined, _getIsExpandedCallback?: (node: LTreeNode<T>) => boolean, _isSelectableMember?: string | null | undefined, _getIsSelectableCallback?: (node: LTreeNode<T>) => boolean, _isSelectedMember?: string | null | undefined, _getIsSelectedCallback?: (node: LTreeNode<T>) => boolean, _isDraggableMember?: string | null | undefined, _getIsDraggableCallback?: (node: LTreeNode<T>) => boolean, _isDropAllowedMember?: string | null | undefined, _allowedDropPositionsMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => import('./types.js').DropPosition[] | null | undefined, _isCollapsibleMember?: string | null | undefined, _getIsCollapsibleCallback?: (node: LTreeNode<T>) => boolean, _orderMember?: string | null | undefined, _treeId?: string, _treePathSeparator?: string | null | undefined, _expandLevel?: number | null | undefined, _shouldUseInternalSearchIndex?: boolean | null | undefined, _initializeIndexCallback?: () => Index, _indexerBatchSize?: number | null | undefined, _indexerTimeout?: number | null | undefined, opts?: Partial<Ltree<T>>): Ltree<T>;
|
|
4
|
+
export declare function createLTree<T>(_idMember: string, _pathMember: string, _parentPathMember?: string | null | undefined, _levelMember?: string | null | undefined, _hasChildrenMember?: string | null | undefined, _isExpandedMember?: string | null | undefined, _getIsExpandedCallback?: (node: LTreeNode<T>) => boolean, _isSelectableMember?: string | null | undefined, _getIsSelectableCallback?: (node: LTreeNode<T>) => boolean, _isSelectedMember?: string | null | undefined, _getIsSelectedCallback?: (node: LTreeNode<T>) => boolean, _isDraggableMember?: string | null | undefined, _getIsDraggableCallback?: (node: LTreeNode<T>) => boolean, _isDropAllowedMember?: string | null | undefined, _getIsDropAllowedCallback?: (node: LTreeNode<T>) => boolean, _allowedDropPositionsMember?: string | null | undefined, _displayValueMember?: string | null | undefined, _getDisplayValueCallback?: (node: LTreeNode<T>) => string, _searchValueMember?: string | null | undefined, _getSearchValueCallback?: (node: LTreeNode<T>) => string, _getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => import('./types.js').DropPosition[] | null | undefined, _isCollapsibleMember?: string | null | undefined, _getIsCollapsibleCallback?: (node: LTreeNode<T>) => boolean, _orderMember?: string | null | undefined, _treeId?: string, _treePathSeparator?: string | null | undefined, _expandLevel?: number | null | undefined, _shouldUseInternalSearchIndex?: boolean | null | undefined, _initializeIndexCallback?: () => Index, _indexerBatchSize?: number | null | undefined, _indexerTimeout?: number | null | undefined, opts?: Partial<Ltree<T>>): Ltree<T>;
|
|
@@ -9,7 +9,7 @@ import { perfStart, perfEnd, perfSummary } from '../perf-logger.js';
|
|
|
9
9
|
function getField(item, member) {
|
|
10
10
|
return item[member];
|
|
11
11
|
}
|
|
12
|
-
export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _getIsExpandedCallback, _isSelectableMember, _getIsSelectableCallback, _isSelectedMember, _getIsSelectedCallback, _isDraggableMember, _getIsDraggableCallback, _isDropAllowedMember, _allowedDropPositionsMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _getAllowedDropPositionsCallback, _isCollapsibleMember, _getIsCollapsibleCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
|
|
12
|
+
export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMember, _hasChildrenMember, _isExpandedMember, _getIsExpandedCallback, _isSelectableMember, _getIsSelectableCallback, _isSelectedMember, _getIsSelectedCallback, _isDraggableMember, _getIsDraggableCallback, _isDropAllowedMember, _getIsDropAllowedCallback, _allowedDropPositionsMember, _displayValueMember, _getDisplayValueCallback, _searchValueMember, _getSearchValueCallback, _getAllowedDropPositionsCallback, _isCollapsibleMember, _getIsCollapsibleCallback, _orderMember, _treeId, _treePathSeparator, _expandLevel, _shouldUseInternalSearchIndex, _initializeIndexCallback, _indexerBatchSize, _indexerTimeout, opts) {
|
|
13
13
|
let shouldCalculateParentPath = isEmptyString(_parentPathMember);
|
|
14
14
|
let shouldCalculateLevel = isEmptyString(_levelMember);
|
|
15
15
|
let shouldCalculateHasChildren = isEmptyString(_hasChildrenMember);
|
|
@@ -76,6 +76,7 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
76
76
|
isDraggableMember: _isDraggableMember,
|
|
77
77
|
getIsDraggableCallback: _getIsDraggableCallback,
|
|
78
78
|
isDropAllowedMember: _isDropAllowedMember,
|
|
79
|
+
getIsDropAllowedCallback: _getIsDropAllowedCallback,
|
|
79
80
|
allowedDropPositionsMember: _allowedDropPositionsMember,
|
|
80
81
|
hasChildrenMember: _hasChildrenMember,
|
|
81
82
|
displayValueMember: _displayValueMember,
|
|
@@ -213,11 +214,15 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
213
214
|
node.isSelected = _getIsSelectedCallback(node);
|
|
214
215
|
else if (!shouldCalculateIsSelected)
|
|
215
216
|
node.isSelected = getField(row, _isSelectedMember);
|
|
216
|
-
if (
|
|
217
|
+
if (_getIsDraggableCallback)
|
|
218
|
+
node.isDraggable = _getIsDraggableCallback(node);
|
|
219
|
+
else if (!shouldCalculateIsDraggable)
|
|
217
220
|
node.isDraggable = getField(row, _isDraggableMember);
|
|
218
221
|
if (!shouldCalculateIsCollapsible)
|
|
219
222
|
node.isCollapsible = getField(row, _isCollapsibleMember);
|
|
220
|
-
if (
|
|
223
|
+
if (_getIsDropAllowedCallback)
|
|
224
|
+
node.isDropAllowed = _getIsDropAllowedCallback(node);
|
|
225
|
+
else if (!shouldCalculateIsDropAllowed)
|
|
221
226
|
node.isDropAllowed = getField(row, _isDropAllowedMember);
|
|
222
227
|
if (!shouldCalculateAllowedDropPositions)
|
|
223
228
|
node.allowedDropPositions = getField(row, _allowedDropPositionsMember);
|
|
@@ -707,6 +712,13 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
707
712
|
return getField(node.data, _isDraggableMember);
|
|
708
713
|
return node.isDraggable;
|
|
709
714
|
},
|
|
715
|
+
getNodeIsDropAllowed(node) {
|
|
716
|
+
if (this.getIsDropAllowedCallback)
|
|
717
|
+
return this.getIsDropAllowedCallback(node);
|
|
718
|
+
if (!shouldCalculateIsDropAllowed && node.data)
|
|
719
|
+
return getField(node.data, _isDropAllowedMember);
|
|
720
|
+
return node.isDropAllowed;
|
|
721
|
+
},
|
|
710
722
|
getNodeIsCollapsible(node) {
|
|
711
723
|
if (this.getIsCollapsibleCallback)
|
|
712
724
|
return this.getIsCollapsibleCallback(node);
|
|
@@ -1339,11 +1351,15 @@ export function createLTree(_idMember, _pathMember, _parentPathMember, _levelMem
|
|
|
1339
1351
|
node.isSelected = _getIsSelectedCallback(node);
|
|
1340
1352
|
else if (!shouldCalculateIsSelected)
|
|
1341
1353
|
node.isSelected = getField(row, _isSelectedMember);
|
|
1342
|
-
if (
|
|
1354
|
+
if (_getIsDraggableCallback)
|
|
1355
|
+
node.isDraggable = _getIsDraggableCallback(node);
|
|
1356
|
+
else if (!shouldCalculateIsDraggable)
|
|
1343
1357
|
node.isDraggable = getField(row, _isDraggableMember);
|
|
1344
1358
|
if (!shouldCalculateIsCollapsible)
|
|
1345
1359
|
node.isCollapsible = getField(row, _isCollapsibleMember);
|
|
1346
|
-
if (
|
|
1360
|
+
if (_getIsDropAllowedCallback)
|
|
1361
|
+
node.isDropAllowed = _getIsDropAllowedCallback(node);
|
|
1362
|
+
else if (!shouldCalculateIsDropAllowed)
|
|
1347
1363
|
node.isDropAllowed = getField(row, _isDropAllowedMember);
|
|
1348
1364
|
if (!shouldCalculateAllowedDropPositions)
|
|
1349
1365
|
node.allowedDropPositions = getField(row, _allowedDropPositionsMember);
|
package/dist/ltree/types.d.ts
CHANGED
|
@@ -97,6 +97,7 @@ export interface Ltree<T> {
|
|
|
97
97
|
isDraggableMember: string | null | undefined;
|
|
98
98
|
getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
|
|
99
99
|
isDropAllowedMember: string | null | undefined;
|
|
100
|
+
getIsDropAllowedCallback?: (node: LTreeNode<T>) => boolean;
|
|
100
101
|
allowedDropPositionsMember: string | null | undefined;
|
|
101
102
|
getAllowedDropPositionsCallback?: (node: LTreeNode<T>) => DropPosition[] | null | undefined;
|
|
102
103
|
isCollapsibleMember: string | null | undefined;
|
|
@@ -104,6 +105,7 @@ export interface Ltree<T> {
|
|
|
104
105
|
shouldDisplayDebugInformation: boolean | null | undefined;
|
|
105
106
|
getNodeAllowedDropPositions(node: LTreeNode<T>): DropPosition[] | null | undefined;
|
|
106
107
|
getNodeIsDraggable(node: LTreeNode<T>): boolean;
|
|
108
|
+
getNodeIsDropAllowed(node: LTreeNode<T>): boolean;
|
|
107
109
|
getNodeIsCollapsible(node: LTreeNode<T>): boolean;
|
|
108
110
|
get tree(): LTreeNode<T>[];
|
|
109
111
|
/** Flat array of all visible nodes in render order (depth-first, respects isExpanded) */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
@keyframes — all animation declarations live here.
|
|
3
|
+
============================================================================= */
|
|
4
|
+
|
|
5
|
+
/* Loading spinner (used by .ltree-loading-spinner in loading.css). */
|
|
6
|
+
@keyframes ltree-spin {
|
|
7
|
+
to {
|
|
8
|
+
transform: rotate(360deg);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Drop-arrow bounce indicator (used by drag-drop.css drop-arrow pseudo-elements). */
|
|
13
|
+
@keyframes bounce {
|
|
14
|
+
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%); }
|
|
15
|
+
40% { transform: translateY(-60%); }
|
|
16
|
+
60% { transform: translateY(-40%); }
|
|
17
|
+
}
|