@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +10 -7
  3. package/component-variables.manifest.json +6 -3
  4. package/dist/components/Node.svelte +6 -10
  5. package/dist/components/Tree.svelte +24 -0
  6. package/dist/components/Tree.svelte.d.ts +20 -2
  7. package/dist/components/TreeProvider.svelte +1 -0
  8. package/dist/constants.generated.d.ts +1 -1
  9. package/dist/constants.generated.js +1 -1
  10. package/dist/core/TreeController.svelte.d.ts +11 -0
  11. package/dist/core/TreeController.svelte.js +61 -2
  12. package/dist/ltree/ltree-node.svelte.js +2 -2
  13. package/dist/ltree/ltree.svelte.d.ts +1 -1
  14. package/dist/ltree/ltree.svelte.js +21 -5
  15. package/dist/ltree/types.d.ts +2 -0
  16. package/dist/styles/animations.css +17 -0
  17. package/dist/styles/{_base.css → base.css} +2 -0
  18. package/dist/styles/controls.css +4 -0
  19. package/dist/styles/dark-mode.css +139 -0
  20. package/dist/styles/{_drag-drop.css → drag-drop.css} +2 -6
  21. package/dist/styles/floating.css +5 -0
  22. package/dist/styles/{_loading.css → loading.css} +1 -5
  23. package/dist/styles/main.css +44 -37
  24. package/dist/styles/{_variables.css → variables.css} +56 -30
  25. package/dist/styles.css +939 -869
  26. package/package.json +6 -3
  27. /package/dist/styles/{_checkbox.css → checkbox.css} +0 -0
  28. /package/dist/styles/{_context-menu.css → context-menu.css} +0 -0
  29. /package/dist/styles/{_debug.css → debug.css} +0 -0
  30. /package/dist/styles/{_drop-zones.css → drop-zones.css} +0 -0
  31. /package/dist/styles/{_node.css → node.css} +0 -0
  32. /package/dist/styles/{_states.css → states.css} +0 -0
  33. /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-border", "category": "color", "usage": "Default border color" },
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
- function _onNodeDblClicked(event?: MouseEvent) {
327
- if (clickBehavior === 'select') {
328
- // In select mode, double-click expands/collapses
329
- toggleExpanded()
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 {
@@ -12,6 +12,7 @@
12
12
  ...controllerProps
13
13
  }: Props = $props();
14
14
 
15
+ // svelte-ignore state_referenced_locally
15
16
  const controller = createTreeController<T>(controllerProps as TreeControllerProps<T>);
16
17
 
17
18
  // Set contexts that Node.svelte expects
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "5.0.0-rc09";
1
+ export declare const VERSION = "5.0.0-rc10";
2
2
  export declare const PACKAGE_NAME = "@keenmate/svelte-treeview";
3
3
  export declare const AUTHOR = "KeenMate";
4
4
  export declare const LICENSE = "MIT";
@@ -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-rc09";
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) ?? true;
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: true,
18
+ isDraggable: false,
19
19
  isCollapsible: true,
20
- isDropAllowed: true,
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 (!shouldCalculateIsDraggable)
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 (!shouldCalculateIsDropAllowed)
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 (!shouldCalculateIsDraggable)
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 (!shouldCalculateIsDropAllowed)
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);
@@ -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
+ }