@headless-tree/core 0.0.0-20260108010329 → 0.0.0-20260109213608

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-20260108010329
3
+ ## 0.0.0-20260109213608
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
@@ -70,6 +70,9 @@ type DragAndDropFeatureDef<T> = {
70
70
  openOnDropDelay?: number;
71
71
  /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
72
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;
73
76
  };
74
77
  treeInstance: {
75
78
  getDragTarget: () => DragTarget<T> | null;
package/dist/index.d.ts CHANGED
@@ -70,6 +70,9 @@ type DragAndDropFeatureDef<T> = {
70
70
  openOnDropDelay?: number;
71
71
  /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
72
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;
73
76
  };
74
77
  treeInstance: {
75
78
  getDragTarget: () => DragTarget<T> | null;
package/dist/index.js CHANGED
@@ -1025,6 +1025,7 @@ var checkboxesFeature = {
1025
1025
  };
1026
1026
 
1027
1027
  // src/features/hotkeys-core/feature.ts
1028
+ var resolveKeyCode = (e) => e.code !== "" && e.code !== "Unidentified" ? e.code : e.key;
1028
1029
  var specialKeys = {
1029
1030
  // TODO:breaking deprecate auto-lowercase
1030
1031
  letter: /^Key[A-Z]$/,
@@ -1073,9 +1074,10 @@ var hotkeysCoreFeature = {
1073
1074
  if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
1074
1075
  return;
1075
1076
  }
1077
+ const resolvedCode = resolveKeyCode(e);
1076
1078
  (_b = (_a = data.current).pressedKeys) != null ? _b : _a.pressedKeys = /* @__PURE__ */ new Set();
1077
- const newMatch = !data.current.pressedKeys.has(e.code);
1078
- data.current.pressedKeys.add(e.code);
1079
+ const newMatch = !data.current.pressedKeys.has(resolvedCode);
1080
+ data.current.pressedKeys.add(resolvedCode);
1079
1081
  const hotkeyName = findHotkeyMatch(
1080
1082
  data.current.pressedKeys,
1081
1083
  tree,
@@ -1083,7 +1085,7 @@ var hotkeysCoreFeature = {
1083
1085
  hotkeys
1084
1086
  );
1085
1087
  if (e.target instanceof HTMLInputElement) {
1086
- data.current.pressedKeys.delete(e.code);
1088
+ data.current.pressedKeys.delete(resolvedCode);
1087
1089
  }
1088
1090
  if (!hotkeyName) return;
1089
1091
  const hotkeyConfig = __spreadValues(__spreadValues({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys == null ? void 0 : hotkeys[hotkeyName]);
@@ -1098,7 +1100,7 @@ var hotkeysCoreFeature = {
1098
1100
  const keyup = (e) => {
1099
1101
  var _a, _b;
1100
1102
  (_b = (_a = data.current).pressedKeys) != null ? _b : _a.pressedKeys = /* @__PURE__ */ new Set();
1101
- data.current.pressedKeys.delete(e.code);
1103
+ data.current.pressedKeys.delete(resolveKeyCode(e));
1102
1104
  };
1103
1105
  const reset = () => {
1104
1106
  data.current.pressedKeys = /* @__PURE__ */ new Set();
@@ -1527,7 +1529,8 @@ var dragAndDropFeature = {
1527
1529
  canDragForeignDragObjectOver: defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject ? (dataTransfer) => dataTransfer.effectAllowed !== "none" : () => false,
1528
1530
  setDndState: makeStateUpdater("dnd", tree),
1529
1531
  canReorder: true,
1530
- openOnDropDelay: 800
1532
+ openOnDropDelay: 800,
1533
+ draggedItemOverwritesSelection: true
1531
1534
  }, defaultConfig),
1532
1535
  stateHandlerNames: {
1533
1536
  dnd: "setDndState"
@@ -1697,10 +1700,12 @@ var dragAndDropFeature = {
1697
1700
  draggable: true,
1698
1701
  onDragStart: (e) => {
1699
1702
  var _a, _b, _c, _d;
1703
+ const { draggedItemOverwritesSelection } = tree.getConfig();
1700
1704
  const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1701
- const items = selectedItems.includes(item) ? selectedItems : [item];
1705
+ const overwriteSelection = !selectedItems.includes(item) && draggedItemOverwritesSelection;
1706
+ const items = overwriteSelection ? [item] : selectedItems;
1702
1707
  const config = tree.getConfig();
1703
- if (!selectedItems.includes(item)) {
1708
+ if (overwriteSelection) {
1704
1709
  (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1705
1710
  }
1706
1711
  if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
package/dist/index.mjs CHANGED
@@ -981,6 +981,7 @@ var checkboxesFeature = {
981
981
  };
982
982
 
983
983
  // src/features/hotkeys-core/feature.ts
984
+ var resolveKeyCode = (e) => e.code !== "" && e.code !== "Unidentified" ? e.code : e.key;
984
985
  var specialKeys = {
985
986
  // TODO:breaking deprecate auto-lowercase
986
987
  letter: /^Key[A-Z]$/,
@@ -1029,9 +1030,10 @@ var hotkeysCoreFeature = {
1029
1030
  if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
1030
1031
  return;
1031
1032
  }
1033
+ const resolvedCode = resolveKeyCode(e);
1032
1034
  (_b = (_a = data.current).pressedKeys) != null ? _b : _a.pressedKeys = /* @__PURE__ */ new Set();
1033
- const newMatch = !data.current.pressedKeys.has(e.code);
1034
- data.current.pressedKeys.add(e.code);
1035
+ const newMatch = !data.current.pressedKeys.has(resolvedCode);
1036
+ data.current.pressedKeys.add(resolvedCode);
1035
1037
  const hotkeyName = findHotkeyMatch(
1036
1038
  data.current.pressedKeys,
1037
1039
  tree,
@@ -1039,7 +1041,7 @@ var hotkeysCoreFeature = {
1039
1041
  hotkeys
1040
1042
  );
1041
1043
  if (e.target instanceof HTMLInputElement) {
1042
- data.current.pressedKeys.delete(e.code);
1044
+ data.current.pressedKeys.delete(resolvedCode);
1043
1045
  }
1044
1046
  if (!hotkeyName) return;
1045
1047
  const hotkeyConfig = __spreadValues(__spreadValues({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys == null ? void 0 : hotkeys[hotkeyName]);
@@ -1054,7 +1056,7 @@ var hotkeysCoreFeature = {
1054
1056
  const keyup = (e) => {
1055
1057
  var _a, _b;
1056
1058
  (_b = (_a = data.current).pressedKeys) != null ? _b : _a.pressedKeys = /* @__PURE__ */ new Set();
1057
- data.current.pressedKeys.delete(e.code);
1059
+ data.current.pressedKeys.delete(resolveKeyCode(e));
1058
1060
  };
1059
1061
  const reset = () => {
1060
1062
  data.current.pressedKeys = /* @__PURE__ */ new Set();
@@ -1483,7 +1485,8 @@ var dragAndDropFeature = {
1483
1485
  canDragForeignDragObjectOver: defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject ? (dataTransfer) => dataTransfer.effectAllowed !== "none" : () => false,
1484
1486
  setDndState: makeStateUpdater("dnd", tree),
1485
1487
  canReorder: true,
1486
- openOnDropDelay: 800
1488
+ openOnDropDelay: 800,
1489
+ draggedItemOverwritesSelection: true
1487
1490
  }, defaultConfig),
1488
1491
  stateHandlerNames: {
1489
1492
  dnd: "setDndState"
@@ -1653,10 +1656,12 @@ var dragAndDropFeature = {
1653
1656
  draggable: true,
1654
1657
  onDragStart: (e) => {
1655
1658
  var _a, _b, _c, _d;
1659
+ const { draggedItemOverwritesSelection } = tree.getConfig();
1656
1660
  const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1657
- const items = selectedItems.includes(item) ? selectedItems : [item];
1661
+ const overwriteSelection = !selectedItems.includes(item) && draggedItemOverwritesSelection;
1662
+ const items = overwriteSelection ? [item] : selectedItems;
1658
1663
  const config = tree.getConfig();
1659
- if (!selectedItems.includes(item)) {
1664
+ if (overwriteSelection) {
1660
1665
  (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1661
1666
  }
1662
1667
  if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "checkbox",
14
14
  "hook"
15
15
  ],
16
- "version": "0.0.0-20260108010329",
16
+ "version": "0.0.0-20260109213608",
17
17
  "main": "dist/index.d.ts",
18
18
  "module": "dist/index.mjs",
19
19
  "types": "dist/index.d.mts",
@@ -787,5 +787,99 @@ describe("core-feature/drag-and-drop", () => {
787
787
  expect(canDrop).toBeCalledTimes(3);
788
788
  });
789
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
+ });
790
884
  });
791
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
 
@@ -280,13 +281,16 @@ export const dragAndDropFeature: FeatureImplementation = {
280
281
  draggable: true,
281
282
 
282
283
  onDragStart: (e: DragEvent) => {
284
+ const { draggedItemOverwritesSelection } = tree.getConfig();
283
285
  const selectedItems = tree.getSelectedItems
284
286
  ? tree.getSelectedItems()
285
287
  : [tree.getFocusedItem()];
286
- const items = selectedItems.includes(item) ? selectedItems : [item];
288
+ const overwriteSelection =
289
+ !selectedItems.includes(item) && draggedItemOverwritesSelection;
290
+ const items = overwriteSelection ? [item] : selectedItems;
287
291
  const config = tree.getConfig();
288
292
 
289
- if (!selectedItems.includes(item)) {
293
+ if (overwriteSelection) {
290
294
  tree.setSelectedItems?.([item.getItemMeta().itemId]);
291
295
  }
292
296
 
@@ -100,6 +100,10 @@ export type DragAndDropFeatureDef<T> = {
100
100
 
101
101
  /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
102
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;
103
107
  };
104
108
  treeInstance: {
105
109
  getDragTarget: () => DragTarget<T> | null;
@@ -5,6 +5,11 @@ import {
5
5
  } from "../../types/core";
6
6
  import { HotkeyConfig, HotkeysCoreDataRef } from "./types";
7
7
 
8
+ // e.code may be empty or "Unidentified" on mobile virtual keyboards
9
+ // or during IME composition, so we fall back to e.key
10
+ const resolveKeyCode = (e: KeyboardEvent): string =>
11
+ e.code !== "" && e.code !== "Unidentified" ? e.code : e.key;
12
+
8
13
  const specialKeys: Record<string, RegExp> = {
9
14
  // TODO:breaking deprecate auto-lowercase
10
15
  letter: /^Key[A-Z]$/,
@@ -71,9 +76,11 @@ export const hotkeysCoreFeature: FeatureImplementation = {
71
76
  return;
72
77
  }
73
78
 
79
+ const resolvedCode = resolveKeyCode(e);
80
+
74
81
  data.current.pressedKeys ??= new Set();
75
- const newMatch = !data.current.pressedKeys.has(e.code);
76
- data.current.pressedKeys.add(e.code);
82
+ const newMatch = !data.current.pressedKeys.has(resolvedCode);
83
+ data.current.pressedKeys.add(resolvedCode);
77
84
 
78
85
  const hotkeyName = findHotkeyMatch(
79
86
  data.current.pressedKeys,
@@ -85,7 +92,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
85
92
  if (e.target instanceof HTMLInputElement) {
86
93
  // JS respects composite keydowns while input elements are focused, and
87
94
  // doesnt send the associated keyup events with the same key name
88
- data.current.pressedKeys.delete(e.code);
95
+ data.current.pressedKeys.delete(resolvedCode);
89
96
  }
90
97
 
91
98
  if (!hotkeyName) return;
@@ -110,7 +117,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
110
117
 
111
118
  const keyup = (e: KeyboardEvent) => {
112
119
  data.current.pressedKeys ??= new Set();
113
- data.current.pressedKeys.delete(e.code);
120
+ data.current.pressedKeys.delete(resolveKeyCode(e));
114
121
  };
115
122
 
116
123
  const reset = () => {