@ixfx/components 0.8.7 → 0.8.9

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 (85) hide show
  1. package/bundle/index.d.ts +743 -383
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2093 -222
  4. package/bundle/index.js.map +1 -1
  5. package/dist/crumbs.d.ts +1 -1
  6. package/dist/crumbs.js +2 -2
  7. package/dist/{data-grid-CYVPSUwh.d.ts → data-grid-C588pTgt.d.ts} +15 -2
  8. package/dist/data-grid-C588pTgt.d.ts.map +1 -0
  9. package/dist/data-grid.d.ts +3 -2
  10. package/dist/data-grid.js +95 -5
  11. package/dist/data-grid.js.map +1 -1
  12. package/dist/{detail-list-vH56z7WE.d.ts → detail-list-IKl76Wnp.d.ts} +22 -5
  13. package/dist/detail-list-IKl76Wnp.d.ts.map +1 -0
  14. package/dist/detail-list.d.ts +4 -3
  15. package/dist/detail-list.js +103 -3
  16. package/dist/detail-list.js.map +1 -1
  17. package/dist/{grid-list-Cq8ga2WZ.d.ts → grid-list-BUlUnisU.d.ts} +214 -112
  18. package/dist/grid-list-BUlUnisU.d.ts.map +1 -0
  19. package/dist/{grid-list-j5C7qtlG.js → grid-list-D18FNJRH.js} +77 -164
  20. package/dist/grid-list-D18FNJRH.js.map +1 -0
  21. package/dist/grid-list.d.ts +3 -2
  22. package/dist/grid-list.js +1 -1
  23. package/dist/incr-search.d.ts +1 -1
  24. package/dist/{index-DDREWqMm.d.ts → index--qnhBWZJ.d.ts} +2 -2
  25. package/dist/{index-DDREWqMm.d.ts.map → index--qnhBWZJ.d.ts.map} +1 -1
  26. package/dist/index.d.ts +212 -253
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +13 -12
  29. package/dist/index.js.map +1 -1
  30. package/dist/{interaction-B9AAlsFO.js → interaction-BskmlA9t.js} +1 -1
  31. package/dist/{labelled-slider-input-BCH9968c.js → labelled-slider-input-aEYDvW2S.js} +2 -2
  32. package/dist/{labelled-slider-input-BCH9968c.js.map → labelled-slider-input-aEYDvW2S.js.map} +1 -1
  33. package/dist/labelled-slider-input.js +1 -1
  34. package/dist/{list-item-animation-DuiwgN3l.js → list-item-animation-BUoEKi5s.js} +29 -10
  35. package/dist/list-item-animation-BUoEKi5s.js.map +1 -0
  36. package/dist/list-native-drag-controller-CFyrkKUj.d.ts +61 -0
  37. package/dist/list-native-drag-controller-CFyrkKUj.d.ts.map +1 -0
  38. package/dist/list-native-drag-controller-DxD6Yjci.js +567 -0
  39. package/dist/list-native-drag-controller-DxD6Yjci.js.map +1 -0
  40. package/dist/{menu-HMoxCPY2.js → menu-CMXhIaNZ.js} +3 -3
  41. package/dist/{menu-HMoxCPY2.js.map → menu-CMXhIaNZ.js.map} +1 -1
  42. package/dist/{menu-item-BQDYxk5A.js → menu-item-BYGQzyCe.js} +2 -2
  43. package/dist/{menu-item-BQDYxk5A.js.map → menu-item-BYGQzyCe.js.map} +1 -1
  44. package/dist/menu.js +2 -2
  45. package/dist/miller.d.ts +2 -2
  46. package/dist/miller.js +1 -1
  47. package/dist/narrowed-text.js +1 -1
  48. package/dist/plots.js +1 -1
  49. package/dist/select-horiz.js +1 -1
  50. package/dist/slider-input-BxURAAmD.d.ts.map +1 -1
  51. package/dist/{slider-input-WR8_RwHg.js → slider-input-Cb5uSV42.js} +78 -11
  52. package/dist/slider-input-Cb5uSV42.js.map +1 -0
  53. package/dist/slider-input.js +1 -1
  54. package/dist/{tree-CozQj01a.js → tree-a14n9y7i.js} +1055 -32
  55. package/dist/tree-a14n9y7i.js.map +1 -0
  56. package/dist/{tree-component-BMpxpdTi.d.ts → tree-component-BW2iCFq9.d.ts} +108 -4
  57. package/dist/tree-component-BW2iCFq9.d.ts.map +1 -0
  58. package/dist/tree.d.ts +102 -4
  59. package/dist/tree.d.ts.map +1 -1
  60. package/dist/tree.js +2 -2
  61. package/dist/{vertical-list-BGn9nyKh.d.ts → vertical-list-CHrXmmQ3.d.ts} +21 -4
  62. package/dist/vertical-list-CHrXmmQ3.d.ts.map +1 -0
  63. package/dist/vertical-list.d.ts +3 -2
  64. package/dist/vertical-list.js +103 -3
  65. package/dist/vertical-list.js.map +1 -1
  66. package/dist/{xy-axis-CrrVdDFe.js → xy-axis-COKw338Q.js} +2 -2
  67. package/dist/xy-axis-COKw338Q.js.map +1 -0
  68. package/dist/xy-axis-DQqd2yXJ.d.ts.map +1 -1
  69. package/docs-user/data-grid.md +30 -0
  70. package/docs-user/detail-list.md +29 -0
  71. package/docs-user/grid-list.md +34 -4
  72. package/docs-user/index.json +2 -2
  73. package/docs-user/tree.md +76 -1
  74. package/docs-user/vertical-list.md +30 -1
  75. package/package.json +1 -1
  76. package/dist/data-grid-CYVPSUwh.d.ts.map +0 -1
  77. package/dist/detail-list-vH56z7WE.d.ts.map +0 -1
  78. package/dist/grid-list-Cq8ga2WZ.d.ts.map +0 -1
  79. package/dist/grid-list-j5C7qtlG.js.map +0 -1
  80. package/dist/list-item-animation-DuiwgN3l.js.map +0 -1
  81. package/dist/slider-input-WR8_RwHg.js.map +0 -1
  82. package/dist/tree-CozQj01a.js.map +0 -1
  83. package/dist/tree-component-BMpxpdTi.d.ts.map +0 -1
  84. package/dist/vertical-list-BGn9nyKh.d.ts.map +0 -1
  85. package/dist/xy-axis-CrrVdDFe.js.map +0 -1
@@ -3,22 +3,146 @@ import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrA
3
3
  import { t as themeFallbacks } from "./fallbacks-BcKGq-2F.js";
4
4
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
5
  import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-C05WjbZh.js";
6
- import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-DuiwgN3l.js";
7
- import "./interaction-B9AAlsFO.js";
6
+ import { t as ListNativeDragController } from "./list-native-drag-controller-DxD6Yjci.js";
7
+ import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
8
+ import "./interaction-BskmlA9t.js";
8
9
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
9
10
  import { LitElement, css, html, nothing } from "lit";
10
11
  import { property, state } from "lit/decorators.js";
11
12
  import { createCommandRegistry } from "@clinth/ui-commands";
12
13
  import { repeat } from "lit/directives/repeat.js";
13
- //#region src/common/tree-data-model.ts
14
+ //#region src/common/tree-drop-apply.ts
14
15
  function findNode(root, key) {
15
16
  if (root.item.key === key) return root;
16
- if (!root.children) return void 0;
17
- for (const child of root.children) {
17
+ for (const child of root.children ?? []) {
18
18
  const found = findNode(child, key);
19
19
  if (found) return found;
20
20
  }
21
21
  }
22
+ function findParent(root, key, parent = void 0) {
23
+ for (const child of root.children ?? []) {
24
+ if (child.item.key === key) return parent === void 0 ? root : parent;
25
+ const found = findParent(child, key, child);
26
+ if (found) return found;
27
+ }
28
+ }
29
+ /** True when `key` is `ancestorKey`, or a descendant of it. */
30
+ function isSelfOrDescendant(root, ancestorKey, key) {
31
+ const ancestor = findNode(root, ancestorKey);
32
+ return ancestor !== void 0 && findNode(ancestor, key) !== void 0;
33
+ }
34
+ /** Remove every node whose key is in `keys` (with its whole subtree), in one pass. */
35
+ function removeKeys(node, keys) {
36
+ if (!node.children) return node;
37
+ return {
38
+ ...node,
39
+ children: node.children.filter((c) => !keys.has(c.item.key)).map((c) => removeKeys(c, keys))
40
+ };
41
+ }
42
+ function childrenOf(root, parentKey) {
43
+ if (parentKey === void 0) return root.children ?? [];
44
+ return findNode(root, parentKey)?.children ?? [];
45
+ }
46
+ /** Return a new root with `parentKey`'s children replaced (root itself when `parentKey` is undefined). */
47
+ function withChildren(node, parentKey, children) {
48
+ if (parentKey === void 0) return {
49
+ ...node,
50
+ children
51
+ };
52
+ if (node.item.key === parentKey) return {
53
+ ...node,
54
+ children
55
+ };
56
+ if (!node.children) return node;
57
+ return {
58
+ ...node,
59
+ children: node.children.map((c) => withChildren(c, parentKey, children))
60
+ };
61
+ }
62
+ function clamp$1(n, min, max) {
63
+ return Math.max(min, Math.min(max, n));
64
+ }
65
+ /**
66
+ * Drop keys whose ancestor is also being dragged — only the top-most selected
67
+ * nodes move; their subtrees travel with them. Preserves input order.
68
+ */
69
+ function pruneDraggedDescendants(root, keys) {
70
+ const set = new Set(keys);
71
+ return keys.filter((key) => {
72
+ let parent = findParent(root, key);
73
+ while (parent) {
74
+ if (set.has(parent.item.key)) return false;
75
+ parent = findParent(root, parent.item.key);
76
+ }
77
+ return true;
78
+ });
79
+ }
80
+ /**
81
+ * Framework-agnostic drop application. Given the current immutable `root` and a
82
+ * resolved drop context, returns a **new** root with the drop applied, or the
83
+ * unchanged `root` when the drop is invalid (dragged node into its own subtree,
84
+ * missing reference, empty payload). Never throws, never mutates.
85
+ *
86
+ * The insertion point is re-derived from `target.referenceKey` + `target.position`
87
+ * against the post-removal tree, so same-parent reorders are off-by-one-safe and
88
+ * multi-node drops land as siblings in drag order. `target.index` is honoured only
89
+ * for `position: 'inside'` (0 = first child, large = append).
90
+ */
91
+ function applyTreeDrop(root, ctx) {
92
+ const { target, source } = ctx;
93
+ let nodes;
94
+ let removeSet = /* @__PURE__ */ new Set();
95
+ if (source === `internal`) {
96
+ const keys = pruneDraggedDescendants(root, [...ctx.draggedKeys]);
97
+ if (keys.length === 0) return root;
98
+ if (target.parentKey !== void 0) {
99
+ for (const key of keys) if (isSelfOrDescendant(root, key, target.parentKey)) return root;
100
+ }
101
+ if (target.position !== `inside`) {
102
+ for (const key of keys) if (isSelfOrDescendant(root, key, target.referenceKey)) return root;
103
+ }
104
+ const resolved = keys.map((k) => findNode(root, k));
105
+ if (resolved.includes(void 0)) return root;
106
+ nodes = resolved;
107
+ removeSet = new Set(keys);
108
+ } else {
109
+ nodes = (ctx.dragged ?? []).map((n) => ({ ...n }));
110
+ if (nodes.length === 0) return root;
111
+ }
112
+ const afterRemoval = removeSet.size > 0 ? removeKeys(root, removeSet) : root;
113
+ let parentKey;
114
+ let index;
115
+ if (target.position === `inside`) {
116
+ if (findNode(afterRemoval, target.referenceKey) === void 0) return root;
117
+ parentKey = target.referenceKey;
118
+ const kids = childrenOf(afterRemoval, parentKey);
119
+ index = clamp$1(target.index, 0, kids.length);
120
+ } else {
121
+ parentKey = findParent(afterRemoval, target.referenceKey)?.item.key;
122
+ const refIdx = childrenOf(afterRemoval, parentKey).findIndex((c) => c.item.key === target.referenceKey);
123
+ if (refIdx < 0) return root;
124
+ index = target.position === `before` ? refIdx : refIdx + 1;
125
+ }
126
+ const kids = [...childrenOf(afterRemoval, parentKey)];
127
+ const next = [
128
+ ...kids.slice(0, index),
129
+ ...nodes,
130
+ ...kids.slice(index)
131
+ ];
132
+ return withChildren(afterRemoval, parentKey, next);
133
+ }
134
+ /** Build a synthetic `TreeDropTarget` for programmatic moves (see `TreeDataModel`). */
135
+ function dropTargetFor(referenceKey, position, index = position === `inside` ? Number.MAX_SAFE_INTEGER : 0) {
136
+ return {
137
+ referenceKey,
138
+ position,
139
+ index,
140
+ parentKey: position === `inside` ? referenceKey : void 0,
141
+ depth: 0
142
+ };
143
+ }
144
+ //#endregion
145
+ //#region src/common/tree-data-model.ts
22
146
  /** Return a new root with the node at `key` replaced by `replacer(node)`. */
23
147
  function mapNode(root, key, replacer) {
24
148
  if (root.item.key === key) return replacer(root);
@@ -43,11 +167,23 @@ function removeNodeFrom(root, key) {
43
167
  children: root.children.filter((c) => c.item.key !== key).map((c) => removeNodeFrom(c, key))
44
168
  };
45
169
  }
46
- /** Return a new root with the node at `key` moved to become a child of `newParentKey`. */
47
- function moveNodeTo(root, key, newParentKey) {
48
- const target = findNode(root, key);
49
- if (!target) return root;
50
- return addChildTo(removeNodeFrom(root, key), newParentKey, target);
170
+ /** Return a new root with `newChild` inserted at `index` among the children of `parentKey`. */
171
+ function insertChildAt(root, parentKey, newChild, index) {
172
+ return mapNode(root, parentKey, (parent) => {
173
+ const children = parent.children ?? [];
174
+ const clampedIndex = Math.max(0, Math.min(index, children.length));
175
+ return {
176
+ item: parent.item,
177
+ children: [
178
+ ...children.slice(0, clampedIndex),
179
+ newChild,
180
+ ...children.slice(clampedIndex)
181
+ ]
182
+ };
183
+ });
184
+ }
185
+ function getRootChildren(root) {
186
+ return root.children ?? [];
51
187
  }
52
188
  /**
53
189
  * Manages tree data for tree-like components.
@@ -175,10 +311,80 @@ var TreeDataModel = class {
175
311
  }
176
312
  /**
177
313
  * Moves the node with the given key to become a child of `newParentKey`.
314
+ * When `index` is provided, inserts at that position; otherwise appends.
315
+ * Returns the new root (connected components are notified automatically).
316
+ */
317
+ moveNode(key, newParentKey, index) {
318
+ const next = applyTreeDrop(this.#root, {
319
+ source: `internal`,
320
+ draggedKeys: [key],
321
+ target: dropTargetFor(newParentKey, `inside`, index ?? Number.MAX_SAFE_INTEGER)
322
+ });
323
+ if (next !== this.#root) {
324
+ this.#root = next;
325
+ this.#notify({
326
+ type: `root`,
327
+ root: this.#root
328
+ });
329
+ }
330
+ return this.#root;
331
+ }
332
+ /**
333
+ * Moves the node with the given key to become a sibling of `referenceKey`,
334
+ * positioned `before` or `after` it. A no-op (returns the unchanged root) when
335
+ * `key` is `referenceKey`, an ancestor of it, or either is missing.
336
+ * Returns the new root (connected components are notified automatically).
337
+ */
338
+ moveNodeRelative(key, referenceKey, position) {
339
+ const next = applyTreeDrop(this.#root, {
340
+ source: `internal`,
341
+ draggedKeys: [key],
342
+ target: dropTargetFor(referenceKey, position)
343
+ });
344
+ if (next !== this.#root) {
345
+ this.#root = next;
346
+ this.#notify({
347
+ type: `root`,
348
+ root: this.#root
349
+ });
350
+ }
351
+ return this.#root;
352
+ }
353
+ /**
354
+ * Applies a resolved drop (from `ixfx-tree-list`'s `tree-drop` event or a
355
+ * hand-built context) to the current root. No-op when the drop is invalid.
356
+ * Returns the new root (connected components are notified automatically).
357
+ */
358
+ applyDrop(ctx) {
359
+ const next = applyTreeDrop(this.#root, ctx);
360
+ if (next !== this.#root) {
361
+ this.#root = next;
362
+ this.#notify({
363
+ type: `root`,
364
+ root: this.#root
365
+ });
366
+ }
367
+ return this.#root;
368
+ }
369
+ /**
370
+ * Inserts `node` as a child of `parentKey` (or at the root level when `parentKey` is undefined).
371
+ * When `index` is provided, inserts at that position; otherwise appends.
178
372
  * Returns the new root (connected components are notified automatically).
179
373
  */
180
- moveNode(key, newParentKey) {
181
- this.#root = moveNodeTo(this.#root, key, newParentKey);
374
+ insertNode(parentKey, node, index) {
375
+ if (parentKey === void 0) {
376
+ const children = getRootChildren(this.#root);
377
+ const clampedIndex = Math.max(0, Math.min(index ?? children.length, children.length));
378
+ this.#root = {
379
+ ...this.#root,
380
+ children: [
381
+ ...children.slice(0, clampedIndex),
382
+ node,
383
+ ...children.slice(clampedIndex)
384
+ ]
385
+ };
386
+ } else if (index !== void 0) this.#root = insertChildAt(this.#root, parentKey, node, index);
387
+ else this.#root = addChildTo(this.#root, parentKey, node);
182
388
  this.#notify({
183
389
  type: `root`,
184
390
  root: this.#root
@@ -200,6 +406,360 @@ var TreeDataModel = class {
200
406
  }
201
407
  };
202
408
  //#endregion
409
+ //#region src/tree/tree-drag-registry.ts
410
+ const TREE_DRAG_MIME = `application/x-ixfx-tree`;
411
+ const registry = /* @__PURE__ */ new Map();
412
+ const weakRefs = /* @__PURE__ */ new Map();
413
+ /**
414
+ * The source id of the drag currently in flight, if any. There is only ever one
415
+ * native drag at a time, so a drop target can resolve the dragged nodes during
416
+ * `dragover` — when `DataTransfer.getData()` is unreadable (protected mode) — by
417
+ * looking this up in the registry.
418
+ */
419
+ let currentSourceId;
420
+ let sourceIdCounter = 0;
421
+ function treeDragGenerateSourceId() {
422
+ return `tree-drag-${++sourceIdCounter}-${Date.now()}`;
423
+ }
424
+ function treeDragRegister(sourceId, element, nodes, keys) {
425
+ registry.set(sourceId, {
426
+ sourceId,
427
+ nodes,
428
+ keys
429
+ });
430
+ weakRefs.set(sourceId, new WeakRef(element));
431
+ currentSourceId = sourceId;
432
+ }
433
+ function treeDragUnregister(sourceId) {
434
+ registry.delete(sourceId);
435
+ weakRefs.delete(sourceId);
436
+ if (currentSourceId === sourceId) currentSourceId = void 0;
437
+ }
438
+ /** The in-flight drag's `{ keys, nodes, element }`, or `undefined` when nothing is dragging. */
439
+ function treeDragCurrent() {
440
+ if (currentSourceId === void 0) return void 0;
441
+ const entry = registry.get(currentSourceId);
442
+ if (!entry) return void 0;
443
+ return {
444
+ sourceId: currentSourceId,
445
+ keys: entry.keys,
446
+ nodes: entry.nodes,
447
+ element: weakRefs.get(currentSourceId)?.deref()
448
+ };
449
+ }
450
+ //#endregion
451
+ //#region src/tree/tree-drop-controller.ts
452
+ const EDGE_ZONE_PX = 24;
453
+ const MAX_SCROLL_SPEED = 16;
454
+ function clamp(value, min, max) {
455
+ return Math.max(min, Math.min(max, value));
456
+ }
457
+ var TreeDropController = class {
458
+ #host;
459
+ #config;
460
+ #active = false;
461
+ #current;
462
+ #source = `internal`;
463
+ #dragKeys = [];
464
+ #dragNodes = [];
465
+ #expandTimer;
466
+ #expandPendingKey;
467
+ #scrollRaf = 0;
468
+ #pointerX = 0;
469
+ #pointerY = 0;
470
+ #boundContainer;
471
+ constructor(host, config) {
472
+ this.#host = host;
473
+ this.#config = config;
474
+ host.addController(this);
475
+ }
476
+ /** A drag is currently over this tree. */
477
+ get isDragging() {
478
+ return this.#active;
479
+ }
480
+ /** A drag is over this tree but the current position is not a valid drop. */
481
+ get isRejected() {
482
+ return this.#active && this.#current === void 0;
483
+ }
484
+ get dropState() {
485
+ return this.#current;
486
+ }
487
+ hostConnected() {
488
+ this.#bind();
489
+ }
490
+ /** Bind once the shadow `#container` exists (it does not yet at `hostConnected`). */
491
+ hostUpdated() {
492
+ this.#bind();
493
+ }
494
+ #bind() {
495
+ const container = this.#config.getScrollContainer();
496
+ if (!container || container === this.#boundContainer) return;
497
+ this.#unbind();
498
+ this.#boundContainer = container;
499
+ container.addEventListener(`dragenter`, this.#onDragEnter);
500
+ container.addEventListener(`dragover`, this.#onDragOver);
501
+ container.addEventListener(`dragleave`, this.#onDragLeave);
502
+ container.addEventListener(`drop`, this.#onDrop);
503
+ }
504
+ #unbind() {
505
+ const c = this.#boundContainer;
506
+ if (!c) return;
507
+ c.removeEventListener(`dragenter`, this.#onDragEnter);
508
+ c.removeEventListener(`dragover`, this.#onDragOver);
509
+ c.removeEventListener(`dragleave`, this.#onDragLeave);
510
+ c.removeEventListener(`drop`, this.#onDrop);
511
+ this.#boundContainer = void 0;
512
+ }
513
+ hostDisconnected() {
514
+ this.#unbind();
515
+ this.#clearExpand();
516
+ this.#stopScroll();
517
+ this.#active = false;
518
+ this.#current = void 0;
519
+ }
520
+ #onDragEnter = (event) => {
521
+ if (this.#config.getDndMode() === `none` || !event.dataTransfer) return;
522
+ if (this.#config.resolveDrag(event.dataTransfer, `hover`) === void 0) return;
523
+ event.preventDefault();
524
+ if (!this.#active) {
525
+ this.#active = true;
526
+ this.#config.onActiveChange(true);
527
+ }
528
+ };
529
+ #onDragOver = (event) => {
530
+ if (this.#config.getDndMode() === `none` || !event.dataTransfer) return;
531
+ this.#pointerX = event.clientX;
532
+ this.#pointerY = event.clientY;
533
+ const drag = this.#config.resolveDrag(event.dataTransfer, `hover`);
534
+ if (!drag) {
535
+ this.#reject(event);
536
+ return;
537
+ }
538
+ if (!this.#active) {
539
+ this.#active = true;
540
+ this.#config.onActiveChange(true);
541
+ }
542
+ this.#source = drag.source;
543
+ this.#dragKeys = drag.keys;
544
+ this.#dragNodes = drag.nodes;
545
+ const rows = this.#config.getVisibleRows();
546
+ this.#handleExpandHover(this.#rowAtPointer(rows, event.clientY));
547
+ this.#kickScroll();
548
+ const target = this.#resolveIntent(event, rows, drag.source);
549
+ if (!target) {
550
+ this.#reject(event, false);
551
+ return;
552
+ }
553
+ const ctx = {
554
+ source: drag.source,
555
+ dragged: drag.nodes.length > 0 ? drag.nodes : void 0,
556
+ draggedKeys: drag.keys,
557
+ target,
558
+ dataTransfer: event.dataTransfer
559
+ };
560
+ if (!this.#config.vet(ctx)) {
561
+ this.#reject(event, false);
562
+ return;
563
+ }
564
+ event.preventDefault();
565
+ event.dataTransfer.dropEffect = drag.source === `internal` ? `move` : `copy`;
566
+ this.#setDropState(target);
567
+ };
568
+ /** Not a valid drop here: keep the drag active for feedback but accept nothing. */
569
+ #reject(event, clearHover = true) {
570
+ if (event.dataTransfer) event.dataTransfer.dropEffect = `none`;
571
+ this.#setDropState(void 0);
572
+ if (clearHover) this.#clearExpand();
573
+ }
574
+ #onDragLeave = (event) => {
575
+ const container = this.#config.getScrollContainer();
576
+ const related = event.relatedTarget;
577
+ if (container && related && container.contains(related)) return;
578
+ this.#endHover();
579
+ };
580
+ #onDrop = (event) => {
581
+ if (!event.dataTransfer) {
582
+ this.#endHover();
583
+ return;
584
+ }
585
+ event.preventDefault();
586
+ this.#stopScroll();
587
+ this.#clearExpand();
588
+ const finish = (dropped) => {
589
+ this.#current = void 0;
590
+ this.#active = false;
591
+ this.#config.onDropStateChange(void 0);
592
+ this.#config.onActiveChange(false);
593
+ this.#config.onDragEnd(dropped);
594
+ };
595
+ if (this.#config.getDndMode() === `none`) {
596
+ finish(false);
597
+ return;
598
+ }
599
+ const drag = this.#config.resolveDrag(event.dataTransfer, `drop`);
600
+ if (!drag) {
601
+ finish(false);
602
+ return;
603
+ }
604
+ this.#dragKeys = drag.keys;
605
+ this.#dragNodes = drag.nodes;
606
+ const target = this.#resolveIntent(event, this.#config.getVisibleRows(), drag.source);
607
+ if (!target) {
608
+ finish(false);
609
+ return;
610
+ }
611
+ const ctx = {
612
+ source: drag.source,
613
+ dragged: drag.nodes.length > 0 ? drag.nodes : void 0,
614
+ draggedKeys: drag.keys,
615
+ target,
616
+ dataTransfer: event.dataTransfer
617
+ };
618
+ if (!this.#config.vet(ctx)) {
619
+ finish(false);
620
+ return;
621
+ }
622
+ const detail = {
623
+ ...ctx,
624
+ external: drag.source === `external` ? drag.nodes : void 0
625
+ };
626
+ finish(this.#config.commitDrop(detail));
627
+ };
628
+ #endHover() {
629
+ this.#clearExpand();
630
+ this.#stopScroll();
631
+ if (this.#current !== void 0) {
632
+ this.#current = void 0;
633
+ this.#config.onDropStateChange(void 0);
634
+ }
635
+ if (this.#active) {
636
+ this.#active = false;
637
+ this.#config.onActiveChange(false);
638
+ }
639
+ }
640
+ #rowAtPointer(rows, clientY) {
641
+ if (rows.length === 0) return void 0;
642
+ if (clientY < rows[0].rect.top) return rows[0];
643
+ if (clientY > rows[rows.length - 1].rect.bottom) return rows[rows.length - 1];
644
+ return rows.find((r) => clientY >= r.rect.top && clientY <= r.rect.bottom) ?? rows[rows.length - 1];
645
+ }
646
+ #parentRow(rows, index) {
647
+ const target = rows[index];
648
+ for (let i = index - 1; i >= 0; i--) if (rows[i].depth === target.depth - 1) return rows[i];
649
+ }
650
+ #resolveIntent(event, rows, source) {
651
+ if (rows.length === 0) return void 0;
652
+ const row = this.#rowAtPointer(rows, event.clientY);
653
+ if (!row) return void 0;
654
+ const rowIndex = rows.indexOf(row);
655
+ const f = clamp((event.clientY - row.rect.top) / row.rect.height, 0, 1);
656
+ let position = f < .25 ? `before` : f > .75 ? `after` : `inside`;
657
+ const isLeaf = row.node.item.isLeaf === true;
658
+ if (position === `inside` && isLeaf) position = `after`;
659
+ let insideFromExpandedAfter = false;
660
+ if (position === `after` && !isLeaf && this.#config.isExpanded(row.key)) {
661
+ position = `inside`;
662
+ insideFromExpandedAfter = true;
663
+ }
664
+ let parentKey;
665
+ let referenceKey;
666
+ let depth;
667
+ let index;
668
+ if (position === `inside`) {
669
+ parentKey = row.key;
670
+ referenceKey = row.key;
671
+ depth = row.depth + 1;
672
+ index = insideFromExpandedAfter ? 0 : row.node.children?.length ?? 0;
673
+ } else {
674
+ const indent = this.#config.getIndentPx() || 1;
675
+ const treeLeft = rows[0].rect.left;
676
+ const effDepth = clamp(Math.round((event.clientX - treeLeft) / indent), 0, row.depth);
677
+ let anchorIndex = rowIndex;
678
+ for (let up = row.depth - effDepth; up > 0; up--) {
679
+ const parent = this.#parentRow(rows, anchorIndex);
680
+ if (!parent) break;
681
+ anchorIndex = rows.indexOf(parent);
682
+ }
683
+ const anchor = rows[anchorIndex];
684
+ referenceKey = anchor.key;
685
+ depth = anchor.depth;
686
+ const parent = this.#parentRow(rows, anchorIndex);
687
+ parentKey = parent?.key;
688
+ const refIdx = (parent ? parent.node.children ?? [] : rows.filter((r) => r.depth === 0).map((r) => r.node)).findIndex((n) => n.item.key === anchor.key);
689
+ index = (refIdx < 0 ? 0 : refIdx) + (position === `after` ? 1 : 0);
690
+ }
691
+ if (source === `internal`) {
692
+ for (const key of this.#dragKeys) {
693
+ if (key === parentKey || key === referenceKey) return void 0;
694
+ if (parentKey !== void 0 && this.#config.isSelfOrDescendant(key, parentKey)) return void 0;
695
+ if (position !== `inside` && this.#config.isSelfOrDescendant(key, referenceKey)) return void 0;
696
+ }
697
+ if (this.#dragKeys.length > 0 && !this.#config.isReorderAllowed()) {
698
+ if (!this.#dragKeys.some((k) => this.#config.getParentKey(k) !== parentKey)) return void 0;
699
+ }
700
+ }
701
+ return {
702
+ parentKey,
703
+ index,
704
+ referenceKey,
705
+ position,
706
+ depth
707
+ };
708
+ }
709
+ #handleExpandHover(row) {
710
+ if (!row || row.node.item.isLeaf === true || this.#config.isExpanded(row.key)) {
711
+ this.#clearExpand();
712
+ return;
713
+ }
714
+ if (row.key === this.#expandPendingKey) return;
715
+ this.#clearExpand();
716
+ const key = this.#expandPendingKey = row.key;
717
+ this.#expandTimer = setTimeout(() => {
718
+ this.#expandTimer = void 0;
719
+ if (this.#expandPendingKey === key) {
720
+ this.#expandPendingKey = void 0;
721
+ this.#config.expandKey(key);
722
+ }
723
+ }, Math.max(0, this.#config.getExpandHoverMs()));
724
+ }
725
+ #clearExpand() {
726
+ if (this.#expandTimer !== void 0) {
727
+ clearTimeout(this.#expandTimer);
728
+ this.#expandTimer = void 0;
729
+ }
730
+ this.#expandPendingKey = void 0;
731
+ }
732
+ #kickScroll() {
733
+ if (this.#scrollRaf === 0) this.#scrollRaf = requestAnimationFrame(this.#scrollTick);
734
+ }
735
+ #scrollTick = () => {
736
+ this.#scrollRaf = 0;
737
+ if (!this.#active) return;
738
+ const container = this.#config.getScrollContainer();
739
+ if (!container) return;
740
+ const rect = container.getBoundingClientRect();
741
+ const y = this.#pointerY - rect.top;
742
+ let dy = 0;
743
+ if (y < EDGE_ZONE_PX) dy = -Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, y) / EDGE_ZONE_PX));
744
+ else if (y > rect.height - EDGE_ZONE_PX) dy = Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, rect.height - y) / EDGE_ZONE_PX));
745
+ if (dy !== 0 && this.#pointerX >= rect.left && this.#pointerX <= rect.right) {
746
+ container.scrollTop += dy;
747
+ this.#kickScroll();
748
+ }
749
+ };
750
+ #stopScroll() {
751
+ if (this.#scrollRaf !== 0) {
752
+ cancelAnimationFrame(this.#scrollRaf);
753
+ this.#scrollRaf = 0;
754
+ }
755
+ }
756
+ #setDropState(state) {
757
+ const unchanged = state === void 0 ? this.#current === void 0 : state.referenceKey === this.#current?.referenceKey && state.position === this.#current?.position && state.depth === this.#current?.depth && state.index === this.#current?.index;
758
+ this.#current = state;
759
+ if (!unchanged || this.#active) this.#config.onDropStateChange(state);
760
+ }
761
+ };
762
+ //#endregion
203
763
  //#region src/tree/tree-base.ts
204
764
  /** Above this many rows entering / leaving at once, skip the animation entirely. */
205
765
  const MAX_ANIMATED_ROWS = 50;
@@ -212,6 +772,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
212
772
  this._selectedNodes = /* @__PURE__ */ new Set();
213
773
  this.disableKeyboardNavigation = false;
214
774
  this.registry = void 0;
775
+ this.itemDnd = `none`;
776
+ this.dndExpandHoverMs = 500;
777
+ this.reorderable = true;
778
+ this.applyInternalDrop = true;
215
779
  this._expandedKeys = /* @__PURE__ */ new Set();
216
780
  this.tickled = new TickledController(this, {
217
781
  getItems: () => {
@@ -220,6 +784,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
220
784
  enabled: true
221
785
  }));
222
786
  },
787
+ hoverDisabled: () => this.#drop?.isDragging === true || this.#nativeDrag?.isDragging === true,
223
788
  onStateChange: () => {
224
789
  this.setAttribute(`data-input-mode`, this.tickled.inputMode);
225
790
  }
@@ -234,8 +799,14 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
234
799
  this.#lastNodeByKey = /* @__PURE__ */ new Map();
235
800
  this.#enteringKeys = /* @__PURE__ */ new Set();
236
801
  this.#leaving = /* @__PURE__ */ new Map();
237
- this.#leavingAnimating = /* @__PURE__ */ new Set();
802
+ this.#leavingAnimations = /* @__PURE__ */ new Map();
238
803
  this.#treeAnimReady = false;
804
+ this.#dragSourceId = ``;
805
+ this.#nativeDragKeys = /* @__PURE__ */ new Set();
806
+ this.#internalDropHandled = false;
807
+ this.#onContainerScroll = () => {
808
+ this.#rowsCache = void 0;
809
+ };
239
810
  this.#onKeyDown = (event) => {
240
811
  if (this.search.isOpen) return;
241
812
  const nodes = this.#getAllNodes();
@@ -387,8 +958,40 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
387
958
  #lastNodeByKey;
388
959
  #enteringKeys;
389
960
  #leaving;
390
- #leavingAnimating;
961
+ #leavingAnimations;
391
962
  #treeAnimReady;
963
+ #dragSourceId;
964
+ #postDragSuppressTimer;
965
+ #dropState;
966
+ #drop;
967
+ #nativeDrag;
968
+ /** Keys of the nodes currently being dragged out (for `.dragging` styling). */
969
+ #nativeDragKeys;
970
+ /** Set by `commitDrop` for a same-tree internal drop, read by the native `onEnd` to avoid a duplicate `tree-drag-end`. */
971
+ #internalDropHandled;
972
+ /** Per-drag cache of measured rows; invalidated on scroll / update. */
973
+ #rowsCache;
974
+ #onContainerScroll;
975
+ get nativeDrag() {
976
+ return this.#nativeDrag;
977
+ }
978
+ get dropState() {
979
+ return this.#dropState;
980
+ }
981
+ /** Keys of the nodes currently being dragged out of this tree. */
982
+ get nativeDragKeys() {
983
+ return this.#nativeDragKeys;
984
+ }
985
+ #cancelLeavingAnimation(key) {
986
+ this.#leavingAnimations.get(key)?.cancel();
987
+ this.#leavingAnimations.delete(key);
988
+ }
989
+ #cancelAllLeavingAnimations() {
990
+ for (const animation of this.#leavingAnimations.values()) try {
991
+ animation.cancel();
992
+ } catch {}
993
+ this.#leavingAnimations.clear();
994
+ }
392
995
  willUpdate(changedProps) {
393
996
  super.willUpdate(changedProps);
394
997
  this.#diffVisibleNodes();
@@ -407,12 +1010,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
407
1010
  }
408
1011
  }
409
1012
  for (const key of [...this.#leaving.keys()]) if (nowKeys.has(key)) {
1013
+ this.#cancelLeavingAnimation(key);
410
1014
  this.#leaving.delete(key);
411
- this.#leavingAnimating.delete(key);
412
1015
  }
413
1016
  if (this.#leaving.size > MAX_ANIMATED_ROWS) {
1017
+ this.#cancelAllLeavingAnimations();
414
1018
  this.#leaving.clear();
415
- this.#leavingAnimating.clear();
416
1019
  }
417
1020
  }
418
1021
  /** True while `node` is a fading ghost row (removed / collapsed, still animating out). */
@@ -449,18 +1052,38 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
449
1052
  if (this.#enteringKeys.size > 0) {
450
1053
  for (const el of this.renderRoot.querySelectorAll(`.item`)) {
451
1054
  const key = el.dataset.key;
452
- if (key !== void 0 && this.#enteringKeys.has(key)) animateListItemIn(el);
1055
+ if (key === void 0 || !this.#enteringKeys.has(key)) continue;
1056
+ if (this.#leavingAnimations.has(key)) this.#cancelLeavingAnimation(key);
1057
+ else for (const animation of el.getAnimations()) animation.cancel();
1058
+ animateListItemIn(el);
453
1059
  }
454
1060
  this.#enteringKeys.clear();
455
1061
  }
456
1062
  if (this.#leaving.size > 0) for (const el of this.renderRoot.querySelectorAll(`.item.leaving`)) {
457
1063
  const key = el.dataset.key;
458
- if (key === void 0 || this.#leavingAnimating.has(key)) continue;
459
- this.#leavingAnimating.add(key);
460
- animateListItemOut(el).then(() => {
1064
+ if (key === void 0 || this.#leavingAnimations.has(key)) continue;
1065
+ const result = animateListItemOut(el);
1066
+ const animation = result.animation;
1067
+ if (!animation) {
1068
+ result.then(() => {
1069
+ if (!this.#leaving.has(key)) return;
1070
+ this.#leaving.delete(key);
1071
+ this.requestUpdate();
1072
+ });
1073
+ continue;
1074
+ }
1075
+ this.#cancelLeavingAnimation(key);
1076
+ this.#leavingAnimations.set(key, animation);
1077
+ result.then(() => {
1078
+ if (this.#leavingAnimations.get(key) !== animation) return;
1079
+ this.#leavingAnimations.delete(key);
461
1080
  this.#leaving.delete(key);
462
- this.#leavingAnimating.delete(key);
463
1081
  this.requestUpdate();
1082
+ this.updateComplete.then(() => {
1083
+ try {
1084
+ animation.cancel();
1085
+ } catch {}
1086
+ });
464
1087
  });
465
1088
  }
466
1089
  const visible = this.root ? this.#getAllNodes() : [];
@@ -470,12 +1093,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
470
1093
  this.#treeAnimReady = true;
471
1094
  }
472
1095
  updated(changedProps) {
1096
+ this.#rowsCache = void 0;
473
1097
  if (changedProps.has(`root`) && this.root) {
474
1098
  this.#abortPending();
475
1099
  this._expandedKeys.clear();
476
1100
  this.#clearSelectionInternal();
477
1101
  this.tickled.clearTickled();
478
1102
  this._focusedDepth = 0;
1103
+ this.#cancelAllLeavingAnimations();
1104
+ this.#leaving.clear();
1105
+ this.#enteringKeys.clear();
479
1106
  }
480
1107
  if (changedProps.has(`model`)) {
481
1108
  this.#modelUnsubscribe?.();
@@ -523,13 +1150,307 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
523
1150
  });
524
1151
  this.registry = registry;
525
1152
  this.#syncSearchCommands();
1153
+ this.#initDnd();
1154
+ }
1155
+ #initDnd() {
1156
+ this.#dragSourceId = treeDragGenerateSourceId();
1157
+ this.#nativeDrag = new ListNativeDragController(this, {
1158
+ source: `tree-list`,
1159
+ isEnabled: () => this.itemDnd !== `none` && this.#anyRowDraggable(),
1160
+ resolveEventToKey: (target) => {
1161
+ if (!(target instanceof Element)) return void 0;
1162
+ return target.closest(`li[data-key]`)?.getAttribute(`data-key`) ?? void 0;
1163
+ },
1164
+ prepareSelection: (originKey) => {
1165
+ const allNodes = this.#getAllNodes();
1166
+ const originEntry = allNodes.find((n) => n.node.item.key === originKey);
1167
+ if (!originEntry) return void 0;
1168
+ const selectedNodes = new Set(this._selectedNodes);
1169
+ if (!selectedNodes.has(originEntry.node)) selectedNodes.clear();
1170
+ selectedNodes.add(originEntry.node);
1171
+ const orderedKeys = allNodes.filter((n) => selectedNodes.has(n.node)).map((n) => n.node.item.key);
1172
+ const topKeys = new Set(this.root ? pruneDraggedDescendants(this.root, orderedKeys) : orderedKeys);
1173
+ const selected = allNodes.filter((n) => topKeys.has(n.node.item.key)).map((n, i) => ({
1174
+ key: n.node.item.key,
1175
+ index: i,
1176
+ item: n.node,
1177
+ element: this.shadowRoot?.querySelector(`li[data-key="${CSS.escape(n.node.item.key)}"]`) ?? void 0
1178
+ }));
1179
+ const origin = selected.find((e) => e.key === originKey) ?? {
1180
+ key: originKey,
1181
+ index: 0,
1182
+ item: originEntry.node,
1183
+ element: void 0
1184
+ };
1185
+ if (!selected.some((e) => e.key === origin.key)) selected.unshift(origin);
1186
+ return {
1187
+ origin,
1188
+ selected
1189
+ };
1190
+ },
1191
+ getMetrics: () => ({ rowHeight: this.#getItemHeight() }),
1192
+ getRenderer: () => this.dragPreviewRenderer,
1193
+ dispatch: (detail) => {
1194
+ const keys = detail.selected.map((e) => e.key);
1195
+ const nodes = detail.selected.map((e) => e.item);
1196
+ const evt = new CustomEvent(`list-native-drag-start`, {
1197
+ detail,
1198
+ bubbles: true,
1199
+ composed: true,
1200
+ cancelable: true
1201
+ });
1202
+ const notCancelled = this.dispatchEvent(evt);
1203
+ if (notCancelled) {
1204
+ this.#nativeDragKeys = new Set(keys);
1205
+ this.#internalDropHandled = false;
1206
+ if (this.itemDnd === `internal` || this.itemDnd === `both`) treeDragRegister(this.#dragSourceId, this, nodes, keys);
1207
+ this.dispatchEvent(new CustomEvent(`tree-drag-start`, {
1208
+ detail: {
1209
+ nodes,
1210
+ keys
1211
+ },
1212
+ bubbles: true,
1213
+ composed: true
1214
+ }));
1215
+ }
1216
+ return notCancelled;
1217
+ },
1218
+ writeDataTransfer: (dataTransfer, detail) => {
1219
+ if (this.itemDnd !== `internal` && this.itemDnd !== `both`) return;
1220
+ const keys = detail.selected.map((e) => e.key);
1221
+ const nodes = detail.selected.map((e) => e.item);
1222
+ try {
1223
+ dataTransfer.setData(TREE_DRAG_MIME, JSON.stringify({
1224
+ sourceId: this.#dragSourceId,
1225
+ keys,
1226
+ nodes
1227
+ }));
1228
+ dataTransfer.setData(`text/plain`, nodes.map((n) => n.item.label).join(`\n`));
1229
+ } catch {}
1230
+ },
1231
+ onActiveChange: (active) => {
1232
+ this.toggleAttribute(`data-native-dragging`, active);
1233
+ if (!active) this.#nativeDragKeys = /* @__PURE__ */ new Set();
1234
+ this.requestUpdate();
1235
+ },
1236
+ onEnd: () => {
1237
+ treeDragUnregister(this.#dragSourceId);
1238
+ this.#nativeDragKeys = /* @__PURE__ */ new Set();
1239
+ this.#postDragSuppressTimer = setTimeout(() => {
1240
+ this.#postDragSuppressTimer = void 0;
1241
+ }, 150);
1242
+ if (!this.#internalDropHandled) this.dispatchEvent(new CustomEvent(`tree-drag-end`, {
1243
+ detail: { dropped: false },
1244
+ bubbles: true,
1245
+ composed: true
1246
+ }));
1247
+ this.#internalDropHandled = false;
1248
+ }
1249
+ });
1250
+ this.#drop = new TreeDropController(this, {
1251
+ getDndMode: () => this.itemDnd,
1252
+ getExpandHoverMs: () => this.dndExpandHoverMs,
1253
+ getVisibleRows: () => this.#getVisibleRows(),
1254
+ getIndentPx: () => this.#getIndentPx(),
1255
+ getScrollContainer: () => this.#container(),
1256
+ isSelfOrDescendant: (ancestorKey, key) => this.#isSelfOrDescendant(ancestorKey, key),
1257
+ isExpanded: (key) => this._expandedKeys.has(key),
1258
+ isReorderAllowed: () => this.reorderable,
1259
+ getParentKey: (key) => this.#findParentByKey(key)?.item.key,
1260
+ resolveDrag: (dataTransfer, phase) => this.#resolveDrag(dataTransfer, phase),
1261
+ vet: (ctx) => {
1262
+ if (this.canDrop && !this.canDrop(ctx)) return false;
1263
+ const evt = new CustomEvent(`tree-drag-over`, {
1264
+ detail: ctx,
1265
+ bubbles: true,
1266
+ composed: true,
1267
+ cancelable: true
1268
+ });
1269
+ return this.dispatchEvent(evt);
1270
+ },
1271
+ expandKey: (key) => this.#springLoadExpand(key),
1272
+ onDropStateChange: (state) => {
1273
+ this.#dropState = state;
1274
+ this.toggleAttribute(`data-drop-rejected`, this.#drop.isDragging && state === void 0);
1275
+ this.requestUpdate();
1276
+ },
1277
+ onActiveChange: (active) => {
1278
+ if (active) {
1279
+ this.#rowsCache = void 0;
1280
+ this.#container()?.addEventListener(`scroll`, this.#onContainerScroll, { passive: true });
1281
+ } else {
1282
+ this.#container()?.removeEventListener(`scroll`, this.#onContainerScroll);
1283
+ this.#rowsCache = void 0;
1284
+ this.toggleAttribute(`data-drop-rejected`, false);
1285
+ }
1286
+ this.requestUpdate();
1287
+ },
1288
+ commitDrop: (detail) => {
1289
+ if (!this.dispatchEvent(new CustomEvent(`tree-drop`, {
1290
+ detail,
1291
+ bubbles: true,
1292
+ composed: true,
1293
+ cancelable: true
1294
+ }))) return false;
1295
+ if (this.applyInternalDrop && detail.source === `internal` && this.model) this.model.applyDrop({
1296
+ source: `internal`,
1297
+ draggedKeys: detail.draggedKeys,
1298
+ target: detail.target
1299
+ });
1300
+ this.#internalDropHandled = true;
1301
+ return true;
1302
+ },
1303
+ onDragEnd: (dropped) => {
1304
+ this.#dropState = void 0;
1305
+ this.toggleAttribute(`data-drop-rejected`, false);
1306
+ this.requestUpdate();
1307
+ this.dispatchEvent(new CustomEvent(`tree-drag-end`, {
1308
+ detail: { dropped },
1309
+ bubbles: true,
1310
+ composed: true
1311
+ }));
1312
+ }
1313
+ });
1314
+ }
1315
+ #container() {
1316
+ return this.shadowRoot?.querySelector(`#container`) ?? void 0;
1317
+ }
1318
+ #getIndentPx() {
1319
+ const parsed = Number.parseInt(getComputedStyle(this).getPropertyValue(`--tree-indent`), 10);
1320
+ return Number.isNaN(parsed) ? 20 : parsed;
1321
+ }
1322
+ #getItemHeight() {
1323
+ const parsed = Number.parseInt(getComputedStyle(this).getPropertyValue(`--tree-item-height`), 10);
1324
+ return Number.isNaN(parsed) ? 30 : parsed;
1325
+ }
1326
+ /** Measured rows, cached for the duration of a drag and invalidated on scroll / update. */
1327
+ #getVisibleRows() {
1328
+ if (this.#rowsCache) return this.#rowsCache;
1329
+ const rows = [];
1330
+ for (const { node, depth } of this.#getAllNodes()) {
1331
+ const li = this.shadowRoot?.querySelector(`li[data-key="${CSS.escape(node.item.key)}"]`);
1332
+ if (li) rows.push({
1333
+ key: node.item.key,
1334
+ node,
1335
+ depth,
1336
+ rect: li.getBoundingClientRect()
1337
+ });
1338
+ }
1339
+ this.#rowsCache = rows;
1340
+ return rows;
1341
+ }
1342
+ /** Route spring-load through the real expand path so lazy children load and `expand` fires with the right depth. */
1343
+ #springLoadExpand(key) {
1344
+ if (this._expandedKeys.has(key)) return;
1345
+ const entry = this.#getAllNodes().find((n) => n.node.item.key === key);
1346
+ if (!entry || entry.node.item.isLeaf === true) return;
1347
+ this.#toggleExpand(entry.node, entry.depth);
1348
+ this.#rowsCache = void 0;
1349
+ this.requestUpdate();
1350
+ }
1351
+ #isSelfOrDescendant(ancestorKey, key) {
1352
+ if (ancestorKey === key) return true;
1353
+ let node = this.#getAllNodes().find((n) => n.node.item.key === key)?.node;
1354
+ let parent = node ? this.#findParentByKey(node.item.key) : void 0;
1355
+ while (parent) {
1356
+ if (parent.item.key === ancestorKey) return true;
1357
+ node = parent;
1358
+ parent = this.#findParentByKey(node.item.key);
1359
+ }
1360
+ return false;
1361
+ }
1362
+ #findParentByKey(key) {
1363
+ for (const { node } of this.#getAllNodes()) if (node.children?.some((c) => c.item.key === key)) return node;
1364
+ }
1365
+ /**
1366
+ * Decide whether a DataTransfer is acceptable and resolve the dragged set.
1367
+ * During `hover` the DataTransfer is read-protected, so an internal drag is
1368
+ * recovered from the module-level drag registry (there is only one at a time).
1369
+ */
1370
+ #resolveDrag(dataTransfer, phase) {
1371
+ const mode = this.itemDnd;
1372
+ if (mode === `none`) return void 0;
1373
+ const canInternal = mode === `internal` || mode === `both`;
1374
+ const canExternal = mode === `external` || mode === `both`;
1375
+ let types = [];
1376
+ try {
1377
+ types = Array.from(dataTransfer.types);
1378
+ } catch {}
1379
+ const current = treeDragCurrent();
1380
+ const looksInternal = types.includes(TREE_DRAG_MIME) || current !== void 0;
1381
+ if (canInternal && looksInternal) {
1382
+ if (current && current.keys.length > 0) {
1383
+ const keys = this.root ? pruneDraggedDescendants(this.root, current.keys) : current.keys;
1384
+ return {
1385
+ source: `internal`,
1386
+ keys,
1387
+ nodes: keys.map((k) => this.#getAllNodes().find((n) => n.node.item.key === k)?.node).filter((n) => n !== void 0)
1388
+ };
1389
+ }
1390
+ if (phase === `drop`) try {
1391
+ const raw = dataTransfer.getData(TREE_DRAG_MIME);
1392
+ if (raw) {
1393
+ const payload = JSON.parse(raw);
1394
+ return {
1395
+ source: `internal`,
1396
+ keys: payload.keys ?? [],
1397
+ nodes: payload.nodes ?? []
1398
+ };
1399
+ }
1400
+ } catch {}
1401
+ return {
1402
+ source: `internal`,
1403
+ keys: [],
1404
+ nodes: []
1405
+ };
1406
+ }
1407
+ if (canExternal && !looksInternal) {
1408
+ if (this.acceptDropTypes !== void 0) {
1409
+ const accepted = this.acceptDropTypes.trim().split(/\s+/).filter(Boolean);
1410
+ const present = Array.from(dataTransfer.types);
1411
+ if (accepted.length > 0 && !accepted.some((t) => present.includes(t))) return void 0;
1412
+ }
1413
+ if (phase === `drop`) {
1414
+ const nodes = this.parseExternalDrop?.(dataTransfer);
1415
+ if (!nodes || nodes.length === 0) return void 0;
1416
+ return {
1417
+ source: `external`,
1418
+ keys: nodes.map((n) => n.item.key),
1419
+ nodes
1420
+ };
1421
+ }
1422
+ return this.parseExternalDrop ? {
1423
+ source: `external`,
1424
+ keys: [],
1425
+ nodes: []
1426
+ } : void 0;
1427
+ }
1428
+ }
1429
+ #anyRowDraggable() {
1430
+ if (this.itemDnd === `none`) return false;
1431
+ const allNodes = this.#getAllNodes();
1432
+ if (allNodes.length === 0) return false;
1433
+ if (!this.canDrag) return true;
1434
+ return allNodes.some(({ node }) => this.canDrag({
1435
+ nodes: [node],
1436
+ keys: [node.item.key]
1437
+ }));
526
1438
  }
527
1439
  disconnectedCallback() {
528
1440
  super.disconnectedCallback();
1441
+ this.#cancelAllLeavingAnimations();
1442
+ this.#leaving.clear();
1443
+ this.#enteringKeys.clear();
529
1444
  this.#modelUnsubscribe?.();
530
1445
  this.#modelUnsubscribe = void 0;
531
1446
  this.#unregisterSearchCommands?.();
532
1447
  this.#unregisterSearchCommands = void 0;
1448
+ if (this.#postDragSuppressTimer !== void 0) {
1449
+ clearTimeout(this.#postDragSuppressTimer);
1450
+ this.#postDragSuppressTimer = void 0;
1451
+ }
1452
+ this.#container()?.removeEventListener(`scroll`, this.#onContainerScroll);
1453
+ treeDragUnregister(this.#dragSourceId);
533
1454
  if (this.#ownedRegistry) {
534
1455
  this.#ownedRegistry.unregister(`select-all`);
535
1456
  this.#ownedRegistry.unregister(`deselect-all`);
@@ -704,6 +1625,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
704
1625
  `;
705
1626
  }
706
1627
  _handleItemClick(node, depth, event) {
1628
+ if (this.#drop?.isDragging || this.#nativeDrag?.isDragging || this.#postDragSuppressTimer !== void 0) return;
707
1629
  if (event && event.detail >= 2) return;
708
1630
  if (this.#pendingNode && this._expandedKeys.has(this.#pendingNode.item.key)) this.#abortPending();
709
1631
  this.tickled.handlePointerEnter(node.item.key);
@@ -1138,6 +2060,23 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1138
2060
  </ul>
1139
2061
  `;
1140
2062
  }
2063
+ /** Shared drag-and-drop render state for a row, used by every `_renderNode` variant. */
2064
+ _dndRowState(node) {
2065
+ const draggable = this.itemDnd !== `none` && (this.root?.children?.length ?? 0) > 0 && (this.itemDnd === `internal` || this.itemDnd === `both` || this.itemDnd === `external`) && (this.canDrag?.({
2066
+ nodes: [node],
2067
+ keys: [node.item.key]
2068
+ }) ?? true);
2069
+ const dragging = this.#nativeDragKeys.has(node.item.key);
2070
+ const state = this.#dropState;
2071
+ const dropTarget = state !== void 0 && state.referenceKey === node.item.key;
2072
+ return {
2073
+ draggable,
2074
+ dragging,
2075
+ dropTarget,
2076
+ dropPosition: dropTarget ? state.position : void 0,
2077
+ dropDepth: dropTarget ? state.depth : void 0
2078
+ };
2079
+ }
1141
2080
  _renderNode(node, depth) {
1142
2081
  const selected = this._isSelected(node);
1143
2082
  const tickled = this._isFocused(node);
@@ -1146,9 +2085,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1146
2085
  const isPending = this.#pendingNode === node;
1147
2086
  const isFiltered = this._isFilteredOut(node, depth);
1148
2087
  const isLeaving = this._isLeaving(node);
2088
+ const dnd = this._dndRowState(node);
1149
2089
  return html`
1150
2090
  <li
1151
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``} ${isLeaving ? `leaving` : ``}"
2091
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
1152
2092
  role="treeitem"
1153
2093
  aria-selected=${selected}
1154
2094
  aria-expanded=${isBranch ? isExpanded : nothing}
@@ -1156,6 +2096,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1156
2096
  aria-level=${depth + 1}
1157
2097
  data-key=${node.item.key}
1158
2098
  style=${`--depth: ${depth}`}
2099
+ draggable=${dnd.draggable ? `true` : nothing}
2100
+ part=${dnd.dropTarget ? `drop-target` : nothing}
1159
2101
  @click=${(e) => this._handleItemClick(node, depth, e)}
1160
2102
  @pointerenter=${() => {
1161
2103
  this.tickled.handlePointerEnter(node.item.key);
@@ -1169,6 +2111,11 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1169
2111
  }));
1170
2112
  }}
1171
2113
  >
2114
+ ${dnd.dropTarget && (dnd.dropPosition === `before` || dnd.dropPosition === `after`) ? html`<div
2115
+ class="drop-line drop-line-${dnd.dropPosition}"
2116
+ style="--drop-depth: ${dnd.dropDepth ?? depth}"
2117
+ part="drop-indicator"
2118
+ ></div>` : nothing}
1172
2119
  ${this.renderNodeContent(node, depth)}
1173
2120
  ${isPending ? this.renderLoadingIndicator() : nothing}
1174
2121
  </li>
@@ -1186,9 +2133,13 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1186
2133
  --tree-text: var(--surface-3-text);
1187
2134
  --tree-muted-text: var(--surface-muted-text);
1188
2135
  --tree-indent: 1ch;
1189
- --tree-item-height: 2ch;
2136
+ --tree-item-height: 30px;
1190
2137
  --tree-caret-size: 30px;
1191
2138
  --tree-radius: var(--radius-s);
2139
+ --tree-drop-line: var(--accent);
2140
+ --tree-drop-nub-size: 5px;
2141
+ --tree-drop-into-outline: 2px solid var(--accent);
2142
+ --tree-drop-into-bg: color-mix(in srgb, var(--accent) 14%, transparent);
1192
2143
 
1193
2144
  position: relative;
1194
2145
  display: block;
@@ -1216,10 +2167,11 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1216
2167
  }
1217
2168
 
1218
2169
  .item {
2170
+ position: relative; /* anchor for the .drop-line before/after indicator */
1219
2171
  display: flex;
1220
2172
  align-items: center;
1221
2173
  height: var(--tree-item-height);
1222
- padding-left: calc(var(--tree-indent) * var(--depth, 0) + var(--space-s));
2174
+ padding-left: calc(var(--tree-indent) * var(--depth, 0));
1223
2175
  cursor: pointer;
1224
2176
  user-select: none;
1225
2177
  font-family: var(--font-family);
@@ -1291,11 +2243,49 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1291
2243
  overflow: hidden;
1292
2244
  }
1293
2245
 
1294
- ::highlight(incr-search-highlight) {
1295
- background: var(--search-highlight-bg, var(--accent));
1296
- color: var(--search-highlight-color, var(--accent-text, #fff));
1297
- border-radius: 2px;
1298
- padding: 0 1px;
2246
+ /* Drag and drop styles */
2247
+ .item.dragging {
2248
+ opacity: 0.4;
2249
+ }
2250
+
2251
+ .item.drop-into {
2252
+ outline: var(--tree-drop-into-outline);
2253
+ outline-offset: -2px;
2254
+ background: var(--tree-drop-into-bg);
2255
+ }
2256
+
2257
+ .drop-line {
2258
+ position: absolute;
2259
+ left: calc(var(--tree-indent) * var(--drop-depth, 0) + var(--space-xs));
2260
+ right: var(--space-xs);
2261
+ height: 2px;
2262
+ background: var(--tree-drop-line);
2263
+ pointer-events: none;
2264
+ z-index: 10;
2265
+ }
2266
+
2267
+ /* Nub at the left end of the insertion line (VS Code style). */
2268
+ .drop-line::before {
2269
+ content: "";
2270
+ position: absolute;
2271
+ left: calc(-1 * var(--tree-drop-nub-size) / 2);
2272
+ top: calc(50% - var(--tree-drop-nub-size) / 2);
2273
+ width: var(--tree-drop-nub-size);
2274
+ height: var(--tree-drop-nub-size);
2275
+ border-radius: 50%;
2276
+ background: var(--tree-drop-line);
2277
+ }
2278
+
2279
+ .drop-line-before {
2280
+ top: -1px;
2281
+ }
2282
+
2283
+ .drop-line-after {
2284
+ bottom: -1px;
2285
+ }
2286
+
2287
+ :host([data-drop-rejected]) {
2288
+ cursor: not-allowed;
1299
2289
  }
1300
2290
  `
1301
2291
  ];
@@ -1329,6 +2319,31 @@ __decorate([property({
1329
2319
  attribute: `search-modes`,
1330
2320
  reflect: true
1331
2321
  })], TreeBaseElement.prototype, "searchModes", void 0);
2322
+ __decorate([property({
2323
+ reflect: true,
2324
+ attribute: `item-dnd`
2325
+ })], TreeBaseElement.prototype, "itemDnd", void 0);
2326
+ __decorate([property({
2327
+ type: String,
2328
+ attribute: `accept-drop-types`
2329
+ })], TreeBaseElement.prototype, "acceptDropTypes", void 0);
2330
+ __decorate([property({
2331
+ type: Number,
2332
+ attribute: `dnd-expand-hover-ms`
2333
+ })], TreeBaseElement.prototype, "dndExpandHoverMs", void 0);
2334
+ __decorate([property({
2335
+ reflect: true,
2336
+ attribute: `reorderable`,
2337
+ converter: {
2338
+ fromAttribute: (value) => value !== `false`,
2339
+ toAttribute: (value) => value ? null : `false`
2340
+ }
2341
+ })], TreeBaseElement.prototype, "reorderable", void 0);
2342
+ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewRenderer", void 0);
2343
+ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrag", void 0);
2344
+ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrop", void 0);
2345
+ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "parseExternalDrop", void 0);
2346
+ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "applyInternalDrop", void 0);
1332
2347
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
1333
2348
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
1334
2349
  TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
@@ -1410,9 +2425,10 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
1410
2425
  const hasIconAtDepth = this.#depthsWithIcons.has(depth);
1411
2426
  const isLeaving = this._isLeaving(node);
1412
2427
  const isFiltered = this._isFilteredOut(node, depth);
2428
+ const dnd = this._dndRowState(node);
1413
2429
  return html`
1414
2430
  <li
1415
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${isFiltered ? `filtered` : ``}"
2431
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${isFiltered ? `filtered` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
1416
2432
  data-key=${node.item.key}
1417
2433
  role="treeitem"
1418
2434
  aria-selected=${selected}
@@ -1420,9 +2436,16 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
1420
2436
  aria-hidden=${isLeaving ? `true` : nothing}
1421
2437
  aria-level=${depth + 1}
1422
2438
  style=${`--depth: ${depth}`}
2439
+ draggable=${dnd.draggable ? `true` : nothing}
2440
+ part=${dnd.dropTarget ? `drop-target` : nothing}
1423
2441
  @click=${(e) => this._handleItemClick(node, depth, e)}
1424
2442
  @pointerenter=${() => this.tickled.handlePointerEnter(node.item.key)}
1425
2443
  >
2444
+ ${dnd.dropTarget && (dnd.dropPosition === `before` || dnd.dropPosition === `after`) ? html`<div
2445
+ class="drop-line drop-line-${dnd.dropPosition}"
2446
+ style="--drop-depth: ${dnd.dropDepth ?? depth}"
2447
+ part="drop-indicator"
2448
+ ></div>` : nothing}
1426
2449
  ${this.renderNodeContent(node, depth)}
1427
2450
  ${isPending ? this.renderLoadingIndicator() : nothing}
1428
2451
  </li>
@@ -1605,6 +2628,6 @@ var TreeController = class {
1605
2628
  }
1606
2629
  };
1607
2630
  //#endregion
1608
- export { TreeDataModel as i, TreeListElement as n, TreeBaseElement as r, TreeController as t };
2631
+ export { TreeDataModel as a, pruneDraggedDescendants as c, TreeDropController as i, TreeListElement as n, applyTreeDrop as o, TreeBaseElement as r, dropTargetFor as s, TreeController as t };
1609
2632
 
1610
- //# sourceMappingURL=tree-CozQj01a.js.map
2633
+ //# sourceMappingURL=tree-a14n9y7i.js.map