@headless-tree/core 0.0.0-20260107224057 → 0.0.0-20260108162121

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,6 +1,12 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20260107224057
3
+ ## 0.0.0-20260108162121
4
+
5
+ ### Patch Changes
6
+
7
+ - 4397d8c: Added `draggedItemOverwritesSelection` as config option to the Drag Feature. Setting it to false will disable the current default behavior, where dragging an unselected item will overwrite the selection to just the dragged item.
8
+
9
+ ## 1.6.2
4
10
 
5
11
  ### Patch Changes
6
12
 
package/dist/index.d.mts CHANGED
@@ -68,6 +68,11 @@ type DragAndDropFeatureDef<T> = {
68
68
  onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
69
69
  /** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
70
70
  openOnDropDelay?: number;
71
+ /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
72
+ seperateDragHandle?: boolean;
73
+ /** If true, the item that is dragged is not selected, the selected items will be overwritten to just the dragged item.
74
+ * Defaults to true */
75
+ draggedItemOverwritesSelection?: boolean;
71
76
  };
72
77
  treeInstance: {
73
78
  getDragTarget: () => DragTarget<T> | null;
@@ -84,6 +89,9 @@ type DragAndDropFeatureDef<T> = {
84
89
  isDragTargetAbove: () => boolean;
85
90
  isDragTargetBelow: () => boolean;
86
91
  isDraggingOver: () => boolean;
92
+ /** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
93
+ * disable the default behavior and use this on the handler element instead. */
94
+ getDragHandleProps: () => Record<string, any>;
87
95
  };
88
96
  hotkeys: never;
89
97
  };
@@ -422,6 +430,7 @@ interface PropMemoizationDataRef {
422
430
  memo?: {
423
431
  tree?: Record<string, any>;
424
432
  item?: Record<string, any>;
433
+ drag?: Record<string, any>;
425
434
  search?: Record<string, any>;
426
435
  rename?: Record<string, any>;
427
436
  };
package/dist/index.d.ts CHANGED
@@ -68,6 +68,11 @@ type DragAndDropFeatureDef<T> = {
68
68
  onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
69
69
  /** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
70
70
  openOnDropDelay?: number;
71
+ /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
72
+ seperateDragHandle?: boolean;
73
+ /** If true, the item that is dragged is not selected, the selected items will be overwritten to just the dragged item.
74
+ * Defaults to true */
75
+ draggedItemOverwritesSelection?: boolean;
71
76
  };
72
77
  treeInstance: {
73
78
  getDragTarget: () => DragTarget<T> | null;
@@ -84,6 +89,9 @@ type DragAndDropFeatureDef<T> = {
84
89
  isDragTargetAbove: () => boolean;
85
90
  isDragTargetBelow: () => boolean;
86
91
  isDraggingOver: () => boolean;
92
+ /** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
93
+ * disable the default behavior and use this on the handler element instead. */
94
+ getDragHandleProps: () => Record<string, any>;
87
95
  };
88
96
  hotkeys: never;
89
97
  };
@@ -422,6 +430,7 @@ interface PropMemoizationDataRef {
422
430
  memo?: {
423
431
  tree?: Record<string, any>;
424
432
  item?: Record<string, any>;
433
+ drag?: Record<string, any>;
425
434
  search?: Record<string, any>;
426
435
  rename?: Record<string, any>;
427
436
  };
package/dist/index.js CHANGED
@@ -1527,7 +1527,8 @@ var dragAndDropFeature = {
1527
1527
  canDragForeignDragObjectOver: defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject ? (dataTransfer) => dataTransfer.effectAllowed !== "none" : () => false,
1528
1528
  setDndState: makeStateUpdater("dnd", tree),
1529
1529
  canReorder: true,
1530
- openOnDropDelay: 800
1530
+ openOnDropDelay: 800,
1531
+ draggedItemOverwritesSelection: true
1531
1532
  }, defaultConfig),
1532
1533
  stateHandlerNames: {
1533
1534
  dnd: "setDndState"
@@ -1620,36 +1621,8 @@ var dragAndDropFeature = {
1620
1621
  }
1621
1622
  },
1622
1623
  itemInstance: {
1623
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1624
- draggable: true,
1624
+ getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues(__spreadValues({}, prev == null ? void 0 : prev()), tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()), {
1625
1625
  onDragEnter: (e) => e.preventDefault(),
1626
- onDragStart: (e) => {
1627
- var _a, _b, _c, _d;
1628
- const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1629
- const items = selectedItems.includes(item) ? selectedItems : [item];
1630
- const config = tree.getConfig();
1631
- if (!selectedItems.includes(item)) {
1632
- (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1633
- }
1634
- if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1635
- e.preventDefault();
1636
- return;
1637
- }
1638
- if (config.setDragImage) {
1639
- const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1640
- (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1641
- }
1642
- if (config.createForeignDragObject && e.dataTransfer) {
1643
- const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1644
- e.dataTransfer.setData(format, data);
1645
- if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1646
- if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1647
- }
1648
- tree.applySubStateUpdate("dnd", {
1649
- draggedItems: items,
1650
- draggingOverItem: tree.getFocusedItem()
1651
- });
1652
- },
1653
1626
  onDragOver: (e) => {
1654
1627
  var _a, _b, _c;
1655
1628
  e.stopPropagation();
@@ -1693,19 +1666,6 @@ var dragAndDropFeature = {
1693
1666
  }));
1694
1667
  }, 100);
1695
1668
  },
1696
- onDragEnd: (e) => {
1697
- var _a, _b;
1698
- const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1699
- const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1700
- if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1701
- return;
1702
- }
1703
- const target = getDragTarget(e, item, tree, false);
1704
- if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1705
- return;
1706
- }
1707
- onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1708
- },
1709
1669
  onDrop: (e) => __async(null, null, function* () {
1710
1670
  var _a, _b, _c;
1711
1671
  e.stopPropagation();
@@ -1734,6 +1694,51 @@ var dragAndDropFeature = {
1734
1694
  tree.updateDomFocus();
1735
1695
  })
1736
1696
  }),
1697
+ getDragHandleProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1698
+ draggable: true,
1699
+ onDragStart: (e) => {
1700
+ var _a, _b, _c, _d;
1701
+ const { draggedItemOverwritesSelection } = tree.getConfig();
1702
+ const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1703
+ const overwriteSelection = !selectedItems.includes(item) && draggedItemOverwritesSelection;
1704
+ const items = overwriteSelection ? [item] : selectedItems;
1705
+ const config = tree.getConfig();
1706
+ if (overwriteSelection) {
1707
+ (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1708
+ }
1709
+ if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1710
+ e.preventDefault();
1711
+ return;
1712
+ }
1713
+ if (config.setDragImage) {
1714
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1715
+ (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1716
+ }
1717
+ if (config.createForeignDragObject && e.dataTransfer) {
1718
+ const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1719
+ e.dataTransfer.setData(format, data);
1720
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1721
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1722
+ }
1723
+ tree.applySubStateUpdate("dnd", {
1724
+ draggedItems: items,
1725
+ draggingOverItem: tree.getFocusedItem()
1726
+ });
1727
+ },
1728
+ onDragEnd: (e) => {
1729
+ var _a, _b;
1730
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1731
+ const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1732
+ if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1733
+ return;
1734
+ }
1735
+ const target = getDragTarget(e, item, tree, false);
1736
+ if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1737
+ return;
1738
+ }
1739
+ onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1740
+ }
1741
+ }),
1737
1742
  isDragTarget: ({ tree, item }) => {
1738
1743
  const target = tree.getDragTarget();
1739
1744
  return target ? target.item.getId() === item.getId() : false;
@@ -2308,6 +2313,14 @@ var propMemoizationFeature = {
2308
2313
  (_e = (_d = dataRef.current.memo).item) != null ? _e : _d.item = {};
2309
2314
  return memoize(props, dataRef.current.memo.item);
2310
2315
  },
2316
+ getDragHandleProps: ({ item, prev }) => {
2317
+ var _a, _b, _c, _d, _e;
2318
+ const dataRef = item.getDataRef();
2319
+ const props = (_a = prev == null ? void 0 : prev()) != null ? _a : {};
2320
+ (_c = (_b = dataRef.current).memo) != null ? _c : _b.memo = {};
2321
+ (_e = (_d = dataRef.current.memo).drag) != null ? _e : _d.drag = {};
2322
+ return memoize(props, dataRef.current.memo.drag);
2323
+ },
2311
2324
  getRenameInputProps: ({ item, prev }) => {
2312
2325
  var _a, _b, _c, _d, _e;
2313
2326
  const dataRef = item.getDataRef();
package/dist/index.mjs CHANGED
@@ -1483,7 +1483,8 @@ var dragAndDropFeature = {
1483
1483
  canDragForeignDragObjectOver: defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject ? (dataTransfer) => dataTransfer.effectAllowed !== "none" : () => false,
1484
1484
  setDndState: makeStateUpdater("dnd", tree),
1485
1485
  canReorder: true,
1486
- openOnDropDelay: 800
1486
+ openOnDropDelay: 800,
1487
+ draggedItemOverwritesSelection: true
1487
1488
  }, defaultConfig),
1488
1489
  stateHandlerNames: {
1489
1490
  dnd: "setDndState"
@@ -1576,36 +1577,8 @@ var dragAndDropFeature = {
1576
1577
  }
1577
1578
  },
1578
1579
  itemInstance: {
1579
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1580
- draggable: true,
1580
+ getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues(__spreadValues({}, prev == null ? void 0 : prev()), tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()), {
1581
1581
  onDragEnter: (e) => e.preventDefault(),
1582
- onDragStart: (e) => {
1583
- var _a, _b, _c, _d;
1584
- const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1585
- const items = selectedItems.includes(item) ? selectedItems : [item];
1586
- const config = tree.getConfig();
1587
- if (!selectedItems.includes(item)) {
1588
- (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1589
- }
1590
- if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1591
- e.preventDefault();
1592
- return;
1593
- }
1594
- if (config.setDragImage) {
1595
- const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1596
- (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1597
- }
1598
- if (config.createForeignDragObject && e.dataTransfer) {
1599
- const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1600
- e.dataTransfer.setData(format, data);
1601
- if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1602
- if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1603
- }
1604
- tree.applySubStateUpdate("dnd", {
1605
- draggedItems: items,
1606
- draggingOverItem: tree.getFocusedItem()
1607
- });
1608
- },
1609
1582
  onDragOver: (e) => {
1610
1583
  var _a, _b, _c;
1611
1584
  e.stopPropagation();
@@ -1649,19 +1622,6 @@ var dragAndDropFeature = {
1649
1622
  }));
1650
1623
  }, 100);
1651
1624
  },
1652
- onDragEnd: (e) => {
1653
- var _a, _b;
1654
- const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1655
- const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1656
- if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1657
- return;
1658
- }
1659
- const target = getDragTarget(e, item, tree, false);
1660
- if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1661
- return;
1662
- }
1663
- onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1664
- },
1665
1625
  onDrop: (e) => __async(null, null, function* () {
1666
1626
  var _a, _b, _c;
1667
1627
  e.stopPropagation();
@@ -1690,6 +1650,51 @@ var dragAndDropFeature = {
1690
1650
  tree.updateDomFocus();
1691
1651
  })
1692
1652
  }),
1653
+ getDragHandleProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1654
+ draggable: true,
1655
+ onDragStart: (e) => {
1656
+ var _a, _b, _c, _d;
1657
+ const { draggedItemOverwritesSelection } = tree.getConfig();
1658
+ const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1659
+ const overwriteSelection = !selectedItems.includes(item) && draggedItemOverwritesSelection;
1660
+ const items = overwriteSelection ? [item] : selectedItems;
1661
+ const config = tree.getConfig();
1662
+ if (overwriteSelection) {
1663
+ (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1664
+ }
1665
+ if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1666
+ e.preventDefault();
1667
+ return;
1668
+ }
1669
+ if (config.setDragImage) {
1670
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1671
+ (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1672
+ }
1673
+ if (config.createForeignDragObject && e.dataTransfer) {
1674
+ const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1675
+ e.dataTransfer.setData(format, data);
1676
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1677
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1678
+ }
1679
+ tree.applySubStateUpdate("dnd", {
1680
+ draggedItems: items,
1681
+ draggingOverItem: tree.getFocusedItem()
1682
+ });
1683
+ },
1684
+ onDragEnd: (e) => {
1685
+ var _a, _b;
1686
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1687
+ const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1688
+ if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1689
+ return;
1690
+ }
1691
+ const target = getDragTarget(e, item, tree, false);
1692
+ if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1693
+ return;
1694
+ }
1695
+ onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1696
+ }
1697
+ }),
1693
1698
  isDragTarget: ({ tree, item }) => {
1694
1699
  const target = tree.getDragTarget();
1695
1700
  return target ? target.item.getId() === item.getId() : false;
@@ -2264,6 +2269,14 @@ var propMemoizationFeature = {
2264
2269
  (_e = (_d = dataRef.current.memo).item) != null ? _e : _d.item = {};
2265
2270
  return memoize(props, dataRef.current.memo.item);
2266
2271
  },
2272
+ getDragHandleProps: ({ item, prev }) => {
2273
+ var _a, _b, _c, _d, _e;
2274
+ const dataRef = item.getDataRef();
2275
+ const props = (_a = prev == null ? void 0 : prev()) != null ? _a : {};
2276
+ (_c = (_b = dataRef.current).memo) != null ? _c : _b.memo = {};
2277
+ (_e = (_d = dataRef.current.memo).drag) != null ? _e : _d.drag = {};
2278
+ return memoize(props, dataRef.current.memo.drag);
2279
+ },
2267
2280
  getRenameInputProps: ({ item, prev }) => {
2268
2281
  var _a, _b, _c, _d, _e;
2269
2282
  const dataRef = item.getDataRef();
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "checkbox",
14
14
  "hook"
15
15
  ],
16
- "version": "0.0.0-20260107224057",
16
+ "version": "0.0.0-20260108162121",
17
17
  "main": "dist/index.d.ts",
18
18
  "module": "dist/index.mjs",
19
19
  "types": "dist/index.d.mts",
@@ -729,6 +729,54 @@ describe("core-feature/drag-and-drop", () => {
729
729
  });
730
730
  });
731
731
 
732
+ describe("seperateDragHandle", () => {
733
+ it("includes all drag handlers in getProps() when seperateDragHandle is false", () => {
734
+ const props = tree.instance.getItemInstance("x111").getProps();
735
+ expect(props.draggable).toBe(true);
736
+ expect(props.onDragStart).toBeDefined();
737
+ expect(props.onDragEnd).toBeDefined();
738
+ expect(props.onDragEnter).toBeDefined();
739
+ expect(props.onDragOver).toBeDefined();
740
+ expect(props.onDragLeave).toBeDefined();
741
+ expect(props.onDrop).toBeDefined();
742
+ });
743
+
744
+ it("includes drag handle handlers in getDragHandleProps() when seperateDragHandle is false", () => {
745
+ const props = tree.instance
746
+ .getItemInstance("x111")
747
+ .getDragHandleProps();
748
+ expect(props.draggable).toBe(true);
749
+ expect(props.onDragStart).toBeDefined();
750
+ expect(props.onDragEnd).toBeDefined();
751
+ });
752
+
753
+ it("excludes drag handle handlers from getProps() but includes drop handlers when seperateDragHandle is true", async () => {
754
+ const testTree = await tree
755
+ .with({ seperateDragHandle: true })
756
+ .createTestCaseTree();
757
+ const props = testTree.instance.getItemInstance("x111").getProps();
758
+ expect(props.draggable).toBeUndefined();
759
+ expect(props.onDragStart).toBeUndefined();
760
+ expect(props.onDragEnd).toBeUndefined();
761
+ expect(props.onDragEnter).toBeDefined();
762
+ expect(props.onDragOver).toBeDefined();
763
+ expect(props.onDragLeave).toBeDefined();
764
+ expect(props.onDrop).toBeDefined();
765
+ });
766
+
767
+ it("includes drag handle handlers in getDragHandleProps() when seperateDragHandle is true", async () => {
768
+ const testTree = await tree
769
+ .with({ seperateDragHandle: true })
770
+ .createTestCaseTree();
771
+ const props = testTree.instance
772
+ .getItemInstance("x111")
773
+ .getDragHandleProps();
774
+ expect(props.draggable).toBe(true);
775
+ expect(props.onDragStart).toBeDefined();
776
+ expect(props.onDragEnd).toBeDefined();
777
+ });
778
+ });
779
+
732
780
  describe("retains last drag state with dragcode", () => {
733
781
  it("uses constant number of calls to canDrop", () => {
734
782
  const canDrop = tree.mockedHandler("canDrop").mockReturnValue(true);
@@ -739,5 +787,99 @@ describe("core-feature/drag-and-drop", () => {
739
787
  expect(canDrop).toBeCalledTimes(3);
740
788
  });
741
789
  });
790
+
791
+ describe("draggedItemOverwritesSelection", () => {
792
+ it("overwrites selection when dragging unselected item with draggedItemOverwritesSelection=true", () => {
793
+ tree.do.selectItem("x111");
794
+ tree.do.ctrlSelectItem("x112");
795
+ tree.do.ctrlSelectItem("x113");
796
+
797
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
798
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
799
+ expect(tree.instance.getItemInstance("x113").isSelected()).toBe(true);
800
+ expect(tree.instance.getItemInstance("x114").isSelected()).toBe(false);
801
+
802
+ tree.do.startDrag("x114");
803
+
804
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(false);
805
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(false);
806
+ expect(tree.instance.getItemInstance("x113").isSelected()).toBe(false);
807
+ expect(tree.instance.getItemInstance("x114").isSelected()).toBe(true);
808
+ });
809
+
810
+ it("preserves selection when dragging unselected item with draggedItemOverwritesSelection=false", async () => {
811
+ const testTree = await tree
812
+ .with({ draggedItemOverwritesSelection: false })
813
+ .createTestCaseTree();
814
+
815
+ testTree.do.selectItem("x111");
816
+ testTree.do.ctrlSelectItem("x112");
817
+ testTree.do.ctrlSelectItem("x113");
818
+
819
+ expect(testTree.item("x111").isSelected()).toBe(true);
820
+ expect(testTree.item("x112").isSelected()).toBe(true);
821
+ expect(testTree.item("x113").isSelected()).toBe(true);
822
+ expect(testTree.item("x114").isSelected()).toBe(false);
823
+ testTree.do.startDrag("x114");
824
+
825
+ expect(testTree.item("x111").isSelected()).toBe(true);
826
+ expect(testTree.item("x112").isSelected()).toBe(true);
827
+ expect(testTree.item("x113").isSelected()).toBe(true);
828
+ expect(testTree.item("x114").isSelected()).toBe(false);
829
+ });
830
+
831
+ it("does not overwrite selection when dragging selected item with draggedItemOverwritesSelection=true", () => {
832
+ tree.do.selectItem("x111");
833
+ tree.do.ctrlSelectItem("x112");
834
+ tree.do.ctrlSelectItem("x113");
835
+
836
+ expect(tree.item("x111").isSelected()).toBe(true);
837
+ expect(tree.item("x112").isSelected()).toBe(true);
838
+ expect(tree.item("x113").isSelected()).toBe(true);
839
+
840
+ tree.do.startDrag("x111");
841
+
842
+ expect(tree.item("x111").isSelected()).toBe(true);
843
+ expect(tree.item("x112").isSelected()).toBe(true);
844
+ expect(tree.item("x113").isSelected()).toBe(true);
845
+ });
846
+
847
+ it("does not overwrite selection when dragging selected item with draggedItemOverwritesSelection=false", async () => {
848
+ const testTree = await tree
849
+ .with({ draggedItemOverwritesSelection: false })
850
+ .createTestCaseTree();
851
+
852
+ testTree.do.selectItem("x111");
853
+ testTree.do.ctrlSelectItem("x112");
854
+ testTree.do.ctrlSelectItem("x113");
855
+
856
+ expect(testTree.item("x111").isSelected()).toBe(true);
857
+ expect(testTree.item("x112").isSelected()).toBe(true);
858
+ expect(testTree.item("x113").isSelected()).toBe(true);
859
+
860
+ testTree.do.startDrag("x111");
861
+
862
+ expect(testTree.item("x111").isSelected()).toBe(true);
863
+ expect(testTree.item("x112").isSelected()).toBe(true);
864
+ expect(testTree.item("x113").isSelected()).toBe(true);
865
+ });
866
+
867
+ it("drags all selected items when draggedItemOverwritesSelection=false", async () => {
868
+ const testTree = await tree
869
+ .with({ draggedItemOverwritesSelection: false })
870
+ .createTestCaseTree();
871
+
872
+ testTree.do.selectItem("x111");
873
+ testTree.do.ctrlSelectItem("x112");
874
+ testTree.do.ctrlSelectItem("x113");
875
+
876
+ testTree.do.startDrag("x111");
877
+ testTree.do.dragOverAndDrop("x21");
878
+
879
+ testTree.expect.dropped(["x111", "x112", "x113"], {
880
+ item: testTree.item("x21"),
881
+ });
882
+ });
883
+ });
742
884
  });
743
885
  });
@@ -57,6 +57,7 @@ export const dragAndDropFeature: FeatureImplementation = {
57
57
  setDndState: makeStateUpdater("dnd", tree),
58
58
  canReorder: true,
59
59
  openOnDropDelay: 800,
60
+ draggedItemOverwritesSelection: true,
60
61
  ...defaultConfig,
61
62
  }),
62
63
 
@@ -178,47 +179,10 @@ export const dragAndDropFeature: FeatureImplementation = {
178
179
  itemInstance: {
179
180
  getProps: ({ tree, item, prev }) => ({
180
181
  ...prev?.(),
181
-
182
- draggable: true,
182
+ ...(tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()),
183
183
 
184
184
  onDragEnter: (e: DragEvent) => e.preventDefault(),
185
185
 
186
- onDragStart: (e: DragEvent) => {
187
- const selectedItems = tree.getSelectedItems
188
- ? tree.getSelectedItems()
189
- : [tree.getFocusedItem()];
190
- const items = selectedItems.includes(item) ? selectedItems : [item];
191
- const config = tree.getConfig();
192
-
193
- if (!selectedItems.includes(item)) {
194
- tree.setSelectedItems?.([item.getItemMeta().itemId]);
195
- }
196
-
197
- if (!(config.canDrag?.(items) ?? true)) {
198
- e.preventDefault();
199
- return;
200
- }
201
-
202
- if (config.setDragImage) {
203
- const { imgElement, xOffset, yOffset } = config.setDragImage(items);
204
- e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
205
- }
206
-
207
- if (config.createForeignDragObject && e.dataTransfer) {
208
- const { format, data, dropEffect, effectAllowed } =
209
- config.createForeignDragObject(items);
210
- e.dataTransfer.setData(format, data);
211
-
212
- if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
213
- if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
214
- }
215
-
216
- tree.applySubStateUpdate("dnd", {
217
- draggedItems: items,
218
- draggingOverItem: tree.getFocusedItem(),
219
- });
220
- },
221
-
222
186
  onDragOver: (e: DragEvent) => {
223
187
  e.stopPropagation(); // don't bubble up to container dragover
224
188
  const dataRef = tree.getDataRef<DndDataRef>();
@@ -277,27 +241,6 @@ export const dragAndDropFeature: FeatureImplementation = {
277
241
  }, 100);
278
242
  },
279
243
 
280
- onDragEnd: (e: DragEvent) => {
281
- const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
282
- tree.getConfig();
283
- const draggedItems = tree.getState().dnd?.draggedItems;
284
-
285
- if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
286
- return;
287
- }
288
-
289
- const target = getDragTarget(e, item, tree, false);
290
- if (
291
- canDragForeignDragObjectOver &&
292
- e.dataTransfer &&
293
- !canDragForeignDragObjectOver(e.dataTransfer, target)
294
- ) {
295
- return;
296
- }
297
-
298
- onCompleteForeignDrop?.(draggedItems);
299
- },
300
-
301
244
  onDrop: async (e: DragEvent) => {
302
245
  e.stopPropagation();
303
246
  const dataRef = tree.getDataRef<DndDataRef>();
@@ -332,6 +275,72 @@ export const dragAndDropFeature: FeatureImplementation = {
332
275
  },
333
276
  }),
334
277
 
278
+ getDragHandleProps: ({ tree, item, prev }) => ({
279
+ ...prev?.(),
280
+
281
+ draggable: true,
282
+
283
+ onDragStart: (e: DragEvent) => {
284
+ const { draggedItemOverwritesSelection } = tree.getConfig();
285
+ const selectedItems = tree.getSelectedItems
286
+ ? tree.getSelectedItems()
287
+ : [tree.getFocusedItem()];
288
+ const overwriteSelection =
289
+ !selectedItems.includes(item) && draggedItemOverwritesSelection;
290
+ const items = overwriteSelection ? [item] : selectedItems;
291
+ const config = tree.getConfig();
292
+
293
+ if (overwriteSelection) {
294
+ tree.setSelectedItems?.([item.getItemMeta().itemId]);
295
+ }
296
+
297
+ if (!(config.canDrag?.(items) ?? true)) {
298
+ e.preventDefault();
299
+ return;
300
+ }
301
+
302
+ if (config.setDragImage) {
303
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
304
+ e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
305
+ }
306
+
307
+ if (config.createForeignDragObject && e.dataTransfer) {
308
+ const { format, data, dropEffect, effectAllowed } =
309
+ config.createForeignDragObject(items);
310
+ e.dataTransfer.setData(format, data);
311
+
312
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
313
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
314
+ }
315
+
316
+ tree.applySubStateUpdate("dnd", {
317
+ draggedItems: items,
318
+ draggingOverItem: tree.getFocusedItem(),
319
+ });
320
+ },
321
+
322
+ onDragEnd: (e: DragEvent) => {
323
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
324
+ tree.getConfig();
325
+ const draggedItems = tree.getState().dnd?.draggedItems;
326
+
327
+ if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
328
+ return;
329
+ }
330
+
331
+ const target = getDragTarget(e, item, tree, false);
332
+ if (
333
+ canDragForeignDragObjectOver &&
334
+ e.dataTransfer &&
335
+ !canDragForeignDragObjectOver(e.dataTransfer, target)
336
+ ) {
337
+ return;
338
+ }
339
+
340
+ onCompleteForeignDrop?.(draggedItems);
341
+ },
342
+ }),
343
+
335
344
  isDragTarget: ({ tree, item }) => {
336
345
  const target = tree.getDragTarget();
337
346
  return target ? target.item.getId() === item.getId() : false;
@@ -97,6 +97,13 @@ export type DragAndDropFeatureDef<T> = {
97
97
 
98
98
  /** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
99
99
  openOnDropDelay?: number;
100
+
101
+ /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
102
+ seperateDragHandle?: boolean;
103
+
104
+ /** If true, the item that is dragged is not selected, the selected items will be overwritten to just the dragged item.
105
+ * Defaults to true */
106
+ draggedItemOverwritesSelection?: boolean;
100
107
  };
101
108
  treeInstance: {
102
109
  getDragTarget: () => DragTarget<T> | null;
@@ -118,6 +125,10 @@ export type DragAndDropFeatureDef<T> = {
118
125
  isDragTargetAbove: () => boolean;
119
126
  isDragTargetBelow: () => boolean;
120
127
  isDraggingOver: () => boolean;
128
+
129
+ /** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
130
+ * disable the default behavior and use this on the handler element instead. */
131
+ getDragHandleProps: () => Record<string, any>;
121
132
  };
122
133
  hotkeys: never;
123
134
  };
@@ -59,6 +59,14 @@ export const propMemoizationFeature: FeatureImplementation = {
59
59
  return memoize(props, dataRef.current.memo.item);
60
60
  },
61
61
 
62
+ getDragHandleProps: ({ item, prev }) => {
63
+ const dataRef = item.getDataRef<PropMemoizationDataRef>();
64
+ const props = prev?.() ?? {};
65
+ dataRef.current.memo ??= {};
66
+ dataRef.current.memo.drag ??= {};
67
+ return memoize(props, dataRef.current.memo.drag);
68
+ },
69
+
62
70
  getRenameInputProps: ({ item, prev }) => {
63
71
  const dataRef = item.getDataRef<PropMemoizationDataRef>();
64
72
  const props = prev?.() ?? {};
@@ -2,6 +2,7 @@ export interface PropMemoizationDataRef {
2
2
  memo?: {
3
3
  tree?: Record<string, any>;
4
4
  item?: Record<string, any>;
5
+ drag?: Record<string, any>;
5
6
  search?: Record<string, any>;
6
7
  rename?: Record<string, any>;
7
8
  };