@headless-tree/core 0.0.0-20250726131941 → 0.0.0-20250731075124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20250726131941
3
+ ## 0.0.0-20250731075124
4
+
5
+ ### Minor Changes
6
+
7
+ - 21d1679: add `canDragForeignDragObjectOver` to allow customizing whether a draggable visualization should be shown when dragging foreign data. This allows differentiating logic between drag-over and drop (via the existing `canDropForeignDataObject`), since for the latter `dataTransfer.getData` is not available by default in browsers.
4
8
 
5
9
  ### Patch Changes
6
10
 
11
+ - e8ddbb0: Added `item.updateCachedData(data)` in async tree feature, that works similar to the existing `item.updateCachedChildrenIds(childrenIds)` feature
7
12
  - 662e2a8: Added stories and documentation on how to use nested DOM rendering for tree structures instead of flat lists,
8
13
  which can be used for animating expand/collapse behavior
9
14
  - b41e1d2: fixed a bug where ending drag without successful drop doesn't properly reset drag line (#132)
package/dist/index.d.mts CHANGED
@@ -45,13 +45,22 @@ type DragAndDropFeatureDef<T> = {
45
45
  createForeignDragObject?: (items: ItemInstance<T>[]) => {
46
46
  format: string;
47
47
  data: any;
48
+ dropEffect?: DataTransfer["dropEffect"];
49
+ effectAllowed?: DataTransfer["effectAllowed"];
48
50
  };
49
51
  setDragImage?: (items: ItemInstance<T>[]) => {
50
52
  imgElement: Element;
51
53
  xOffset?: number;
52
54
  yOffset?: number;
53
55
  };
56
+ /** Checks if a foreign drag object can be dropped on a target, validating that an actual drop can commence based on
57
+ * the data in the DataTransfer object. */
54
58
  canDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => boolean;
59
+ /** Checks if a droppable visualization should be displayed when dragging a foreign object over a target. Since this
60
+ * is executed on a dragover event, `dataTransfer.getData()` is not available, so `dataTransfer.effectAllowed` or
61
+ * `dataTransfer.types` should be used instead. Before actually completing the drag, @{link canDropForeignDragObject}
62
+ * will be called by HT before applying the drop. */
63
+ canDragForeignDragObjectOver?: (dataTransfer: DataTransfer, target: DragTarget<T>) => boolean;
55
64
  onDrop?: (items: ItemInstance<T>[], target: DragTarget<T>) => void | Promise<void>;
56
65
  onDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => void | Promise<void>;
57
66
  onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
@@ -150,8 +159,6 @@ type MainFeatureDef<T = any> = {
150
159
  treeInstance: {
151
160
  /** @internal */
152
161
  applySubStateUpdate: <K extends keyof TreeState<any>>(stateName: K, updater: Updater<TreeState<T>[K]>) => void;
153
- /** @internal */
154
- buildItemInstance: (itemId: string) => ItemInstance<T>;
155
162
  setState: SetStateFn<TreeState<T>>;
156
163
  getState: () => TreeState<T>;
157
164
  setConfig: SetStateFn<TreeConfig<T>>;
@@ -296,6 +303,7 @@ type AsyncDataLoaderFeatureDef<T> = {
296
303
  * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
297
304
  * the tree will continue to display the old data until the new data has loaded. */
298
305
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
306
+ updateCachedData: (data: T) => void;
299
307
  updateCachedChildrenIds: (childrenIds: string[]) => void;
300
308
  isLoading: () => boolean;
301
309
  };
package/dist/index.d.ts CHANGED
@@ -45,13 +45,22 @@ type DragAndDropFeatureDef<T> = {
45
45
  createForeignDragObject?: (items: ItemInstance<T>[]) => {
46
46
  format: string;
47
47
  data: any;
48
+ dropEffect?: DataTransfer["dropEffect"];
49
+ effectAllowed?: DataTransfer["effectAllowed"];
48
50
  };
49
51
  setDragImage?: (items: ItemInstance<T>[]) => {
50
52
  imgElement: Element;
51
53
  xOffset?: number;
52
54
  yOffset?: number;
53
55
  };
56
+ /** Checks if a foreign drag object can be dropped on a target, validating that an actual drop can commence based on
57
+ * the data in the DataTransfer object. */
54
58
  canDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => boolean;
59
+ /** Checks if a droppable visualization should be displayed when dragging a foreign object over a target. Since this
60
+ * is executed on a dragover event, `dataTransfer.getData()` is not available, so `dataTransfer.effectAllowed` or
61
+ * `dataTransfer.types` should be used instead. Before actually completing the drag, @{link canDropForeignDragObject}
62
+ * will be called by HT before applying the drop. */
63
+ canDragForeignDragObjectOver?: (dataTransfer: DataTransfer, target: DragTarget<T>) => boolean;
55
64
  onDrop?: (items: ItemInstance<T>[], target: DragTarget<T>) => void | Promise<void>;
56
65
  onDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => void | Promise<void>;
57
66
  onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
@@ -150,8 +159,6 @@ type MainFeatureDef<T = any> = {
150
159
  treeInstance: {
151
160
  /** @internal */
152
161
  applySubStateUpdate: <K extends keyof TreeState<any>>(stateName: K, updater: Updater<TreeState<T>[K]>) => void;
153
- /** @internal */
154
- buildItemInstance: (itemId: string) => ItemInstance<T>;
155
162
  setState: SetStateFn<TreeState<T>>;
156
163
  getState: () => TreeState<T>;
157
164
  setConfig: SetStateFn<TreeConfig<T>>;
@@ -296,6 +303,7 @@ type AsyncDataLoaderFeatureDef<T> = {
296
303
  * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
297
304
  * the tree will continue to display the old data until the new data has loaded. */
298
305
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
306
+ updateCachedData: (data: T) => void;
299
307
  updateCachedChildrenIds: (childrenIds: string[]) => void;
300
308
  isLoading: () => boolean;
301
309
  };
package/dist/index.js CHANGED
@@ -547,19 +547,6 @@ var createTree = (initialConfig) => {
547
547
  const externalStateSetter = config[stateHandlerNames[stateName]];
548
548
  externalStateSetter == null ? void 0 : externalStateSetter(state[stateName]);
549
549
  },
550
- buildItemInstance: ({}, itemId) => {
551
- const [instance, finalizeInstance] = buildInstance(
552
- features,
553
- "itemInstance",
554
- (instance2) => ({
555
- item: instance2,
556
- tree: treeInstance,
557
- itemId
558
- })
559
- );
560
- finalizeInstance();
561
- return instance;
562
- },
563
550
  // TODO rebuildSubTree: (itemId: string) => void;
564
551
  rebuildTree: () => {
565
552
  var _a2;
@@ -580,7 +567,23 @@ var createTree = (initialConfig) => {
580
567
  (_c2 = config.setState) == null ? void 0 : _c2.call(config, state);
581
568
  }
582
569
  },
583
- getItemInstance: ({}, itemId) => itemInstancesMap[itemId],
570
+ getItemInstance: ({}, itemId) => {
571
+ const existingInstance = itemInstancesMap[itemId];
572
+ if (!existingInstance) {
573
+ const [instance, finalizeInstance] = buildInstance(
574
+ features,
575
+ "itemInstance",
576
+ (instance2) => ({
577
+ item: instance2,
578
+ tree: treeInstance,
579
+ itemId
580
+ })
581
+ );
582
+ finalizeInstance();
583
+ return instance;
584
+ }
585
+ return existingInstance;
586
+ },
584
587
  getItems: () => itemInstances,
585
588
  registerElement: ({}, element) => {
586
589
  if (treeElement === element) {
@@ -636,7 +639,17 @@ var createTree = (initialConfig) => {
636
639
  var _a2;
637
640
  return (_a2 = itemDataRefs[itemId]) != null ? _a2 : itemDataRefs[itemId] = { current: {} };
638
641
  },
639
- getItemMeta: ({ itemId }) => itemMetaMap[itemId]
642
+ getItemMeta: ({ itemId }) => {
643
+ var _a2;
644
+ return (_a2 = itemMetaMap[itemId]) != null ? _a2 : {
645
+ itemId,
646
+ parentId: null,
647
+ level: -1,
648
+ index: -1,
649
+ posInSet: 0,
650
+ setSize: 1
651
+ };
652
+ }
640
653
  }
641
654
  };
642
655
  features.unshift(mainFeature);
@@ -801,7 +814,7 @@ var selectionFeature = {
801
814
 
802
815
  // src/features/checkboxes/feature.ts
803
816
  var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
804
- if (!tree.getConfig().isItemFolder(tree.buildItemInstance(itemId))) {
817
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
805
818
  return [itemId];
806
819
  }
807
820
  const descendants = tree.retrieveChildrenIds(itemId).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
@@ -1147,6 +1160,11 @@ var asyncDataLoaderFeature = {
1147
1160
  const dataRef = tree.getDataRef();
1148
1161
  dataRef.current.childrenIds[itemId] = childrenIds;
1149
1162
  tree.rebuildTree();
1163
+ },
1164
+ updateCachedData: ({ tree, itemId }, data) => {
1165
+ const dataRef = tree.getDataRef();
1166
+ dataRef.current.itemData[itemId] = data;
1167
+ tree.rebuildTree();
1150
1168
  }
1151
1169
  }
1152
1170
  };
@@ -1354,12 +1372,14 @@ var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) =>
1354
1372
  };
1355
1373
 
1356
1374
  // src/features/drag-and-drop/feature.ts
1375
+ var defaultCanDropForeignDragObject = () => false;
1357
1376
  var dragAndDropFeature = {
1358
1377
  key: "drag-and-drop",
1359
1378
  deps: ["selection"],
1360
1379
  getDefaultConfig: (defaultConfig, tree) => __spreadValues({
1361
1380
  canDrop: (_, target) => target.item.isFolder(),
1362
- canDropForeignDragObject: () => false,
1381
+ canDropForeignDragObject: defaultCanDropForeignDragObject,
1382
+ canDragForeignDragObjectOver: defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject ? (dataTransfer) => dataTransfer.effectAllowed !== "none" : () => false,
1363
1383
  setDndState: makeStateUpdater("dnd", tree),
1364
1384
  canReorder: true
1365
1385
  }, defaultConfig),
@@ -1458,7 +1478,7 @@ var dragAndDropFeature = {
1458
1478
  draggable: true,
1459
1479
  onDragEnter: (e) => e.preventDefault(),
1460
1480
  onDragStart: (e) => {
1461
- var _a, _b, _c, _d;
1481
+ var _a, _b, _c;
1462
1482
  const selectedItems = tree.getSelectedItems();
1463
1483
  const items = selectedItems.includes(item) ? selectedItems : [item];
1464
1484
  const config = tree.getConfig();
@@ -1473,9 +1493,11 @@ var dragAndDropFeature = {
1473
1493
  const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1474
1494
  (_c = e.dataTransfer) == null ? void 0 : _c.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1475
1495
  }
1476
- if (config.createForeignDragObject) {
1477
- const { format, data } = config.createForeignDragObject(items);
1478
- (_d = e.dataTransfer) == null ? void 0 : _d.setData(format, data);
1496
+ if (config.createForeignDragObject && e.dataTransfer) {
1497
+ const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1498
+ e.dataTransfer.setData(format, data);
1499
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1500
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1479
1501
  }
1480
1502
  tree.applySubStateUpdate("dnd", {
1481
1503
  draggedItems: items,
@@ -1484,6 +1506,7 @@ var dragAndDropFeature = {
1484
1506
  },
1485
1507
  onDragOver: (e) => {
1486
1508
  var _a, _b, _c;
1509
+ e.stopPropagation();
1487
1510
  const dataRef = tree.getDataRef();
1488
1511
  const nextDragCode = getDragCode(e, item, tree);
1489
1512
  if (nextDragCode === dataRef.current.lastDragCode) {
@@ -1494,7 +1517,7 @@ var dragAndDropFeature = {
1494
1517
  }
1495
1518
  dataRef.current.lastDragCode = nextDragCode;
1496
1519
  const target = getDragTarget(e, item, tree);
1497
- if (!((_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems) && (!e.dataTransfer || !((_c = (_b = tree.getConfig()).canDropForeignDragObject) == null ? void 0 : _c.call(_b, e.dataTransfer, target)))) {
1520
+ if (!((_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems) && (!e.dataTransfer || !((_c = (_b = tree.getConfig()).canDragForeignDragObjectOver) == null ? void 0 : _c.call(_b, e.dataTransfer, target)))) {
1498
1521
  dataRef.current.lastAllowDrop = false;
1499
1522
  return;
1500
1523
  }
@@ -1518,12 +1541,17 @@ var dragAndDropFeature = {
1518
1541
  }));
1519
1542
  },
1520
1543
  onDragEnd: (e) => {
1521
- var _a, _b, _c, _d;
1544
+ var _a, _b;
1545
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1522
1546
  const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1523
1547
  if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1524
1548
  return;
1525
1549
  }
1526
- (_d = (_c = tree.getConfig()).onCompleteForeignDrop) == null ? void 0 : _d.call(_c, draggedItems);
1550
+ const target = getDragTarget(e, item, tree);
1551
+ if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1552
+ return;
1553
+ }
1554
+ onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1527
1555
  },
1528
1556
  onDrop: (e) => __async(null, null, function* () {
1529
1557
  var _a, _b, _c;
package/dist/index.mjs CHANGED
@@ -503,19 +503,6 @@ var createTree = (initialConfig) => {
503
503
  const externalStateSetter = config[stateHandlerNames[stateName]];
504
504
  externalStateSetter == null ? void 0 : externalStateSetter(state[stateName]);
505
505
  },
506
- buildItemInstance: ({}, itemId) => {
507
- const [instance, finalizeInstance] = buildInstance(
508
- features,
509
- "itemInstance",
510
- (instance2) => ({
511
- item: instance2,
512
- tree: treeInstance,
513
- itemId
514
- })
515
- );
516
- finalizeInstance();
517
- return instance;
518
- },
519
506
  // TODO rebuildSubTree: (itemId: string) => void;
520
507
  rebuildTree: () => {
521
508
  var _a2;
@@ -536,7 +523,23 @@ var createTree = (initialConfig) => {
536
523
  (_c2 = config.setState) == null ? void 0 : _c2.call(config, state);
537
524
  }
538
525
  },
539
- getItemInstance: ({}, itemId) => itemInstancesMap[itemId],
526
+ getItemInstance: ({}, itemId) => {
527
+ const existingInstance = itemInstancesMap[itemId];
528
+ if (!existingInstance) {
529
+ const [instance, finalizeInstance] = buildInstance(
530
+ features,
531
+ "itemInstance",
532
+ (instance2) => ({
533
+ item: instance2,
534
+ tree: treeInstance,
535
+ itemId
536
+ })
537
+ );
538
+ finalizeInstance();
539
+ return instance;
540
+ }
541
+ return existingInstance;
542
+ },
540
543
  getItems: () => itemInstances,
541
544
  registerElement: ({}, element) => {
542
545
  if (treeElement === element) {
@@ -592,7 +595,17 @@ var createTree = (initialConfig) => {
592
595
  var _a2;
593
596
  return (_a2 = itemDataRefs[itemId]) != null ? _a2 : itemDataRefs[itemId] = { current: {} };
594
597
  },
595
- getItemMeta: ({ itemId }) => itemMetaMap[itemId]
598
+ getItemMeta: ({ itemId }) => {
599
+ var _a2;
600
+ return (_a2 = itemMetaMap[itemId]) != null ? _a2 : {
601
+ itemId,
602
+ parentId: null,
603
+ level: -1,
604
+ index: -1,
605
+ posInSet: 0,
606
+ setSize: 1
607
+ };
608
+ }
596
609
  }
597
610
  };
598
611
  features.unshift(mainFeature);
@@ -757,7 +770,7 @@ var selectionFeature = {
757
770
 
758
771
  // src/features/checkboxes/feature.ts
759
772
  var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
760
- if (!tree.getConfig().isItemFolder(tree.buildItemInstance(itemId))) {
773
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
761
774
  return [itemId];
762
775
  }
763
776
  const descendants = tree.retrieveChildrenIds(itemId).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
@@ -1103,6 +1116,11 @@ var asyncDataLoaderFeature = {
1103
1116
  const dataRef = tree.getDataRef();
1104
1117
  dataRef.current.childrenIds[itemId] = childrenIds;
1105
1118
  tree.rebuildTree();
1119
+ },
1120
+ updateCachedData: ({ tree, itemId }, data) => {
1121
+ const dataRef = tree.getDataRef();
1122
+ dataRef.current.itemData[itemId] = data;
1123
+ tree.rebuildTree();
1106
1124
  }
1107
1125
  }
1108
1126
  };
@@ -1310,12 +1328,14 @@ var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) =>
1310
1328
  };
1311
1329
 
1312
1330
  // src/features/drag-and-drop/feature.ts
1331
+ var defaultCanDropForeignDragObject = () => false;
1313
1332
  var dragAndDropFeature = {
1314
1333
  key: "drag-and-drop",
1315
1334
  deps: ["selection"],
1316
1335
  getDefaultConfig: (defaultConfig, tree) => __spreadValues({
1317
1336
  canDrop: (_, target) => target.item.isFolder(),
1318
- canDropForeignDragObject: () => false,
1337
+ canDropForeignDragObject: defaultCanDropForeignDragObject,
1338
+ canDragForeignDragObjectOver: defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject ? (dataTransfer) => dataTransfer.effectAllowed !== "none" : () => false,
1319
1339
  setDndState: makeStateUpdater("dnd", tree),
1320
1340
  canReorder: true
1321
1341
  }, defaultConfig),
@@ -1414,7 +1434,7 @@ var dragAndDropFeature = {
1414
1434
  draggable: true,
1415
1435
  onDragEnter: (e) => e.preventDefault(),
1416
1436
  onDragStart: (e) => {
1417
- var _a, _b, _c, _d;
1437
+ var _a, _b, _c;
1418
1438
  const selectedItems = tree.getSelectedItems();
1419
1439
  const items = selectedItems.includes(item) ? selectedItems : [item];
1420
1440
  const config = tree.getConfig();
@@ -1429,9 +1449,11 @@ var dragAndDropFeature = {
1429
1449
  const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1430
1450
  (_c = e.dataTransfer) == null ? void 0 : _c.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1431
1451
  }
1432
- if (config.createForeignDragObject) {
1433
- const { format, data } = config.createForeignDragObject(items);
1434
- (_d = e.dataTransfer) == null ? void 0 : _d.setData(format, data);
1452
+ if (config.createForeignDragObject && e.dataTransfer) {
1453
+ const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1454
+ e.dataTransfer.setData(format, data);
1455
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1456
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1435
1457
  }
1436
1458
  tree.applySubStateUpdate("dnd", {
1437
1459
  draggedItems: items,
@@ -1440,6 +1462,7 @@ var dragAndDropFeature = {
1440
1462
  },
1441
1463
  onDragOver: (e) => {
1442
1464
  var _a, _b, _c;
1465
+ e.stopPropagation();
1443
1466
  const dataRef = tree.getDataRef();
1444
1467
  const nextDragCode = getDragCode(e, item, tree);
1445
1468
  if (nextDragCode === dataRef.current.lastDragCode) {
@@ -1450,7 +1473,7 @@ var dragAndDropFeature = {
1450
1473
  }
1451
1474
  dataRef.current.lastDragCode = nextDragCode;
1452
1475
  const target = getDragTarget(e, item, tree);
1453
- if (!((_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems) && (!e.dataTransfer || !((_c = (_b = tree.getConfig()).canDropForeignDragObject) == null ? void 0 : _c.call(_b, e.dataTransfer, target)))) {
1476
+ if (!((_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems) && (!e.dataTransfer || !((_c = (_b = tree.getConfig()).canDragForeignDragObjectOver) == null ? void 0 : _c.call(_b, e.dataTransfer, target)))) {
1454
1477
  dataRef.current.lastAllowDrop = false;
1455
1478
  return;
1456
1479
  }
@@ -1474,12 +1497,17 @@ var dragAndDropFeature = {
1474
1497
  }));
1475
1498
  },
1476
1499
  onDragEnd: (e) => {
1477
- var _a, _b, _c, _d;
1500
+ var _a, _b;
1501
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1478
1502
  const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1479
1503
  if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1480
1504
  return;
1481
1505
  }
1482
- (_d = (_c = tree.getConfig()).onCompleteForeignDrop) == null ? void 0 : _d.call(_c, draggedItems);
1506
+ const target = getDragTarget(e, item, tree);
1507
+ if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1508
+ return;
1509
+ }
1510
+ onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1483
1511
  },
1484
1512
  onDrop: (e) => __async(null, null, function* () {
1485
1513
  var _a, _b, _c;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.0-20250726131941",
3
+ "version": "0.0.0-20250731075124",
4
4
  "main": "dist/index.d.ts",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
@@ -170,19 +170,6 @@ export const createTree = <T>(
170
170
  ] as Function;
171
171
  externalStateSetter?.(state[stateName]);
172
172
  },
173
- buildItemInstance: ({}, itemId) => {
174
- const [instance, finalizeInstance] = buildInstance(
175
- features,
176
- "itemInstance",
177
- (instance) => ({
178
- item: instance,
179
- tree: treeInstance,
180
- itemId,
181
- }),
182
- );
183
- finalizeInstance();
184
- return instance;
185
- },
186
173
  // TODO rebuildSubTree: (itemId: string) => void;
187
174
  rebuildTree: () => {
188
175
  rebuildItemMeta();
@@ -206,7 +193,23 @@ export const createTree = <T>(
206
193
  config.setState?.(state);
207
194
  }
208
195
  },
209
- getItemInstance: ({}, itemId) => itemInstancesMap[itemId],
196
+ getItemInstance: ({}, itemId) => {
197
+ const existingInstance = itemInstancesMap[itemId];
198
+ if (!existingInstance) {
199
+ const [instance, finalizeInstance] = buildInstance(
200
+ features,
201
+ "itemInstance",
202
+ (instance) => ({
203
+ item: instance,
204
+ tree: treeInstance,
205
+ itemId,
206
+ }),
207
+ );
208
+ finalizeInstance();
209
+ return instance;
210
+ }
211
+ return existingInstance;
212
+ },
210
213
  getItems: () => itemInstances,
211
214
  registerElement: ({}, element) => {
212
215
  if (treeElement === element) {
@@ -249,7 +252,15 @@ export const createTree = <T>(
249
252
  getElement: ({ itemId }) => itemElementsMap[itemId],
250
253
  // eslint-disable-next-line no-return-assign
251
254
  getDataRef: ({ itemId }) => (itemDataRefs[itemId] ??= { current: {} }),
252
- getItemMeta: ({ itemId }) => itemMetaMap[itemId],
255
+ getItemMeta: ({ itemId }) =>
256
+ itemMetaMap[itemId] ?? {
257
+ itemId,
258
+ parentId: null,
259
+ level: -1,
260
+ index: -1,
261
+ posInSet: 0,
262
+ setSize: 1,
263
+ },
253
264
  },
254
265
  };
255
266
 
@@ -165,5 +165,10 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
165
165
  dataRef.current.childrenIds[itemId] = childrenIds;
166
166
  tree.rebuildTree();
167
167
  },
168
+ updateCachedData: ({ tree, itemId }, data) => {
169
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
170
+ dataRef.current.itemData[itemId] = data;
171
+ tree.rebuildTree();
172
+ },
168
173
  },
169
174
  };
@@ -34,6 +34,7 @@ export type AsyncDataLoaderFeatureDef<T> = {
34
34
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
35
35
  loadItemData: (itemId: string) => Promise<T>;
36
36
  loadChildrenIds: (itemId: string) => Promise<string[]>;
37
+ /* idea: recursiveLoadItems: (itemId: string, cancelToken?: { current: boolean }, onLoad: (itemIds: string[]) => void) => Promise<T[]> */
37
38
  };
38
39
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
39
40
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
@@ -46,6 +47,7 @@ export type AsyncDataLoaderFeatureDef<T> = {
46
47
  * the tree will continue to display the old data until the new data has loaded. */
47
48
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
48
49
 
50
+ updateCachedData: (data: T) => void;
49
51
  updateCachedChildrenIds: (childrenIds: string[]) => void;
50
52
  isLoading: () => boolean;
51
53
  };
@@ -8,7 +8,7 @@ const getAllLoadedDescendants = <T>(
8
8
  itemId: string,
9
9
  includeFolders = false,
10
10
  ): string[] => {
11
- if (!tree.getConfig().isItemFolder(tree.buildItemInstance(itemId))) {
11
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
12
12
  return [itemId];
13
13
  }
14
14
  const descendants = tree
@@ -300,6 +300,9 @@ describe("core-feature/drag-and-drop", () => {
300
300
 
301
301
  it("drags foreign object inside tree, on folder", () => {
302
302
  tree.mockedHandler("canDropForeignDragObject").mockReturnValue(true);
303
+ tree
304
+ .mockedHandler("canDragForeignDragObjectOver")
305
+ .mockReturnValue(true);
303
306
  const onDropForeignDragObject = tree.mockedHandler(
304
307
  "onDropForeignDragObject",
305
308
  );
@@ -318,6 +321,9 @@ describe("core-feature/drag-and-drop", () => {
318
321
  tree
319
322
  .mockedHandler("canDropForeignDragObject")
320
323
  .mockImplementation((_, target) => target.item.isFolder());
324
+ tree
325
+ .mockedHandler("canDragForeignDragObjectOver")
326
+ .mockImplementation((_, target) => target.item.isFolder());
321
327
  const onDropForeignDragObject = tree.mockedHandler(
322
328
  "onDropForeignDragObject",
323
329
  );
@@ -340,6 +346,9 @@ describe("core-feature/drag-and-drop", () => {
340
346
 
341
347
  it("doesnt drag foreign object inside tree if not allowed", () => {
342
348
  tree.mockedHandler("canDropForeignDragObject").mockReturnValue(false);
349
+ tree
350
+ .mockedHandler("canDragForeignDragObjectOver")
351
+ .mockReturnValue(false);
343
352
  const onDropForeignDragObject = tree.mockedHandler(
344
353
  "onDropForeignDragObject",
345
354
  );
@@ -8,13 +8,18 @@ import {
8
8
  } from "./utils";
9
9
  import { makeStateUpdater } from "../../utils";
10
10
 
11
+ const defaultCanDropForeignDragObject = () => false;
11
12
  export const dragAndDropFeature: FeatureImplementation = {
12
13
  key: "drag-and-drop",
13
14
  deps: ["selection"],
14
15
 
15
16
  getDefaultConfig: (defaultConfig, tree) => ({
16
17
  canDrop: (_, target) => target.item.isFolder(),
17
- canDropForeignDragObject: () => false,
18
+ canDropForeignDragObject: defaultCanDropForeignDragObject,
19
+ canDragForeignDragObjectOver:
20
+ defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject
21
+ ? (dataTransfer) => dataTransfer.effectAllowed !== "none"
22
+ : () => false,
18
23
  setDndState: makeStateUpdater("dnd", tree),
19
24
  canReorder: true,
20
25
  ...defaultConfig,
@@ -162,9 +167,13 @@ export const dragAndDropFeature: FeatureImplementation = {
162
167
  e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
163
168
  }
164
169
 
165
- if (config.createForeignDragObject) {
166
- const { format, data } = config.createForeignDragObject(items);
167
- e.dataTransfer?.setData(format, data);
170
+ if (config.createForeignDragObject && e.dataTransfer) {
171
+ const { format, data, dropEffect, effectAllowed } =
172
+ config.createForeignDragObject(items);
173
+ e.dataTransfer.setData(format, data);
174
+
175
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
176
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
168
177
  }
169
178
 
170
179
  tree.applySubStateUpdate("dnd", {
@@ -174,6 +183,7 @@ export const dragAndDropFeature: FeatureImplementation = {
174
183
  },
175
184
 
176
185
  onDragOver: (e: DragEvent) => {
186
+ e.stopPropagation(); // don't bubble up to container dragover
177
187
  const dataRef = tree.getDataRef<DndDataRef>();
178
188
  const nextDragCode = getDragCode(e, item, tree);
179
189
  if (nextDragCode === dataRef.current.lastDragCode) {
@@ -191,7 +201,7 @@ export const dragAndDropFeature: FeatureImplementation = {
191
201
  (!e.dataTransfer ||
192
202
  !tree
193
203
  .getConfig()
194
- .canDropForeignDragObject?.(e.dataTransfer, target))
204
+ .canDragForeignDragObjectOver?.(e.dataTransfer, target))
195
205
  ) {
196
206
  dataRef.current.lastAllowDrop = false;
197
207
  return;
@@ -222,13 +232,24 @@ export const dragAndDropFeature: FeatureImplementation = {
222
232
  },
223
233
 
224
234
  onDragEnd: (e: DragEvent) => {
235
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
236
+ tree.getConfig();
225
237
  const draggedItems = tree.getState().dnd?.draggedItems;
226
238
 
227
239
  if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
228
240
  return;
229
241
  }
230
242
 
231
- tree.getConfig().onCompleteForeignDrop?.(draggedItems);
243
+ const target = getDragTarget(e, item, tree);
244
+ if (
245
+ canDragForeignDragObjectOver &&
246
+ e.dataTransfer &&
247
+ !canDragForeignDragObjectOver(e.dataTransfer, target)
248
+ ) {
249
+ return;
250
+ }
251
+
252
+ onCompleteForeignDrop?.(draggedItems);
232
253
  },
233
254
 
234
255
  onDrop: async (e: DragEvent) => {
@@ -58,16 +58,31 @@ export type DragAndDropFeatureDef<T> = {
58
58
  createForeignDragObject?: (items: ItemInstance<T>[]) => {
59
59
  format: string;
60
60
  data: any;
61
+ dropEffect?: DataTransfer["dropEffect"];
62
+ effectAllowed?: DataTransfer["effectAllowed"];
61
63
  };
62
64
  setDragImage?: (items: ItemInstance<T>[]) => {
63
65
  imgElement: Element;
64
66
  xOffset?: number;
65
67
  yOffset?: number;
66
68
  };
69
+
70
+ /** Checks if a foreign drag object can be dropped on a target, validating that an actual drop can commence based on
71
+ * the data in the DataTransfer object. */
67
72
  canDropForeignDragObject?: (
68
73
  dataTransfer: DataTransfer,
69
74
  target: DragTarget<T>,
70
75
  ) => boolean;
76
+
77
+ /** Checks if a droppable visualization should be displayed when dragging a foreign object over a target. Since this
78
+ * is executed on a dragover event, `dataTransfer.getData()` is not available, so `dataTransfer.effectAllowed` or
79
+ * `dataTransfer.types` should be used instead. Before actually completing the drag, @{link canDropForeignDragObject}
80
+ * will be called by HT before applying the drop. */
81
+ canDragForeignDragObjectOver?: (
82
+ dataTransfer: DataTransfer,
83
+ target: DragTarget<T>,
84
+ ) => boolean;
85
+
71
86
  onDrop?: (
72
87
  items: ItemInstance<T>[],
73
88
  target: DragTarget<T>,
@@ -36,8 +36,6 @@ export type MainFeatureDef<T = any> = {
36
36
  stateName: K,
37
37
  updater: Updater<TreeState<T>[K]>,
38
38
  ) => void;
39
- /** @internal */
40
- buildItemInstance: (itemId: string) => ItemInstance<T>;
41
39
  setState: SetStateFn<TreeState<T>>;
42
40
  getState: () => TreeState<T>;
43
41
  setConfig: SetStateFn<TreeConfig<T>>;