@ixfx/components 0.8.8 → 0.9.1

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 (69) hide show
  1. package/bundle/index.d.ts +252 -43
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +1177 -39
  4. package/bundle/index.js.map +1 -1
  5. package/dist/crumbs.d.ts +1 -1
  6. package/dist/{data-grid-BKa9i86q.d.ts → data-grid-C588pTgt.d.ts} +3 -3
  7. package/dist/{data-grid-BKa9i86q.d.ts.map → data-grid-C588pTgt.d.ts.map} +1 -1
  8. package/dist/data-grid.d.ts +2 -2
  9. package/dist/data-grid.js +1 -1
  10. package/dist/{detail-list-CHTomlbR.d.ts → detail-list-IKl76Wnp.d.ts} +4 -4
  11. package/dist/{detail-list-CHTomlbR.d.ts.map → detail-list-IKl76Wnp.d.ts.map} +1 -1
  12. package/dist/detail-list.d.ts +3 -3
  13. package/dist/detail-list.js +1 -1
  14. package/dist/{grid-list-CsZcXxMC.d.ts → grid-list-C1zq5U1W.d.ts} +20 -56
  15. package/dist/grid-list-C1zq5U1W.d.ts.map +1 -0
  16. package/dist/{grid-list-C8oMxLl-.js → grid-list-zUZRI8hk.js} +39 -3
  17. package/dist/grid-list-zUZRI8hk.js.map +1 -0
  18. package/dist/grid-list.d.ts +2 -2
  19. package/dist/grid-list.js +1 -1
  20. package/dist/incr-search.d.ts +1 -1
  21. package/dist/{index-Dgrx-DIA.d.ts → index--qnhBWZJ.d.ts} +2 -2
  22. package/dist/{index-Dgrx-DIA.d.ts.map → index--qnhBWZJ.d.ts.map} +1 -1
  23. package/dist/index.d.ts +30 -9
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +11 -11
  26. package/dist/index.js.map +1 -1
  27. package/dist/{labelled-slider-input-BCH9968c.js → labelled-slider-input-aEYDvW2S.js} +2 -2
  28. package/dist/{labelled-slider-input-BCH9968c.js.map → labelled-slider-input-aEYDvW2S.js.map} +1 -1
  29. package/dist/labelled-slider-input.js +1 -1
  30. package/dist/list-native-drag-controller-Cmpx7jnx.d.ts +68 -0
  31. package/dist/list-native-drag-controller-Cmpx7jnx.d.ts.map +1 -0
  32. package/dist/{list-native-drag-controller-DrY3qeJM.js → list-native-drag-controller-DaBNcjgG.js} +28 -2
  33. package/dist/list-native-drag-controller-DaBNcjgG.js.map +1 -0
  34. package/dist/miller.d.ts +2 -2
  35. package/dist/plots.js +1 -1
  36. package/dist/slider-input-BxURAAmD.d.ts.map +1 -1
  37. package/dist/{slider-input-WR8_RwHg.js → slider-input-Cb5uSV42.js} +78 -11
  38. package/dist/slider-input-Cb5uSV42.js.map +1 -0
  39. package/dist/slider-input.js +1 -1
  40. package/dist/{split-layout-CA5jbNbG.js → split-layout-Bx0g8k8-.js} +24 -1
  41. package/dist/{split-layout-CA5jbNbG.js.map → split-layout-Bx0g8k8-.js.map} +1 -1
  42. package/dist/split-layout-JK3xWVX2.d.ts.map +1 -1
  43. package/dist/split-layout.js +1 -1
  44. package/dist/{tree-DSMn566I.js → tree-CwifXCzR.js} +1006 -19
  45. package/dist/tree-CwifXCzR.js.map +1 -0
  46. package/dist/{tree-component-nPP5Y-WS.d.ts → tree-component-BW2iCFq9.d.ts} +107 -3
  47. package/dist/tree-component-BW2iCFq9.d.ts.map +1 -0
  48. package/dist/tree.d.ts +102 -4
  49. package/dist/tree.d.ts.map +1 -1
  50. package/dist/tree.js +2 -2
  51. package/dist/{vertical-list-A6oWfivZ.d.ts → vertical-list-CHrXmmQ3.d.ts} +3 -3
  52. package/dist/{vertical-list-A6oWfivZ.d.ts.map → vertical-list-CHrXmmQ3.d.ts.map} +1 -1
  53. package/dist/vertical-list.d.ts +2 -2
  54. package/dist/vertical-list.js +1 -1
  55. package/dist/{xy-axis-CrrVdDFe.js → xy-axis-COKw338Q.js} +2 -2
  56. package/dist/xy-axis-COKw338Q.js.map +1 -0
  57. package/dist/xy-axis-DQqd2yXJ.d.ts.map +1 -1
  58. package/docs-user/index.json +2 -2
  59. package/docs-user/tree.md +75 -0
  60. package/package.json +1 -1
  61. package/dist/grid-list-C8oMxLl-.js.map +0 -1
  62. package/dist/grid-list-CsZcXxMC.d.ts.map +0 -1
  63. package/dist/list-drag-types-kpaXz2pZ.d.ts +0 -42
  64. package/dist/list-drag-types-kpaXz2pZ.d.ts.map +0 -1
  65. package/dist/list-native-drag-controller-DrY3qeJM.js.map +0 -1
  66. package/dist/slider-input-WR8_RwHg.js.map +0 -1
  67. package/dist/tree-DSMn566I.js.map +0 -1
  68. package/dist/tree-component-nPP5Y-WS.d.ts.map +0 -1
  69. package/dist/xy-axis-CrrVdDFe.js.map +0 -1
@@ -3,6 +3,7 @@ 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 { t as ListNativeDragController } from "./list-native-drag-controller-DaBNcjgG.js";
6
7
  import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
7
8
  import "./interaction-BskmlA9t.js";
8
9
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
@@ -10,15 +11,138 @@ 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.
178
315
  * Returns the new root (connected components are notified automatically).
179
316
  */
180
- moveNode(key, newParentKey) {
181
- this.#root = moveNodeTo(this.#root, key, newParentKey);
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.
372
+ * Returns the new root (connected components are notified automatically).
373
+ */
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
  }
@@ -236,6 +801,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
236
801
  this.#leaving = /* @__PURE__ */ new Map();
237
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();
@@ -389,6 +960,28 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
389
960
  #leaving;
390
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
+ }
392
985
  #cancelLeavingAnimation(key) {
393
986
  this.#leavingAnimations.get(key)?.cancel();
394
987
  this.#leavingAnimations.delete(key);
@@ -500,6 +1093,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
500
1093
  this.#treeAnimReady = true;
501
1094
  }
502
1095
  updated(changedProps) {
1096
+ this.#rowsCache = void 0;
503
1097
  if (changedProps.has(`root`) && this.root) {
504
1098
  this.#abortPending();
505
1099
  this._expandedKeys.clear();
@@ -556,6 +1150,291 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
556
1150
  });
557
1151
  this.registry = registry;
558
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
+ }));
559
1438
  }
560
1439
  disconnectedCallback() {
561
1440
  super.disconnectedCallback();
@@ -566,6 +1445,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
566
1445
  this.#modelUnsubscribe = void 0;
567
1446
  this.#unregisterSearchCommands?.();
568
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);
569
1454
  if (this.#ownedRegistry) {
570
1455
  this.#ownedRegistry.unregister(`select-all`);
571
1456
  this.#ownedRegistry.unregister(`deselect-all`);
@@ -740,6 +1625,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
740
1625
  `;
741
1626
  }
742
1627
  _handleItemClick(node, depth, event) {
1628
+ if (this.#drop?.isDragging || this.#nativeDrag?.isDragging || this.#postDragSuppressTimer !== void 0) return;
743
1629
  if (event && event.detail >= 2) return;
744
1630
  if (this.#pendingNode && this._expandedKeys.has(this.#pendingNode.item.key)) this.#abortPending();
745
1631
  this.tickled.handlePointerEnter(node.item.key);
@@ -1174,6 +2060,23 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1174
2060
  </ul>
1175
2061
  `;
1176
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
+ }
1177
2080
  _renderNode(node, depth) {
1178
2081
  const selected = this._isSelected(node);
1179
2082
  const tickled = this._isFocused(node);
@@ -1182,9 +2085,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1182
2085
  const isPending = this.#pendingNode === node;
1183
2086
  const isFiltered = this._isFilteredOut(node, depth);
1184
2087
  const isLeaving = this._isLeaving(node);
2088
+ const dnd = this._dndRowState(node);
1185
2089
  return html`
1186
2090
  <li
1187
- 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` : ``}"
1188
2092
  role="treeitem"
1189
2093
  aria-selected=${selected}
1190
2094
  aria-expanded=${isBranch ? isExpanded : nothing}
@@ -1192,6 +2096,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1192
2096
  aria-level=${depth + 1}
1193
2097
  data-key=${node.item.key}
1194
2098
  style=${`--depth: ${depth}`}
2099
+ draggable=${dnd.draggable ? `true` : nothing}
2100
+ part=${dnd.dropTarget ? `drop-target` : nothing}
1195
2101
  @click=${(e) => this._handleItemClick(node, depth, e)}
1196
2102
  @pointerenter=${() => {
1197
2103
  this.tickled.handlePointerEnter(node.item.key);
@@ -1205,6 +2111,11 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1205
2111
  }));
1206
2112
  }}
1207
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}
1208
2119
  ${this.renderNodeContent(node, depth)}
1209
2120
  ${isPending ? this.renderLoadingIndicator() : nothing}
1210
2121
  </li>
@@ -1225,6 +2136,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1225
2136
  --tree-item-height: 30px;
1226
2137
  --tree-caret-size: 30px;
1227
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);
1228
2143
 
1229
2144
  position: relative;
1230
2145
  display: block;
@@ -1252,6 +2167,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1252
2167
  }
1253
2168
 
1254
2169
  .item {
2170
+ position: relative; /* anchor for the .drop-line before/after indicator */
1255
2171
  display: flex;
1256
2172
  align-items: center;
1257
2173
  height: var(--tree-item-height);
@@ -1327,11 +2243,49 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1327
2243
  overflow: hidden;
1328
2244
  }
1329
2245
 
1330
- ::highlight(incr-search-highlight) {
1331
- background: var(--search-highlight-bg, var(--accent));
1332
- color: var(--search-highlight-color, var(--accent-text, #fff));
1333
- border-radius: 2px;
1334
- 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;
1335
2289
  }
1336
2290
  `
1337
2291
  ];
@@ -1365,6 +2319,31 @@ __decorate([property({
1365
2319
  attribute: `search-modes`,
1366
2320
  reflect: true
1367
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);
1368
2347
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
1369
2348
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
1370
2349
  TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
@@ -1446,9 +2425,10 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
1446
2425
  const hasIconAtDepth = this.#depthsWithIcons.has(depth);
1447
2426
  const isLeaving = this._isLeaving(node);
1448
2427
  const isFiltered = this._isFilteredOut(node, depth);
2428
+ const dnd = this._dndRowState(node);
1449
2429
  return html`
1450
2430
  <li
1451
- 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` : ``}"
1452
2432
  data-key=${node.item.key}
1453
2433
  role="treeitem"
1454
2434
  aria-selected=${selected}
@@ -1456,9 +2436,16 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
1456
2436
  aria-hidden=${isLeaving ? `true` : nothing}
1457
2437
  aria-level=${depth + 1}
1458
2438
  style=${`--depth: ${depth}`}
2439
+ draggable=${dnd.draggable ? `true` : nothing}
2440
+ part=${dnd.dropTarget ? `drop-target` : nothing}
1459
2441
  @click=${(e) => this._handleItemClick(node, depth, e)}
1460
2442
  @pointerenter=${() => this.tickled.handlePointerEnter(node.item.key)}
1461
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}
1462
2449
  ${this.renderNodeContent(node, depth)}
1463
2450
  ${isPending ? this.renderLoadingIndicator() : nothing}
1464
2451
  </li>
@@ -1641,6 +2628,6 @@ var TreeController = class {
1641
2628
  }
1642
2629
  };
1643
2630
  //#endregion
1644
- 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 };
1645
2632
 
1646
- //# sourceMappingURL=tree-DSMn566I.js.map
2633
+ //# sourceMappingURL=tree-CwifXCzR.js.map