@headless-tree/core 1.0.1 → 1.2.0

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 (58) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/lib/cjs/core/create-tree.js +13 -2
  3. package/lib/cjs/features/async-data-loader/feature.js +78 -68
  4. package/lib/cjs/features/async-data-loader/types.d.ts +12 -7
  5. package/lib/cjs/features/drag-and-drop/feature.js +1 -0
  6. package/lib/cjs/features/expand-all/feature.js +2 -2
  7. package/lib/cjs/features/hotkeys-core/feature.js +50 -18
  8. package/lib/cjs/features/hotkeys-core/types.d.ts +3 -0
  9. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +1 -1
  10. package/lib/cjs/features/renaming/feature.js +8 -0
  11. package/lib/cjs/features/selection/feature.js +1 -1
  12. package/lib/cjs/features/sync-data-loader/feature.js +13 -9
  13. package/lib/cjs/features/sync-data-loader/types.d.ts +11 -2
  14. package/lib/cjs/features/tree/feature.js +1 -0
  15. package/lib/cjs/features/tree/types.d.ts +1 -0
  16. package/lib/cjs/index.d.ts +2 -0
  17. package/lib/cjs/index.js +5 -0
  18. package/lib/cjs/test-utils/test-tree-expect.js +1 -0
  19. package/lib/cjs/test-utils/test-tree.js +1 -0
  20. package/lib/esm/core/create-tree.js +13 -2
  21. package/lib/esm/features/async-data-loader/feature.js +78 -68
  22. package/lib/esm/features/async-data-loader/types.d.ts +12 -7
  23. package/lib/esm/features/drag-and-drop/feature.js +1 -0
  24. package/lib/esm/features/expand-all/feature.js +2 -2
  25. package/lib/esm/features/hotkeys-core/feature.js +50 -18
  26. package/lib/esm/features/hotkeys-core/types.d.ts +3 -0
  27. package/lib/esm/features/keyboard-drag-and-drop/feature.js +1 -1
  28. package/lib/esm/features/renaming/feature.js +8 -0
  29. package/lib/esm/features/selection/feature.js +1 -1
  30. package/lib/esm/features/sync-data-loader/feature.js +13 -9
  31. package/lib/esm/features/sync-data-loader/types.d.ts +11 -2
  32. package/lib/esm/features/tree/feature.js +1 -0
  33. package/lib/esm/features/tree/types.d.ts +1 -0
  34. package/lib/esm/index.d.ts +2 -0
  35. package/lib/esm/index.js +2 -0
  36. package/lib/esm/test-utils/test-tree-expect.js +1 -0
  37. package/lib/esm/test-utils/test-tree.js +1 -0
  38. package/package.json +7 -2
  39. package/readme.md +2 -2
  40. package/src/core/create-tree.ts +20 -2
  41. package/src/features/async-data-loader/async-data-loader.spec.ts +82 -0
  42. package/src/features/async-data-loader/feature.ts +92 -67
  43. package/src/features/async-data-loader/types.ts +14 -7
  44. package/src/features/drag-and-drop/feature.ts +1 -0
  45. package/src/features/expand-all/feature.ts +2 -2
  46. package/src/features/hotkeys-core/feature.ts +56 -17
  47. package/src/features/hotkeys-core/types.ts +4 -0
  48. package/src/features/keyboard-drag-and-drop/feature.ts +1 -1
  49. package/src/features/renaming/feature.ts +13 -0
  50. package/src/features/renaming/renaming.spec.ts +31 -0
  51. package/src/features/selection/feature.ts +1 -1
  52. package/src/features/sync-data-loader/feature.ts +16 -9
  53. package/src/features/sync-data-loader/types.ts +11 -4
  54. package/src/features/tree/feature.ts +1 -0
  55. package/src/features/tree/types.ts +1 -0
  56. package/src/index.ts +3 -0
  57. package/src/test-utils/test-tree-expect.ts +1 -0
  58. package/src/test-utils/test-tree.ts +1 -0
@@ -11,6 +11,18 @@ const verifyFeatures = (features) => {
11
11
  }
12
12
  }
13
13
  };
14
+ // Check all possible pairs and sort the array
15
+ const exhaustiveSort = (arr, compareFn) => {
16
+ const n = arr.length;
17
+ for (let i = 0; i < n; i++) {
18
+ for (let j = i + 1; j < n; j++) {
19
+ if (compareFn(arr[j], arr[i]) < 0) {
20
+ [arr[i], arr[j]] = [arr[j], arr[i]];
21
+ }
22
+ }
23
+ }
24
+ return arr;
25
+ };
14
26
  const compareFeatures = (originalOrder) => (feature1, feature2) => {
15
27
  var _a, _b;
16
28
  if (feature2.key && ((_a = feature1.overwrites) === null || _a === void 0 ? void 0 : _a.includes(feature2.key))) {
@@ -21,7 +33,7 @@ const compareFeatures = (originalOrder) => (feature1, feature2) => {
21
33
  }
22
34
  return originalOrder.indexOf(feature1) - originalOrder.indexOf(feature2);
23
35
  };
24
- const sortFeatures = (features = []) => features.sort(compareFeatures(features));
36
+ const sortFeatures = (features = []) => exhaustiveSort(features, compareFeatures(features));
25
37
  export const createTree = (initialConfig) => {
26
38
  var _a, _b, _c, _d;
27
39
  const buildInstance = (_a = initialConfig.instanceBuilder) !== null && _a !== void 0 ? _a : buildStaticInstance;
@@ -162,6 +174,5 @@ export const createTree = (initialConfig) => {
162
174
  Object.assign(hotkeyPresets, (_d = feature.hotkeys) !== null && _d !== void 0 ? _d : {});
163
175
  }
164
176
  finalizeTree();
165
- rebuildItemMeta();
166
177
  return treeInstance;
167
178
  };
@@ -8,6 +8,51 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { makeStateUpdater } from "../../utils";
11
+ const getDataRef = (tree) => {
12
+ var _a, _b;
13
+ var _c, _d;
14
+ const dataRef = tree.getDataRef();
15
+ (_a = (_c = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_c.itemData = {});
16
+ (_b = (_d = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_d.childrenIds = {});
17
+ return dataRef;
18
+ };
19
+ const loadItemData = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
20
+ var _a;
21
+ const config = tree.getConfig();
22
+ const dataRef = getDataRef(tree);
23
+ const item = yield config.dataLoader.getItem(itemId);
24
+ dataRef.current.itemData[itemId] = item;
25
+ (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
26
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
27
+ return item;
28
+ });
29
+ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
30
+ var _a, _b;
31
+ const config = tree.getConfig();
32
+ const dataRef = getDataRef(tree);
33
+ let childrenIds;
34
+ if ("getChildrenWithData" in config.dataLoader) {
35
+ const children = yield config.dataLoader.getChildrenWithData(itemId);
36
+ childrenIds = children.map((c) => c.id);
37
+ dataRef.current.childrenIds[itemId] = childrenIds;
38
+ children.forEach(({ id, data }) => {
39
+ var _a;
40
+ dataRef.current.itemData[id] = data;
41
+ (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, id, data);
42
+ });
43
+ (_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
44
+ tree.rebuildTree();
45
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
46
+ }
47
+ else {
48
+ childrenIds = yield config.dataLoader.getChildren(itemId);
49
+ dataRef.current.childrenIds[itemId] = childrenIds;
50
+ (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
51
+ tree.rebuildTree();
52
+ }
53
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
54
+ return childrenIds;
55
+ });
11
56
  export const asyncDataLoaderFeature = {
12
57
  key: "async-data-loader",
13
58
  getInitialState: (initialState) => (Object.assign({ loadingItemData: [], loadingItemChildrens: [] }, initialState)),
@@ -17,41 +62,20 @@ export const asyncDataLoaderFeature = {
17
62
  loadingItemChildrens: "setLoadingItemChildrens",
18
63
  },
19
64
  treeInstance: {
20
- waitForItemDataLoaded: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
21
- tree.retrieveItemData(itemId);
22
- if (!tree.getState().loadingItemData.includes(itemId)) {
23
- return;
24
- }
25
- yield new Promise((resolve) => {
26
- var _a, _b;
27
- var _c, _d;
28
- const dataRef = tree.getDataRef();
29
- (_a = (_c = dataRef.current).awaitingItemDataLoading) !== null && _a !== void 0 ? _a : (_c.awaitingItemDataLoading = {});
30
- (_b = (_d = dataRef.current.awaitingItemDataLoading)[itemId]) !== null && _b !== void 0 ? _b : (_d[itemId] = []);
31
- dataRef.current.awaitingItemDataLoading[itemId].push(resolve);
32
- });
65
+ waitForItemDataLoaded: ({ tree }, itemId) => tree.loadItemData(itemId),
66
+ waitForItemChildrenLoaded: ({ tree }, itemId) => tree.loadChildrenIds(itemId),
67
+ loadItemData: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
68
+ var _b;
69
+ return ((_b = getDataRef(tree).current.itemData[itemId]) !== null && _b !== void 0 ? _b : (yield loadItemData(tree, itemId)));
33
70
  }),
34
- waitForItemChildrenLoaded: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
35
- tree.retrieveChildrenIds(itemId);
36
- if (!tree.getState().loadingItemChildrens.includes(itemId)) {
37
- return;
38
- }
39
- yield new Promise((resolve) => {
40
- var _a, _b;
41
- var _c, _d;
42
- const dataRef = tree.getDataRef();
43
- (_a = (_c = dataRef.current).awaitingItemChildrensLoading) !== null && _a !== void 0 ? _a : (_c.awaitingItemChildrensLoading = {});
44
- (_b = (_d = dataRef.current.awaitingItemChildrensLoading)[itemId]) !== null && _b !== void 0 ? _b : (_d[itemId] = []);
45
- dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
46
- });
71
+ loadChildrenIds: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
72
+ var _b;
73
+ return ((_b = getDataRef(tree).current.childrenIds[itemId]) !== null && _b !== void 0 ? _b : (yield loadChildrenIds(tree, itemId)));
47
74
  }),
48
75
  retrieveItemData: ({ tree }, itemId) => {
49
- var _a, _b, _c, _d;
50
- var _e, _f;
76
+ var _a, _b;
51
77
  const config = tree.getConfig();
52
- const dataRef = tree.getDataRef();
53
- (_a = (_e = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_e.itemData = {});
54
- (_b = (_f = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_f.childrenIds = {});
78
+ const dataRef = getDataRef(tree);
55
79
  if (dataRef.current.itemData[itemId]) {
56
80
  return dataRef.current.itemData[itemId];
57
81
  }
@@ -60,24 +84,12 @@ export const asyncDataLoaderFeature = {
60
84
  ...loadingItemData,
61
85
  itemId,
62
86
  ]);
63
- (() => __awaiter(void 0, void 0, void 0, function* () {
64
- var _a, _b, _c;
65
- const item = yield config.dataLoader.getItem(itemId);
66
- dataRef.current.itemData[itemId] = item;
67
- (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
68
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
69
- (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
70
- (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
71
- }))();
87
+ loadItemData(tree, itemId);
72
88
  }
73
- return (_d = (_c = config.createLoadingItemData) === null || _c === void 0 ? void 0 : _c.call(config)) !== null && _d !== void 0 ? _d : null;
89
+ return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
74
90
  },
75
91
  retrieveChildrenIds: ({ tree }, itemId) => {
76
- var _a;
77
- var _b;
78
- const config = tree.getConfig();
79
- const dataRef = tree.getDataRef();
80
- (_a = (_b = dataRef.current).childrenIds) !== null && _a !== void 0 ? _a : (_b.childrenIds = {});
92
+ const dataRef = getDataRef(tree);
81
93
  if (dataRef.current.childrenIds[itemId]) {
82
94
  return dataRef.current.childrenIds[itemId];
83
95
  }
@@ -85,34 +97,32 @@ export const asyncDataLoaderFeature = {
85
97
  return [];
86
98
  }
87
99
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
88
- (() => __awaiter(void 0, void 0, void 0, function* () {
89
- var _a, _b, _c, _d;
90
- const childrenIds = yield config.dataLoader.getChildren(itemId);
91
- dataRef.current.childrenIds[itemId] = childrenIds;
92
- (_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
93
- tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
94
- tree.rebuildTree();
95
- (_c = (_b = dataRef.current.awaitingItemChildrensLoading) === null || _b === void 0 ? void 0 : _b[itemId]) === null || _c === void 0 ? void 0 : _c.forEach((cb) => cb());
96
- (_d = dataRef.current.awaitingItemChildrensLoading) === null || _d === void 0 ? true : delete _d[itemId];
97
- }))();
100
+ loadChildrenIds(tree, itemId);
98
101
  return [];
99
102
  },
100
103
  },
101
104
  itemInstance: {
102
105
  isLoading: ({ tree, item }) => tree.getState().loadingItemData.includes(item.getItemMeta().itemId) ||
103
106
  tree.getState().loadingItemChildrens.includes(item.getItemMeta().itemId),
104
- invalidateItemData: ({ tree, itemId }) => {
105
- var _a;
106
- const dataRef = tree.getDataRef();
107
- (_a = dataRef.current.itemData) === null || _a === void 0 ? true : delete _a[itemId];
108
- tree.retrieveItemData(itemId);
109
- },
110
- invalidateChildrenIds: ({ tree, itemId }) => {
111
- var _a;
112
- const dataRef = tree.getDataRef();
113
- (_a = dataRef.current.childrenIds) === null || _a === void 0 ? true : delete _a[itemId];
114
- tree.retrieveChildrenIds(itemId);
115
- },
107
+ invalidateItemData: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
108
+ var _b;
109
+ if (!optimistic) {
110
+ (_b = getDataRef(tree).current.itemData) === null || _b === void 0 ? true : delete _b[itemId];
111
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
112
+ ...loadingItemData,
113
+ itemId,
114
+ ]);
115
+ }
116
+ yield loadItemData(tree, itemId);
117
+ }),
118
+ invalidateChildrenIds: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
119
+ var _b;
120
+ if (!optimistic) {
121
+ (_b = getDataRef(tree).current.childrenIds) === null || _b === void 0 ? true : delete _b[itemId];
122
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
123
+ }
124
+ yield loadChildrenIds(tree, itemId);
125
+ }),
116
126
  updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
117
127
  const dataRef = tree.getDataRef();
118
128
  dataRef.current.childrenIds[itemId] = childrenIds;
@@ -1,11 +1,8 @@
1
1
  import { SetStateFn } from "../../types/core";
2
2
  import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
3
- type AwaitingLoaderCallbacks = Record<string, (() => void)[]>;
4
3
  export interface AsyncDataLoaderDataRef<T = any> {
5
4
  itemData: Record<string, T>;
6
5
  childrenIds: Record<string, string[]>;
7
- awaitingItemDataLoading: AwaitingLoaderCallbacks;
8
- awaitingItemChildrensLoading: AwaitingLoaderCallbacks;
9
6
  }
10
7
  /**
11
8
  * @category Async Data Loader/General
@@ -27,16 +24,24 @@ export type AsyncDataLoaderFeatureDef<T> = {
27
24
  onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
28
25
  };
29
26
  treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
27
+ /** @deprecated use loadItemData instead */
30
28
  waitForItemDataLoaded: (itemId: string) => Promise<void>;
29
+ /** @deprecated use loadChildrenIds instead */
31
30
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
31
+ loadItemData: (itemId: string) => Promise<T>;
32
+ loadChildrenIds: (itemId: string) => Promise<string[]>;
32
33
  };
33
34
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
34
- /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
35
- invalidateItemData: () => void;
36
- invalidateChildrenIds: () => void;
35
+ /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
36
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemData`, and
37
+ * the tree will continue to display the old data until the new data has loaded. */
38
+ invalidateItemData: (optimistic?: boolean) => Promise<void>;
39
+ /** Invalidate fetched children ids for item, and triggers a refetch and subsequent rerender if the item is visible
40
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
41
+ * the tree will continue to display the old data until the new data has loaded. */
42
+ invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
37
43
  updateCachedChildrenIds: (childrenIds: string[]) => void;
38
44
  isLoading: () => boolean;
39
45
  };
40
46
  hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
41
47
  };
42
- export {};
@@ -57,6 +57,7 @@ export const dragAndDropFeature = {
57
57
  const dragLine = tree.getDragLineData();
58
58
  return dragLine
59
59
  ? {
60
+ position: "absolute",
60
61
  top: `${dragLine.top + topOffset}px`,
61
62
  left: `${dragLine.left + leftOffset}px`,
62
63
  width: `${dragLine.width - leftOffset}px`,
@@ -48,7 +48,7 @@ export const expandAllFeature = {
48
48
  handler: (_, tree) => __awaiter(void 0, void 0, void 0, function* () {
49
49
  const cancelToken = { current: false };
50
50
  const cancelHandler = (e) => {
51
- if (e.key === "Escape") {
51
+ if (e.code === "Escape") {
52
52
  cancelToken.current = true;
53
53
  }
54
54
  };
@@ -58,7 +58,7 @@ export const expandAllFeature = {
58
58
  }),
59
59
  },
60
60
  collapseSelected: {
61
- hotkey: "Control+Shift+-",
61
+ hotkey: "Control+Shift+Minus",
62
62
  handler: (_, tree) => {
63
63
  tree.getSelectedItems().forEach((item) => item.collapseAll());
64
64
  },
@@ -1,14 +1,29 @@
1
1
  const specialKeys = {
2
- Letter: /^[a-z]$/,
3
- LetterOrNumber: /^[a-z0-9]$/,
4
- Plus: /^\+$/,
5
- Space: /^ $/,
2
+ // TODO:breaking deprecate auto-lowercase
3
+ letter: /^Key[A-Z]$/,
4
+ letterornumber: /^(Key[A-Z]|Digit[0-9])$/,
5
+ plus: /^(NumpadAdd|Plus)$/,
6
+ minus: /^(NumpadSubtract|Minus)$/,
7
+ control: /^(ControlLeft|ControlRight)$/,
8
+ shift: /^(ShiftLeft|ShiftRight)$/,
6
9
  };
7
10
  const testHotkeyMatch = (pressedKeys, tree, hotkey) => {
8
- const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
9
- const doKeysMatch = supposedKeys.every((key) => key in specialKeys
10
- ? [...pressedKeys].some((pressedKey) => specialKeys[key].test(pressedKey))
11
- : pressedKeys.has(key));
11
+ const supposedKeys = hotkey.hotkey.toLowerCase().split("+"); // TODO:breaking deprecate auto-lowercase
12
+ const doKeysMatch = supposedKeys.every((key) => {
13
+ if (key in specialKeys) {
14
+ return [...pressedKeys].some((pressedKey) => specialKeys[key].test(pressedKey));
15
+ }
16
+ const pressedKeysLowerCase = [...pressedKeys] // TODO:breaking deprecate auto-lowercase
17
+ .map((k) => k.toLowerCase());
18
+ if (pressedKeysLowerCase.includes(key.toLowerCase())) {
19
+ return true;
20
+ }
21
+ if (pressedKeysLowerCase.includes(`key${key.toLowerCase()}`)) {
22
+ // TODO:breaking deprecate e.key character matching
23
+ return true;
24
+ }
25
+ return false;
26
+ });
12
27
  const isEnabled = !hotkey.isEnabled || hotkey.isEnabled(tree);
13
28
  const equalCounts = pressedKeys.size === supposedKeys.length;
14
29
  return doKeysMatch && isEnabled && equalCounts;
@@ -22,16 +37,24 @@ export const hotkeysCoreFeature = {
22
37
  onTreeMount: (tree, element) => {
23
38
  const data = tree.getDataRef();
24
39
  const keydown = (e) => {
25
- var _a, _b, _c, _d;
26
- var _e;
27
- const key = e.key.toLowerCase();
28
- (_a = (_e = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_e.pressedKeys = new Set());
29
- const newMatch = !data.current.pressedKeys.has(key);
30
- data.current.pressedKeys.add(key);
31
- const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), tree.getConfig().hotkeys);
40
+ var _a;
41
+ var _b;
42
+ const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
43
+ if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
44
+ return;
45
+ }
46
+ (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
47
+ const newMatch = !data.current.pressedKeys.has(e.code);
48
+ data.current.pressedKeys.add(e.code);
49
+ const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), hotkeys);
50
+ if (e.target instanceof HTMLInputElement) {
51
+ // JS respects composite keydowns while input elements are focused, and
52
+ // doesnt send the associated keyup events with the same key name
53
+ data.current.pressedKeys.delete(e.code);
54
+ }
32
55
  if (!hotkeyName)
33
56
  return;
34
- const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), (_b = tree.getConfig().hotkeys) === null || _b === void 0 ? void 0 : _b[hotkeyName]);
57
+ const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys === null || hotkeys === void 0 ? void 0 : hotkeys[hotkeyName]);
35
58
  if (!hotkeyConfig)
36
59
  return;
37
60
  if (!hotkeyConfig.allowWhenInputFocused &&
@@ -42,21 +65,26 @@ export const hotkeysCoreFeature = {
42
65
  if (hotkeyConfig.preventDefault)
43
66
  e.preventDefault();
44
67
  hotkeyConfig.handler(e, tree);
45
- (_d = (_c = tree.getConfig()).onTreeHotkey) === null || _d === void 0 ? void 0 : _d.call(_c, hotkeyName, e);
68
+ onTreeHotkey === null || onTreeHotkey === void 0 ? void 0 : onTreeHotkey(hotkeyName, e);
46
69
  };
47
70
  const keyup = (e) => {
48
71
  var _a;
49
72
  var _b;
50
73
  (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
51
- data.current.pressedKeys.delete(e.key.toLowerCase());
74
+ data.current.pressedKeys.delete(e.code);
75
+ };
76
+ const reset = () => {
77
+ data.current.pressedKeys = new Set();
52
78
  };
53
79
  // keyup is registered on document, because some hotkeys shift
54
80
  // the focus away from the tree (i.e. search)
55
81
  // and then we wouldn't get the keyup event anymore
56
82
  element.addEventListener("keydown", keydown);
57
83
  document.addEventListener("keyup", keyup);
84
+ window.addEventListener("focus", reset);
58
85
  data.current.keydownHandler = keydown;
59
86
  data.current.keyupHandler = keyup;
87
+ data.current.resetHandler = reset;
60
88
  },
61
89
  onTreeUnmount: (tree, element) => {
62
90
  const data = tree.getDataRef();
@@ -68,5 +96,9 @@ export const hotkeysCoreFeature = {
68
96
  element.removeEventListener("keydown", data.current.keydownHandler);
69
97
  delete data.current.keydownHandler;
70
98
  }
99
+ if (data.current.resetHandler) {
100
+ window.removeEventListener("focus", data.current.resetHandler);
101
+ delete data.current.resetHandler;
102
+ }
71
103
  },
72
104
  };
@@ -10,6 +10,7 @@ export interface HotkeyConfig<T> {
10
10
  export interface HotkeysCoreDataRef {
11
11
  keydownHandler?: (e: KeyboardEvent) => void;
12
12
  keyupHandler?: (e: KeyboardEvent) => void;
13
+ resetHandler?: (e: FocusEvent) => void;
13
14
  pressedKeys: Set<string>;
14
15
  }
15
16
  export type HotkeysCoreFeatureDef<T> = {
@@ -17,6 +18,8 @@ export type HotkeysCoreFeatureDef<T> = {
17
18
  config: {
18
19
  hotkeys?: CustomHotkeysConfig<T>;
19
20
  onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
21
+ /** Do not handle key inputs while an HTML input element is focused */
22
+ ignoreHotkeysOnInputs?: boolean;
20
23
  };
21
24
  treeInstance: {};
22
25
  itemInstance: {};
@@ -141,7 +141,7 @@ export const keyboardDragAndDropFeature = {
141
141
  },
142
142
  hotkeys: {
143
143
  startDrag: {
144
- hotkey: "Control+Shift+D",
144
+ hotkey: "Control+Shift+KeyD",
145
145
  preventDefault: true,
146
146
  isEnabled: (tree) => !tree.getState().dnd,
147
147
  handler: (_, tree) => {
@@ -1,6 +1,7 @@
1
1
  import { makeStateUpdater } from "../../utils";
2
2
  export const renamingFeature = {
3
3
  key: "renaming",
4
+ overwrites: ["drag-and-drop"],
4
5
  getDefaultConfig: (defaultConfig, tree) => (Object.assign({ setRenamingItem: makeStateUpdater("renamingItem", tree), setRenamingValue: makeStateUpdater("renamingValue", tree), canRename: () => true }, defaultConfig)),
5
6
  stateHandlerNames: {
6
7
  renamingItem: "setRenamingItem",
@@ -47,6 +48,13 @@ export const renamingFeature = {
47
48
  }),
48
49
  canRename: ({ tree, item }) => { var _a, _b, _c; return (_c = (_b = (_a = tree.getConfig()).canRename) === null || _b === void 0 ? void 0 : _b.call(_a, item)) !== null && _c !== void 0 ? _c : true; },
49
50
  isRenaming: ({ tree, item }) => item.getId() === tree.getState().renamingItem,
51
+ getProps: ({ prev, item }) => {
52
+ var _a;
53
+ const isRenaming = item.isRenaming();
54
+ const prevProps = (_a = prev === null || prev === void 0 ? void 0 : prev()) !== null && _a !== void 0 ? _a : {};
55
+ return isRenaming
56
+ ? Object.assign(Object.assign({}, prevProps), { draggable: false, onDragStart: () => { } }) : prevProps;
57
+ },
50
58
  },
51
59
  hotkeys: {
52
60
  renameItem: {
@@ -119,7 +119,7 @@ export const selectionFeature = {
119
119
  },
120
120
  },
121
121
  selectAll: {
122
- hotkey: "Control+a",
122
+ hotkey: "Control+KeyA",
123
123
  preventDefault: true,
124
124
  handler: (e, tree) => {
125
125
  tree.setSelectedItems(tree.getItems().map((item) => item.getId()));
@@ -10,6 +10,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { makeStateUpdater } from "../../utils";
11
11
  import { throwError } from "../../utilities/errors";
12
12
  const promiseErrorMessage = "sync dataLoader returned promise";
13
+ const unpromise = (data) => {
14
+ if (!data || (typeof data === "object" && "then" in data)) {
15
+ throw throwError(promiseErrorMessage);
16
+ }
17
+ return data;
18
+ };
13
19
  export const syncDataLoaderFeature = {
14
20
  key: "sync-data-loader",
15
21
  getInitialState: (initialState) => (Object.assign({ loadingItemData: [], loadingItemChildrens: [] }, initialState)),
@@ -22,19 +28,17 @@ export const syncDataLoaderFeature = {
22
28
  waitForItemDataLoaded: () => __awaiter(void 0, void 0, void 0, function* () { }),
23
29
  waitForItemChildrenLoaded: () => __awaiter(void 0, void 0, void 0, function* () { }),
24
30
  retrieveItemData: ({ tree }, itemId) => {
25
- const data = tree.getConfig().dataLoader.getItem(itemId);
26
- if (typeof data === "object" && "then" in data) {
27
- throw throwError(promiseErrorMessage);
28
- }
29
- return data;
31
+ return unpromise(tree.getConfig().dataLoader.getItem(itemId));
30
32
  },
31
33
  retrieveChildrenIds: ({ tree }, itemId) => {
32
- const data = tree.getConfig().dataLoader.getChildren(itemId);
33
- if (typeof data === "object" && "then" in data) {
34
- throw throwError(promiseErrorMessage);
34
+ const { dataLoader } = tree.getConfig();
35
+ if ("getChildren" in dataLoader) {
36
+ return unpromise(dataLoader.getChildren(itemId));
35
37
  }
36
- return data;
38
+ return unpromise(dataLoader.getChildrenWithData(itemId)).map((c) => c.data);
37
39
  },
40
+ loadItemData: ({ tree }, itemId) => tree.retrieveItemData(itemId),
41
+ loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId),
38
42
  },
39
43
  itemInstance: {
40
44
  isLoading: () => false,
@@ -1,7 +1,16 @@
1
- export interface TreeDataLoader<T> {
1
+ export type TreeDataLoader<T> = {
2
2
  getItem: (itemId: string) => T | Promise<T>;
3
3
  getChildren: (itemId: string) => string[] | Promise<string[]>;
4
- }
4
+ } | {
5
+ getItem: (itemId: string) => T | Promise<T>;
6
+ getChildrenWithData: (itemId: string) => {
7
+ id: string;
8
+ data: T;
9
+ }[] | Promise<{
10
+ id: string;
11
+ data: T;
12
+ }[]>;
13
+ };
5
14
  export type SyncDataLoaderFeatureDef<T> = {
6
15
  state: {};
7
16
  config: {
@@ -97,6 +97,7 @@ export const treeFeature = {
97
97
  (_d = item.getElement()) === null || _d === void 0 ? void 0 : _d.scrollIntoView(scrollIntoViewArg);
98
98
  }),
99
99
  getId: ({ itemId }) => itemId,
100
+ getKey: ({ itemId }) => itemId, // TODO apply to all stories to use
100
101
  getProps: ({ item, prev }) => {
101
102
  const itemMeta = item.getItemMeta();
102
103
  return Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { ref: item.registerElement, role: "treeitem", "aria-setsize": itemMeta.setSize, "aria-posinset": itemMeta.posInSet, "aria-selected": "false", "aria-label": item.getItemName(), "aria-level": itemMeta.level, tabIndex: item.isFocused() ? 0 : -1, onClick: (e) => {
@@ -37,6 +37,7 @@ export type TreeFeatureDef<T> = {
37
37
  };
38
38
  itemInstance: {
39
39
  getId: () => string;
40
+ getKey: () => string;
40
41
  getProps: () => Record<string, any>;
41
42
  getItemName: () => string;
42
43
  getItemData: () => T;
@@ -27,3 +27,5 @@ export * from "./utilities/insert-items-at-target";
27
27
  export * from "./utilities/remove-items-from-parents";
28
28
  export * from "./core/build-proxified-instance";
29
29
  export * from "./core/build-static-instance";
30
+ export { makeStateUpdater } from "./utils";
31
+ export { isOrderedDragTarget } from "./features/drag-and-drop/utils";
package/lib/esm/index.js CHANGED
@@ -26,3 +26,5 @@ export * from "./utilities/insert-items-at-target";
26
26
  export * from "./utilities/remove-items-from-parents";
27
27
  export * from "./core/build-proxified-instance";
28
28
  export * from "./core/build-static-instance";
29
+ export { makeStateUpdater } from "./utils";
30
+ export { isOrderedDragTarget } from "./features/drag-and-drop/utils";
@@ -52,6 +52,7 @@ export class TestTreeExpect {
52
52
  top: 0,
53
53
  });
54
54
  expect(this.tree.instance.getDragLineStyle(0, 0)).toEqual({
55
+ position: "absolute",
55
56
  left: `${indent * 20}px`,
56
57
  pointerEvents: "none",
57
58
  top: "0px",
@@ -31,6 +31,7 @@ export class TestTree {
31
31
  get instance() {
32
32
  if (!this.treeInstance) {
33
33
  this.treeInstance = createTree(this.config);
34
+ this.treeInstance.rebuildTree();
34
35
  }
35
36
  return this.treeInstance;
36
37
  }
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "1.0.1",
4
- "type": "module",
3
+ "version": "1.2.0",
5
4
  "main": "lib/cjs/index.js",
6
5
  "module": "lib/esm/index.js",
7
6
  "types": "lib/esm/index.d.ts",
7
+ "exports": {
8
+ "types": "./lib/esm/index.d.ts",
9
+ "import": "./lib/esm/index.js",
10
+ "default": "./lib/cjs/index.js"
11
+ },
8
12
  "sideEffects": false,
9
13
  "scripts": {
10
14
  "build:cjs": "tsc -m commonjs --outDir lib/cjs",
@@ -18,6 +22,7 @@
18
22
  "directory": "packages/core"
19
23
  },
20
24
  "author": "Lukas Bach <npm@lukasbach.com>",
25
+ "funding": "https://github.com/sponsors/lukasbach",
21
26
  "license": "MIT",
22
27
  "devDependencies": {
23
28
  "jsdom": "^26.0.0",
package/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- ![Headless Tree](./packages/docs/static/img/banner-github.png)
1
+ ![Headless Tree](https://github.com/lukasbach/headless-tree/raw/main/packages/docs/static/img/banner-github.png)
2
2
 
3
3
  [![Documentation](https://img.shields.io/badge/docs-1e1f22?style=flat)](https://headless-tree.lukasbach.com/)
4
4
  [![Chat on Discord](https://img.shields.io/badge/discord-4c57d9?style=flat&logo=discord&logoColor=ffffff)](https://discord.gg/KuZ6EezzVw)
@@ -139,7 +139,7 @@ Then, render your tree based on the tree instance returned from the hook:
139
139
  style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
140
140
  >
141
141
  <div
142
- className={cx("treeitem", {
142
+ className={cn("treeitem", {
143
143
  focused: item.isFocused(),
144
144
  expanded: item.isExpanded(),
145
145
  selected: item.isSelected(),