@headless-tree/core 0.0.4 → 0.0.6

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 (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/cjs/core/create-tree.js +11 -10
  3. package/lib/cjs/features/async-data-loader/feature.js +30 -21
  4. package/lib/cjs/features/drag-and-drop/feature.js +34 -22
  5. package/lib/cjs/features/drag-and-drop/types.d.ts +14 -1
  6. package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -1
  7. package/lib/cjs/features/drag-and-drop/utils.js +34 -17
  8. package/lib/cjs/features/expand-all/feature.js +12 -9
  9. package/lib/cjs/features/expand-all/types.d.ts +2 -2
  10. package/lib/cjs/features/main/types.d.ts +4 -1
  11. package/lib/cjs/features/renaming/feature.js +10 -9
  12. package/lib/cjs/features/search/feature.js +21 -7
  13. package/lib/cjs/features/search/types.d.ts +1 -1
  14. package/lib/cjs/features/selection/feature.js +6 -4
  15. package/lib/cjs/features/sync-data-loader/feature.js +6 -0
  16. package/lib/cjs/features/sync-data-loader/types.d.ts +1 -1
  17. package/lib/cjs/features/tree/feature.js +41 -24
  18. package/lib/cjs/features/tree/types.d.ts +7 -2
  19. package/lib/cjs/index.d.ts +3 -1
  20. package/lib/cjs/index.js +3 -1
  21. package/lib/cjs/types/core.d.ts +1 -3
  22. package/lib/cjs/utilities/create-on-drop-handler.d.ts +3 -0
  23. package/lib/cjs/utilities/create-on-drop-handler.js +11 -0
  24. package/lib/cjs/utilities/insert-items-at-target.d.ts +3 -0
  25. package/lib/cjs/utilities/insert-items-at-target.js +24 -0
  26. package/lib/cjs/utilities/remove-items-from-parents.d.ts +2 -0
  27. package/lib/cjs/utilities/remove-items-from-parents.js +17 -0
  28. package/lib/cjs/utils.d.ts +1 -4
  29. package/lib/cjs/utils.js +1 -53
  30. package/lib/esm/core/create-tree.js +11 -10
  31. package/lib/esm/features/async-data-loader/feature.js +30 -21
  32. package/lib/esm/features/drag-and-drop/feature.js +34 -22
  33. package/lib/esm/features/drag-and-drop/types.d.ts +14 -1
  34. package/lib/esm/features/drag-and-drop/utils.d.ts +1 -1
  35. package/lib/esm/features/drag-and-drop/utils.js +35 -18
  36. package/lib/esm/features/expand-all/feature.js +12 -9
  37. package/lib/esm/features/expand-all/types.d.ts +2 -2
  38. package/lib/esm/features/main/types.d.ts +4 -1
  39. package/lib/esm/features/renaming/feature.js +10 -9
  40. package/lib/esm/features/search/feature.js +21 -7
  41. package/lib/esm/features/search/types.d.ts +1 -1
  42. package/lib/esm/features/selection/feature.js +6 -4
  43. package/lib/esm/features/sync-data-loader/feature.js +6 -0
  44. package/lib/esm/features/sync-data-loader/types.d.ts +1 -1
  45. package/lib/esm/features/tree/feature.js +41 -24
  46. package/lib/esm/features/tree/types.d.ts +7 -2
  47. package/lib/esm/index.d.ts +3 -1
  48. package/lib/esm/index.js +3 -1
  49. package/lib/esm/types/core.d.ts +1 -3
  50. package/lib/esm/utilities/create-on-drop-handler.d.ts +3 -0
  51. package/lib/esm/utilities/create-on-drop-handler.js +7 -0
  52. package/lib/esm/utilities/insert-items-at-target.d.ts +3 -0
  53. package/lib/esm/utilities/insert-items-at-target.js +20 -0
  54. package/lib/esm/utilities/remove-items-from-parents.d.ts +2 -0
  55. package/lib/esm/utilities/remove-items-from-parents.js +13 -0
  56. package/lib/esm/utils.d.ts +1 -4
  57. package/lib/esm/utils.js +0 -50
  58. package/package.json +3 -3
  59. package/src/core/create-tree.ts +12 -8
  60. package/src/features/async-data-loader/feature.ts +15 -5
  61. package/src/features/drag-and-drop/feature.ts +42 -20
  62. package/src/features/drag-and-drop/types.ts +23 -6
  63. package/src/features/drag-and-drop/utils.ts +53 -24
  64. package/src/features/expand-all/feature.ts +10 -8
  65. package/src/features/expand-all/types.ts +2 -2
  66. package/src/features/main/types.ts +7 -0
  67. package/src/features/renaming/feature.ts +10 -5
  68. package/src/features/search/feature.ts +22 -5
  69. package/src/features/search/types.ts +1 -0
  70. package/src/features/selection/feature.ts +8 -3
  71. package/src/features/sync-data-loader/feature.ts +17 -2
  72. package/src/features/sync-data-loader/types.ts +1 -1
  73. package/src/features/tree/feature.ts +43 -23
  74. package/src/features/tree/types.ts +10 -2
  75. package/src/index.ts +4 -1
  76. package/src/types/core.ts +4 -4
  77. package/src/utilities/create-on-drop-handler.ts +14 -0
  78. package/src/utilities/insert-items-at-target.ts +30 -0
  79. package/src/utilities/remove-items-from-parents.ts +21 -0
  80. package/src/utils.ts +1 -69
  81. package/tsconfig.json +1 -1
  82. package/lib/cjs/data-adapters/nested-data-adapter.d.ts +0 -9
  83. package/lib/cjs/data-adapters/nested-data-adapter.js +0 -32
  84. package/lib/cjs/data-adapters/types.d.ts +0 -7
  85. package/lib/cjs/data-adapters/types.js +0 -2
  86. package/lib/esm/data-adapters/nested-data-adapter.d.ts +0 -9
  87. package/lib/esm/data-adapters/nested-data-adapter.js +0 -28
  88. package/lib/esm/data-adapters/types.d.ts +0 -7
  89. package/lib/esm/data-adapters/types.js +0 -1
  90. package/src/data-adapters/nested-data-adapter.ts +0 -48
  91. package/src/data-adapters/types.ts +0 -9
@@ -5,6 +5,7 @@ import {
5
5
  SetStateFn,
6
6
  TreeConfig,
7
7
  TreeState,
8
+ Updater,
8
9
  } from "../../types/core";
9
10
  import { ItemMeta } from "../tree/types";
10
11
 
@@ -12,10 +13,16 @@ export type MainFeatureDef<T = any> = {
12
13
  state: {};
13
14
  config: {
14
15
  features?: FeatureImplementation<any>[];
16
+ initialState?: Partial<TreeState<T>>;
15
17
  state?: Partial<TreeState<T>>;
16
18
  setState?: SetStateFn<TreeState<T>>;
17
19
  };
18
20
  treeInstance: {
21
+ /** @internal */
22
+ applySubStateUpdate: <K extends keyof TreeState<any>>(
23
+ stateName: K,
24
+ updater: Updater<TreeState<T>[K]>
25
+ ) => void;
19
26
  setState: SetStateFn<TreeState<T>>;
20
27
  getState: () => TreeState<T>;
21
28
  setConfig: SetStateFn<TreeConfig<T>>;
@@ -19,6 +19,11 @@ export const renamingFeature: FeatureImplementation<
19
19
  ...defaultConfig,
20
20
  }),
21
21
 
22
+ stateHandlerNames: {
23
+ renamingItem: "setRenamingItem",
24
+ renamingValue: "setRenamingValue",
25
+ },
26
+
22
27
  createTreeInstance: (prev, instance) => ({
23
28
  ...prev,
24
29
 
@@ -30,8 +35,8 @@ export const renamingFeature: FeatureImplementation<
30
35
  return;
31
36
  }
32
37
 
33
- config.setRenamingItem?.(itemId);
34
- config.setRenamingValue?.(item.getItemName());
38
+ instance.applySubStateUpdate("renamingItem", itemId);
39
+ instance.applySubStateUpdate("renamingValue", item.getItemName());
35
40
  },
36
41
 
37
42
  getRenamingItem: () => {
@@ -42,7 +47,7 @@ export const renamingFeature: FeatureImplementation<
42
47
  getRenamingValue: () => instance.getState().renamingValue || "",
43
48
 
44
49
  abortRenaming: () => {
45
- instance.getConfig().setRenamingItem?.(null);
50
+ instance.applySubStateUpdate("renamingItem", null);
46
51
  },
47
52
 
48
53
  completeRenaming: () => {
@@ -51,7 +56,7 @@ export const renamingFeature: FeatureImplementation<
51
56
  if (item) {
52
57
  config.onRename?.(item, instance.getState().renamingValue || "");
53
58
  }
54
- instance.getConfig().setRenamingItem?.(null);
59
+ instance.applySubStateUpdate("renamingItem", null);
55
60
  },
56
61
 
57
62
  isRenamingItem: () => !!instance.getState().renamingItem,
@@ -63,7 +68,7 @@ export const renamingFeature: FeatureImplementation<
63
68
  onBlur: () => tree.abortRenaming(),
64
69
  value: tree.getRenamingValue(),
65
70
  onChange: (e) => {
66
- tree.getConfig().setRenamingValue?.(e.target.value);
71
+ tree.applySubStateUpdate("renamingValue", e.target.value);
67
72
  },
68
73
  }),
69
74
 
@@ -25,11 +25,15 @@ export const searchFeature: FeatureImplementation<
25
25
  ...defaultConfig,
26
26
  }),
27
27
 
28
+ stateHandlerNames: {
29
+ search: "setSearch",
30
+ },
31
+
28
32
  createTreeInstance: (prev, instance) => ({
29
33
  ...prev,
30
34
 
31
35
  setSearch: (search) => {
32
- instance.getConfig().setSearch?.(search);
36
+ instance.applySubStateUpdate("search", search);
33
37
  instance
34
38
  .getItems()
35
39
  .find((item) =>
@@ -71,8 +75,9 @@ export const searchFeature: FeatureImplementation<
71
75
 
72
76
  getSearchMatchingItems: memo(
73
77
  (search, items) =>
74
- items.filter((item) =>
75
- instance.getConfig().isSearchMatchingItem?.(search, item)
78
+ items.filter(
79
+ (item) =>
80
+ search && instance.getConfig().isSearchMatchingItem?.(search, item)
76
81
  ),
77
82
  () => [instance.getSearchValue(), instance.getItems()]
78
83
  ),
@@ -105,12 +110,22 @@ export const searchFeature: FeatureImplementation<
105
110
  },
106
111
  },
107
112
 
113
+ submitSearch: {
114
+ hotkey: "Enter",
115
+ allowWhenInputFocused: true,
116
+ isEnabled: (tree) => tree.isSearchOpen(),
117
+ handler: (e, tree) => {
118
+ tree.closeSearch();
119
+ tree.setSelectedItems([tree.getFocusedItem().getId()]);
120
+ },
121
+ },
122
+
108
123
  nextSearchItem: {
109
124
  hotkey: "ArrowDown",
110
125
  allowWhenInputFocused: true,
126
+ canRepeat: true,
111
127
  isEnabled: (tree) => tree.isSearchOpen(),
112
128
  handler: (e, tree) => {
113
- // TODO scroll into view
114
129
  const focusItem = tree
115
130
  .getSearchMatchingItems()
116
131
  .find(
@@ -119,15 +134,16 @@ export const searchFeature: FeatureImplementation<
119
134
  tree.getFocusedItem().getItemMeta().index
120
135
  );
121
136
  focusItem?.setFocused();
137
+ focusItem?.scrollTo({ block: "nearest", inline: "nearest" });
122
138
  },
123
139
  },
124
140
 
125
141
  previousSearchItem: {
126
142
  hotkey: "ArrowUp",
127
143
  allowWhenInputFocused: true,
144
+ canRepeat: true,
128
145
  isEnabled: (tree) => tree.isSearchOpen(),
129
146
  handler: (e, tree) => {
130
- // TODO scroll into view
131
147
  const focusItem = [...tree.getSearchMatchingItems()]
132
148
  .reverse()
133
149
  .find(
@@ -136,6 +152,7 @@ export const searchFeature: FeatureImplementation<
136
152
  tree.getFocusedItem().getItemMeta().index
137
153
  );
138
154
  focusItem?.setFocused();
155
+ focusItem?.scrollTo({ block: "nearest", inline: "nearest" });
139
156
  },
140
157
  },
141
158
  },
@@ -34,6 +34,7 @@ export type SearchFeatureDef<T> = {
34
34
  hotkeys:
35
35
  | "openSearch"
36
36
  | "closeSearch"
37
+ | "submitSearch"
37
38
  | "nextSearchItem"
38
39
  | "previousSearchItem";
39
40
  };
@@ -22,11 +22,15 @@ export const selectionFeature: FeatureImplementation<
22
22
  ...defaultConfig,
23
23
  }),
24
24
 
25
+ stateHandlerNames: {
26
+ selectedItems: "setSelectedItems",
27
+ },
28
+
25
29
  createTreeInstance: (prev, instance) => ({
26
30
  ...prev,
27
31
 
28
32
  setSelectedItems: (selectedItems) => {
29
- instance.getConfig().setSelectedItems?.(selectedItems);
33
+ instance.applySubStateUpdate("selectedItems", selectedItems);
30
34
  },
31
35
 
32
36
  // TODO memo
@@ -91,7 +95,8 @@ export const selectionFeature: FeatureImplementation<
91
95
 
92
96
  getProps: () => ({
93
97
  ...prev.getProps(),
94
- onClick: (e) => {
98
+ "aria-selected": item.isSelected() ? "true" : "false",
99
+ onClick: item.getMemoizedProp("selection/onClick", () => (e) => {
95
100
  if (e.shiftKey) {
96
101
  item.selectUpTo(e.ctrlKey || e.metaKey);
97
102
  } else if (e.ctrlKey || e.metaKey) {
@@ -101,7 +106,7 @@ export const selectionFeature: FeatureImplementation<
101
106
  }
102
107
 
103
108
  prev.getProps().onClick?.(e);
104
- },
109
+ }),
105
110
  }),
106
111
  }),
107
112
 
@@ -1,6 +1,7 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
2
  import { SyncDataLoaderFeatureDef } from "./types";
3
3
  import { MainFeatureDef } from "../main/types";
4
+ import { makeStateUpdater } from "../../utils";
4
5
 
5
6
  export const syncDataLoaderFeature: FeatureImplementation<
6
7
  any,
@@ -10,14 +11,28 @@ export const syncDataLoaderFeature: FeatureImplementation<
10
11
  key: "sync-data-loader",
11
12
  dependingFeatures: ["main"],
12
13
 
14
+ getInitialState: (initialState) => ({
15
+ loadingItems: [],
16
+ ...initialState,
17
+ }),
18
+
19
+ getDefaultConfig: (defaultConfig, tree) => ({
20
+ setLoadingItems: makeStateUpdater("loadingItems", tree),
21
+ ...defaultConfig,
22
+ }),
23
+
24
+ stateHandlerNames: {
25
+ loadingItems: "setLoadingItems",
26
+ },
27
+
13
28
  createTreeInstance: (prev, instance) => ({
14
29
  ...prev,
15
30
 
16
31
  retrieveItemData: (itemId) =>
17
- instance.getConfig().dataLoader.getItem(itemId),
32
+ instance.getConfig().dataLoader!.getItem(itemId),
18
33
 
19
34
  retrieveChildrenIds: (itemId) =>
20
- instance.getConfig().dataLoader.getChildren(itemId),
35
+ instance.getConfig().dataLoader!.getChildren(itemId),
21
36
  }),
22
37
 
23
38
  createItemInstance: (prev) => ({
@@ -7,7 +7,7 @@ export type SyncDataLoaderFeatureDef<T> = {
7
7
  state: {};
8
8
  config: {
9
9
  rootItemId: string;
10
- dataLoader: SyncTreeDataLoader<T>;
10
+ dataLoader?: SyncTreeDataLoader<T>;
11
11
  };
12
12
  treeInstance: {
13
13
  retrieveItemData: (itemId: string) => T;
@@ -1,5 +1,5 @@
1
1
  import { FeatureImplementation, ItemInstance } from "../../types/core";
2
- import { ItemMeta, TreeFeatureDef } from "./types";
2
+ import { ItemMeta, TreeFeatureDef, TreeItemDataRef } from "./types";
3
3
  import { makeStateUpdater, memo, poll } from "../../utils";
4
4
  import { MainFeatureDef } from "../main/types";
5
5
  import { HotkeysCoreFeatureDef } from "../hotkeys-core/types";
@@ -28,6 +28,11 @@ export const treeFeature: FeatureImplementation<
28
28
  ...defaultConfig,
29
29
  }),
30
30
 
31
+ stateHandlerNames: {
32
+ expandedItems: "setExpandedItems",
33
+ focusedItem: "setFocusedItem",
34
+ },
35
+
31
36
  createTreeInstance: (prev, instance) => ({
32
37
  ...prev,
33
38
 
@@ -45,7 +50,6 @@ export const treeFeature: FeatureImplementation<
45
50
  getItemsMeta: () => {
46
51
  const { rootItemId } = instance.getConfig();
47
52
  const { expandedItems } = instance.getState();
48
- // console.log("!", instance.getConfig());
49
53
  const flatItems: ItemMeta[] = [];
50
54
 
51
55
  const recursiveAdd = (
@@ -91,9 +95,10 @@ export const treeFeature: FeatureImplementation<
91
95
  return;
92
96
  }
93
97
 
94
- instance
95
- .getConfig()
96
- .setExpandedItems?.((expandedItems) => [...expandedItems, itemId]);
98
+ instance.applySubStateUpdate("expandedItems", (expandedItems) => [
99
+ ...expandedItems,
100
+ itemId,
101
+ ]);
97
102
  instance.rebuildTree();
98
103
  },
99
104
 
@@ -102,11 +107,9 @@ export const treeFeature: FeatureImplementation<
102
107
  return;
103
108
  }
104
109
 
105
- instance
106
- .getConfig()
107
- .setExpandedItems?.((expandedItems) =>
108
- expandedItems.filter((id) => id !== itemId)
109
- );
110
+ instance.applySubStateUpdate("expandedItems", (expandedItems) =>
111
+ expandedItems.filter((id) => id !== itemId)
112
+ );
110
113
  instance.rebuildTree();
111
114
  },
112
115
 
@@ -119,7 +122,7 @@ export const treeFeature: FeatureImplementation<
119
122
  },
120
123
 
121
124
  focusItem: (itemId) => {
122
- instance.getConfig().setFocusedItem?.(itemId);
125
+ instance.applySubStateUpdate("focusedItem", itemId);
123
126
  },
124
127
 
125
128
  focusNextItem: () => {
@@ -134,7 +137,7 @@ export const treeFeature: FeatureImplementation<
134
137
  instance.focusItem(instance.getItems()[nextIndex].getId());
135
138
  },
136
139
 
137
- updateDomFocus: (scrollIntoView) => {
140
+ updateDomFocus: () => {
138
141
  // Required because if the state is managed outside in react, the state only updated during next render
139
142
  setTimeout(async () => {
140
143
  const focusedItem = instance.getFocusedItem();
@@ -143,9 +146,6 @@ export const treeFeature: FeatureImplementation<
143
146
  const focusedElement = focusedItem.getElement();
144
147
  if (!focusedElement) return;
145
148
  focusedElement.focus();
146
- // if (scrollIntoView) {
147
- // focusedElement.scrollIntoView();
148
- // }
149
149
  });
150
150
  },
151
151
 
@@ -162,6 +162,11 @@ export const treeFeature: FeatureImplementation<
162
162
  isLoading: () => {
163
163
  throw new Error("No data-loader registered");
164
164
  },
165
+ scrollTo: async (scrollIntoViewArg) => {
166
+ tree.getConfig().scrollToItem?.(item as any);
167
+ await poll(() => item.getElement() !== null, 20);
168
+ item.getElement()!.scrollIntoView(scrollIntoViewArg);
169
+ },
165
170
  getId: () => item.getItemMeta().itemId,
166
171
  getProps: () => {
167
172
  const itemMeta = item.getItemMeta();
@@ -170,11 +175,11 @@ export const treeFeature: FeatureImplementation<
170
175
  role: "treeitem",
171
176
  "aria-setsize": itemMeta.setSize,
172
177
  "aria-posinset": itemMeta.posInSet,
173
- "aria-selected": false,
178
+ "aria-selected": "false",
174
179
  "aria-label": item.getItemName(),
175
180
  "aria-level": itemMeta.level,
176
181
  tabIndex: item.isFocused() ? 0 : -1,
177
- onClick: (e) => {
182
+ onClick: item.getMemoizedProp("tree/onClick", () => (e) => {
178
183
  item.setFocused();
179
184
  item.primaryAction();
180
185
 
@@ -191,7 +196,7 @@ export const treeFeature: FeatureImplementation<
191
196
  } else {
192
197
  item.expand();
193
198
  }
194
- },
199
+ }),
195
200
  };
196
201
  },
197
202
  expand: () => tree.expandItem(item.getItemMeta().itemId),
@@ -224,10 +229,8 @@ export const treeFeature: FeatureImplementation<
224
229
  },
225
230
  () => [item.getItemMeta()]
226
231
  ),
227
- getIndexInParent: () =>
228
- item.getItemMeta().index -
229
- (item.getParent()?.getItemMeta().index ?? 0) -
230
- 1,
232
+ // TODO remove
233
+ getIndexInParent: () => item.getItemMeta().posInSet,
231
234
  getChildren: () =>
232
235
  tree
233
236
  .retrieveChildrenIds(item.getItemMeta().itemId)
@@ -235,6 +238,23 @@ export const treeFeature: FeatureImplementation<
235
238
  getTree: () => tree as any,
236
239
  getItemAbove: () => tree.getItems()[item.getItemMeta().index - 1],
237
240
  getItemBelow: () => tree.getItems()[item.getItemMeta().index + 1],
241
+ getMemoizedProp: (name, create, deps) => {
242
+ const data = item.getDataRef<TreeItemDataRef>();
243
+ const memoizedValue = data.current.memoizedValues?.[name];
244
+ if (
245
+ memoizedValue &&
246
+ (!deps ||
247
+ data.current.memoizedDeps?.[name]?.every((d, i) => d === deps![i]))
248
+ ) {
249
+ return memoizedValue;
250
+ }
251
+ data.current.memoizedDeps ??= {};
252
+ data.current.memoizedValues ??= {};
253
+ const value = create();
254
+ data.current.memoizedDeps[name] = deps;
255
+ data.current.memoizedValues[name] = value;
256
+ return value;
257
+ },
238
258
  }),
239
259
 
240
260
  hotkeys: {
@@ -283,7 +303,7 @@ export const treeFeature: FeatureImplementation<
283
303
  item.getItemMeta().level !== 0
284
304
  ) {
285
305
  item.getParent()?.setFocused();
286
- tree.updateDomFocus(true);
306
+ tree.updateDomFocus();
287
307
  } else {
288
308
  item.collapse();
289
309
  }
@@ -9,6 +9,11 @@ export type ItemMeta = {
9
9
  posInSet: number;
10
10
  };
11
11
 
12
+ export type TreeItemDataRef = {
13
+ memoizedValues: Record<string, any>;
14
+ memoizedDeps: Record<string, any[] | undefined>;
15
+ };
16
+
12
17
  export type TreeFeatureDef<T> = {
13
18
  state: {
14
19
  expandedItems: string[];
@@ -36,8 +41,7 @@ export type TreeFeatureDef<T> = {
36
41
  getFocusedItem: () => ItemInstance<any>;
37
42
  focusNextItem: () => void;
38
43
  focusPreviousItem: () => void;
39
- scrollToItem: (item: ItemInstance<any>) => void;
40
- updateDomFocus: (scrollIntoView?: boolean) => void;
44
+ updateDomFocus: () => void;
41
45
 
42
46
  getContainerProps: () => Record<string, any>;
43
47
  };
@@ -59,6 +63,10 @@ export type TreeFeatureDef<T> = {
59
63
  getTree: () => TreeInstance<T>;
60
64
  getItemAbove: () => ItemInstance<T> | null;
61
65
  getItemBelow: () => ItemInstance<T> | null;
66
+ getMemoizedProp: <X>(name: string, create: () => X, deps?: any[]) => X;
67
+ scrollTo: (
68
+ scrollIntoViewArg?: boolean | ScrollIntoViewOptions
69
+ ) => Promise<void>;
62
70
  };
63
71
  hotkeys:
64
72
  | "focusNextItem"
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./types/core";
2
2
  export * from "./core/create-tree";
3
+
3
4
  export * from "./features/tree/types";
4
5
  export * from "./features/main/types";
5
6
  export * from "./features/drag-and-drop/types";
@@ -20,4 +21,6 @@ export * from "./features/search/feature";
20
21
  export * from "./features/renaming/feature";
21
22
  export * from "./features/expand-all/feature";
22
23
 
23
- export * from "./data-adapters/nested-data-adapter";
24
+ export * from "./utilities/create-on-drop-handler";
25
+ export * from "./utilities/insert-items-at-target";
26
+ export * from "./utilities/remove-items-from-parents";
package/src/types/core.ts CHANGED
@@ -130,6 +130,10 @@ export type FeatureImplementation<
130
130
  key?: string;
131
131
  dependingFeatures?: string[];
132
132
 
133
+ stateHandlerNames?: Partial<
134
+ Record<keyof MergedFeatures<F>["state"], keyof MergedFeatures<F>["config"]>
135
+ >;
136
+
133
137
  getInitialState?: (
134
138
  initialState: Partial<MergedFeatures<F>["state"]>,
135
139
  tree: MergedFeatures<F>["treeInstance"]
@@ -174,9 +178,5 @@ export type FeatureImplementation<
174
178
  tree: MergedFeatures<F>["treeInstance"]
175
179
  ) => void;
176
180
 
177
- setState?: (instance: MergedFeatures<F>["treeInstance"]) => void;
178
- onConfigChange?: (instance: MergedFeatures<F>["treeInstance"]) => void;
179
- onStateOrConfigChange?: (instance: MergedFeatures<F>["treeInstance"]) => void;
180
-
181
181
  hotkeys?: HotkeysConfig<T, D>;
182
182
  };
@@ -0,0 +1,14 @@
1
+ import { ItemInstance } from "../types/core";
2
+ import { DropTarget } from "../features/drag-and-drop/types";
3
+ import { removeItemsFromParents } from "./remove-items-from-parents";
4
+ import { insertItemsAtTarget } from "./insert-items-at-target";
5
+
6
+ export const createOnDropHandler =
7
+ <T>(
8
+ onChangeChildren: (item: ItemInstance<T>, newChildren: string[]) => void
9
+ ) =>
10
+ (items: ItemInstance<T>[], target: DropTarget<T>) => {
11
+ const itemIds = items.map((item) => item.getId());
12
+ removeItemsFromParents(items, onChangeChildren);
13
+ insertItemsAtTarget(itemIds, target, onChangeChildren);
14
+ };
@@ -0,0 +1,30 @@
1
+ import { ItemInstance } from "../types/core";
2
+ import { DropTarget } from "../features/drag-and-drop/types";
3
+
4
+ export const insertItemsAtTarget = <T>(
5
+ itemIds: string[],
6
+ target: DropTarget<T>,
7
+ onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => void
8
+ ) => {
9
+ // add moved items to new common parent, if dropped onto parent
10
+ if (target.childIndex === null) {
11
+ onChangeChildren(target.item, [
12
+ ...target.item.getChildren().map((item) => item.getId()),
13
+ ...itemIds,
14
+ ]);
15
+ // TODO items[0].getTree().rebuildTree();
16
+ return;
17
+ }
18
+
19
+ // add moved items to new common parent, if dropped between siblings
20
+ const oldChildren = target.item.getChildren();
21
+ const newChildren = [
22
+ ...oldChildren.slice(0, target.insertionIndex).map((item) => item.getId()),
23
+ ...itemIds,
24
+ ...oldChildren.slice(target.insertionIndex).map((item) => item.getId()),
25
+ ];
26
+
27
+ onChangeChildren(target.item, newChildren);
28
+
29
+ target.item.getTree().rebuildTree();
30
+ };
@@ -0,0 +1,21 @@
1
+ import { ItemInstance } from "../types/core";
2
+
3
+ export const removeItemsFromParents = <T>(
4
+ movedItems: ItemInstance<T>[],
5
+ onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => void
6
+ ) => {
7
+ // TODO bulk sibling changes together
8
+ for (const item of movedItems) {
9
+ const siblings = item.getParent()?.getChildren();
10
+ if (siblings) {
11
+ onChangeChildren(
12
+ item.getParent(),
13
+ siblings
14
+ .filter((sibling) => sibling.getId() !== item.getId())
15
+ .map((i) => i.getId())
16
+ );
17
+ }
18
+ }
19
+
20
+ movedItems[0].getTree().rebuildTree();
21
+ };
package/src/utils.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { ItemInstance, TreeState, Updater } from "./types/core";
2
- import { DropTarget } from "./features/drag-and-drop/types";
1
+ import { TreeState, Updater } from "./types/core";
3
2
 
4
3
  export type NoInfer<T> = [T][T extends any ? 0 : never];
5
4
 
@@ -53,73 +52,6 @@ export function makeStateUpdater<K extends keyof TreeState<any>>(
53
52
  };
54
53
  }
55
54
 
56
- export const scrollIntoView = (element: Element | undefined | null) => {
57
- if (!element) {
58
- return;
59
- }
60
-
61
- if ((element as any).scrollIntoViewIfNeeded) {
62
- (element as any).scrollIntoViewIfNeeded();
63
- } else {
64
- const boundingBox = element.getBoundingClientRect();
65
- const isElementInViewport =
66
- boundingBox.top >= 0 &&
67
- boundingBox.left >= 0 &&
68
- boundingBox.bottom <=
69
- (window.innerHeight || document.documentElement.clientHeight) &&
70
- boundingBox.right <=
71
- (window.innerWidth || document.documentElement.clientWidth);
72
- if (!isElementInViewport) {
73
- element.scrollIntoView();
74
- }
75
- }
76
- };
77
-
78
- export const performItemsMove = <T>(
79
- items: ItemInstance<T>[],
80
- target: DropTarget<T>,
81
- onChangeChildren: (
82
- item: ItemInstance<T>,
83
- newChildren: ItemInstance<T>[]
84
- ) => void
85
- ) => {
86
- const numberOfDragItemsBeforeTarget = !target.childIndex
87
- ? 0
88
- : target.item
89
- .getChildren()
90
- .slice(0, target.childIndex)
91
- .filter((child) => items.some((item) => item.getId() === child.getId()))
92
- .length;
93
-
94
- // TODO bulk sibling changes together
95
- for (const item of items) {
96
- const siblings = item.getParent()?.getChildren();
97
- if (siblings) {
98
- onChangeChildren(
99
- item.getParent(),
100
- siblings.filter((sibling) => sibling.getId() !== item.getId())
101
- );
102
- }
103
- }
104
-
105
- if (target.childIndex === null) {
106
- onChangeChildren(target.item, [...target.item.getChildren(), ...items]);
107
- items[0].getTree().rebuildTree();
108
- return;
109
- }
110
-
111
- const oldChildren = target.item.getChildren();
112
- const newChildren = [
113
- ...oldChildren.slice(0, target.childIndex - numberOfDragItemsBeforeTarget),
114
- ...items,
115
- ...oldChildren.slice(target.childIndex - numberOfDragItemsBeforeTarget),
116
- ];
117
-
118
- onChangeChildren(target.item, newChildren);
119
-
120
- items[0].getTree().rebuildTree();
121
- };
122
-
123
55
  export const poll = (fn: () => boolean, interval = 100, timeout = 1000) =>
124
56
  new Promise<void>((resolve) => {
125
57
  let clear: ReturnType<typeof setTimeout>;
package/tsconfig.json CHANGED
@@ -2,6 +2,6 @@
2
2
  "extends": "../../tsconfig.json",
3
3
  "include": ["./src/**/*"],
4
4
  "compilerOptions": {
5
- "outDir": "lib/cjs"
5
+ "outDir": "lib/esm"
6
6
  }
7
7
  }
@@ -1,9 +0,0 @@
1
- import { DataAdapterConfig } from "./types";
2
- interface NestedDataAdapterProps<T> {
3
- rootItem: T;
4
- getItemId: (item: T) => string;
5
- getChildren: (item: T) => T[] | undefined;
6
- changeChildren?: (item: T, children: T[]) => void;
7
- }
8
- export declare const nestedDataAdapter: <T = any>(props: NestedDataAdapterProps<T>) => DataAdapterConfig<T>;
9
- export {};
@@ -1,32 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.nestedDataAdapter = void 0;
4
- const utils_1 = require("../utils");
5
- const createItemMap = (props, item, map = {}) => {
6
- var _a;
7
- map[props.getItemId(item)] = item;
8
- (_a = props.getChildren(item)) === null || _a === void 0 ? void 0 : _a.forEach((child) => {
9
- createItemMap(props, child, map);
10
- });
11
- return map;
12
- };
13
- const nestedDataAdapter = (props) => {
14
- const itemMap = createItemMap(props, props.rootItem);
15
- return {
16
- rootItemId: props.getItemId(props.rootItem),
17
- dataLoader: {
18
- getItem: (itemId) => itemMap[itemId],
19
- getChildren: (itemId) => { var _a, _b; return (_b = (_a = props.getChildren(itemMap[itemId])) === null || _a === void 0 ? void 0 : _a.map(props.getItemId)) !== null && _b !== void 0 ? _b : []; },
20
- },
21
- onDrop: (items, target) => {
22
- if (!props.changeChildren) {
23
- return;
24
- }
25
- (0, utils_1.performItemsMove)(items, target, (item, newChildren) => {
26
- var _a;
27
- (_a = props.changeChildren) === null || _a === void 0 ? void 0 : _a.call(props, item.getItemData(), newChildren.map((child) => child.getItemData()));
28
- });
29
- },
30
- };
31
- };
32
- exports.nestedDataAdapter = nestedDataAdapter;