@headless-tree/core 0.0.0-20230802230636

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 (149) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/lib/cjs/core/create-tree.d.ts +2 -0
  3. package/lib/cjs/core/create-tree.js +138 -0
  4. package/lib/cjs/features/async-data-loader/feature.d.ts +5 -0
  5. package/lib/cjs/features/async-data-loader/feature.js +88 -0
  6. package/lib/cjs/features/async-data-loader/types.d.ts +41 -0
  7. package/lib/cjs/features/async-data-loader/types.js +2 -0
  8. package/lib/cjs/features/drag-and-drop/feature.d.ts +3 -0
  9. package/lib/cjs/features/drag-and-drop/feature.js +138 -0
  10. package/lib/cjs/features/drag-and-drop/types.d.ts +69 -0
  11. package/lib/cjs/features/drag-and-drop/types.js +9 -0
  12. package/lib/cjs/features/drag-and-drop/utils.d.ts +6 -0
  13. package/lib/cjs/features/drag-and-drop/utils.js +79 -0
  14. package/lib/cjs/features/expand-all/feature.d.ts +6 -0
  15. package/lib/cjs/features/expand-all/feature.js +41 -0
  16. package/lib/cjs/features/expand-all/types.d.ts +17 -0
  17. package/lib/cjs/features/expand-all/types.js +2 -0
  18. package/lib/cjs/features/hotkeys-core/feature.d.ts +4 -0
  19. package/lib/cjs/features/hotkeys-core/feature.js +71 -0
  20. package/lib/cjs/features/hotkeys-core/types.d.ts +25 -0
  21. package/lib/cjs/features/hotkeys-core/types.js +2 -0
  22. package/lib/cjs/features/main/types.d.ts +39 -0
  23. package/lib/cjs/features/main/types.js +2 -0
  24. package/lib/cjs/features/renaming/feature.d.ts +5 -0
  25. package/lib/cjs/features/renaming/feature.js +64 -0
  26. package/lib/cjs/features/renaming/types.d.ts +27 -0
  27. package/lib/cjs/features/renaming/types.js +2 -0
  28. package/lib/cjs/features/search/feature.d.ts +5 -0
  29. package/lib/cjs/features/search/feature.js +103 -0
  30. package/lib/cjs/features/search/types.d.ts +33 -0
  31. package/lib/cjs/features/search/types.js +2 -0
  32. package/lib/cjs/features/selection/feature.d.ts +5 -0
  33. package/lib/cjs/features/selection/feature.js +113 -0
  34. package/lib/cjs/features/selection/types.d.ts +21 -0
  35. package/lib/cjs/features/selection/types.js +2 -0
  36. package/lib/cjs/features/sync-data-loader/feature.d.ts +4 -0
  37. package/lib/cjs/features/sync-data-loader/feature.js +14 -0
  38. package/lib/cjs/features/sync-data-loader/types.d.ts +19 -0
  39. package/lib/cjs/features/sync-data-loader/types.js +2 -0
  40. package/lib/cjs/features/tree/feature.d.ts +6 -0
  41. package/lib/cjs/features/tree/feature.js +230 -0
  42. package/lib/cjs/features/tree/types.d.ts +62 -0
  43. package/lib/cjs/features/tree/types.js +2 -0
  44. package/lib/cjs/index.d.ts +23 -0
  45. package/lib/cjs/index.js +39 -0
  46. package/lib/cjs/mddocs-entry.d.ts +21 -0
  47. package/lib/cjs/mddocs-entry.js +17 -0
  48. package/lib/cjs/types/core.d.ts +67 -0
  49. package/lib/cjs/types/core.js +2 -0
  50. package/lib/cjs/types/deep-merge.d.ts +13 -0
  51. package/lib/cjs/types/deep-merge.js +2 -0
  52. package/lib/cjs/utilities/create-on-drop-handler.d.ts +3 -0
  53. package/lib/cjs/utilities/create-on-drop-handler.js +11 -0
  54. package/lib/cjs/utilities/insert-items-at-target.d.ts +3 -0
  55. package/lib/cjs/utilities/insert-items-at-target.js +24 -0
  56. package/lib/cjs/utilities/remove-items-from-parents.d.ts +2 -0
  57. package/lib/cjs/utilities/remove-items-from-parents.js +17 -0
  58. package/lib/cjs/utils.d.ts +6 -0
  59. package/lib/cjs/utils.js +53 -0
  60. package/lib/esm/core/create-tree.d.ts +2 -0
  61. package/lib/esm/core/create-tree.js +134 -0
  62. package/lib/esm/features/async-data-loader/feature.d.ts +5 -0
  63. package/lib/esm/features/async-data-loader/feature.js +85 -0
  64. package/lib/esm/features/async-data-loader/types.d.ts +41 -0
  65. package/lib/esm/features/async-data-loader/types.js +1 -0
  66. package/lib/esm/features/drag-and-drop/feature.d.ts +3 -0
  67. package/lib/esm/features/drag-and-drop/feature.js +135 -0
  68. package/lib/esm/features/drag-and-drop/types.d.ts +69 -0
  69. package/lib/esm/features/drag-and-drop/types.js +6 -0
  70. package/lib/esm/features/drag-and-drop/utils.d.ts +6 -0
  71. package/lib/esm/features/drag-and-drop/utils.js +72 -0
  72. package/lib/esm/features/expand-all/feature.d.ts +6 -0
  73. package/lib/esm/features/expand-all/feature.js +38 -0
  74. package/lib/esm/features/expand-all/types.d.ts +17 -0
  75. package/lib/esm/features/expand-all/types.js +1 -0
  76. package/lib/esm/features/hotkeys-core/feature.d.ts +4 -0
  77. package/lib/esm/features/hotkeys-core/feature.js +68 -0
  78. package/lib/esm/features/hotkeys-core/types.d.ts +25 -0
  79. package/lib/esm/features/hotkeys-core/types.js +1 -0
  80. package/lib/esm/features/main/types.d.ts +39 -0
  81. package/lib/esm/features/main/types.js +1 -0
  82. package/lib/esm/features/renaming/feature.d.ts +5 -0
  83. package/lib/esm/features/renaming/feature.js +61 -0
  84. package/lib/esm/features/renaming/types.d.ts +27 -0
  85. package/lib/esm/features/renaming/types.js +1 -0
  86. package/lib/esm/features/search/feature.d.ts +5 -0
  87. package/lib/esm/features/search/feature.js +100 -0
  88. package/lib/esm/features/search/types.d.ts +33 -0
  89. package/lib/esm/features/search/types.js +1 -0
  90. package/lib/esm/features/selection/feature.d.ts +5 -0
  91. package/lib/esm/features/selection/feature.js +110 -0
  92. package/lib/esm/features/selection/types.d.ts +21 -0
  93. package/lib/esm/features/selection/types.js +1 -0
  94. package/lib/esm/features/sync-data-loader/feature.d.ts +4 -0
  95. package/lib/esm/features/sync-data-loader/feature.js +11 -0
  96. package/lib/esm/features/sync-data-loader/types.d.ts +19 -0
  97. package/lib/esm/features/sync-data-loader/types.js +1 -0
  98. package/lib/esm/features/tree/feature.d.ts +6 -0
  99. package/lib/esm/features/tree/feature.js +227 -0
  100. package/lib/esm/features/tree/types.d.ts +62 -0
  101. package/lib/esm/features/tree/types.js +1 -0
  102. package/lib/esm/index.d.ts +23 -0
  103. package/lib/esm/index.js +23 -0
  104. package/lib/esm/mddocs-entry.d.ts +21 -0
  105. package/lib/esm/mddocs-entry.js +1 -0
  106. package/lib/esm/types/core.d.ts +67 -0
  107. package/lib/esm/types/core.js +1 -0
  108. package/lib/esm/types/deep-merge.d.ts +13 -0
  109. package/lib/esm/types/deep-merge.js +1 -0
  110. package/lib/esm/utilities/create-on-drop-handler.d.ts +3 -0
  111. package/lib/esm/utilities/create-on-drop-handler.js +7 -0
  112. package/lib/esm/utilities/insert-items-at-target.d.ts +3 -0
  113. package/lib/esm/utilities/insert-items-at-target.js +20 -0
  114. package/lib/esm/utilities/remove-items-from-parents.d.ts +2 -0
  115. package/lib/esm/utilities/remove-items-from-parents.js +13 -0
  116. package/lib/esm/utils.d.ts +6 -0
  117. package/lib/esm/utils.js +46 -0
  118. package/package.json +23 -0
  119. package/src/core/create-tree.ts +228 -0
  120. package/src/features/async-data-loader/feature.ts +126 -0
  121. package/src/features/async-data-loader/types.ts +41 -0
  122. package/src/features/drag-and-drop/feature.ts +214 -0
  123. package/src/features/drag-and-drop/types.ts +89 -0
  124. package/src/features/drag-and-drop/utils.ts +117 -0
  125. package/src/features/expand-all/feature.ts +63 -0
  126. package/src/features/expand-all/types.ts +13 -0
  127. package/src/features/hotkeys-core/feature.ts +110 -0
  128. package/src/features/hotkeys-core/types.ts +36 -0
  129. package/src/features/main/types.ts +48 -0
  130. package/src/features/renaming/feature.ts +105 -0
  131. package/src/features/renaming/types.ts +28 -0
  132. package/src/features/search/feature.ts +158 -0
  133. package/src/features/search/types.ts +40 -0
  134. package/src/features/selection/feature.ts +157 -0
  135. package/src/features/selection/types.ts +28 -0
  136. package/src/features/sync-data-loader/feature.ts +41 -0
  137. package/src/features/sync-data-loader/types.ts +20 -0
  138. package/src/features/tree/feature.ts +326 -0
  139. package/src/features/tree/types.ts +78 -0
  140. package/src/index.ts +26 -0
  141. package/src/mddocs-entry.ts +26 -0
  142. package/src/types/core.ts +183 -0
  143. package/src/types/deep-merge.ts +31 -0
  144. package/src/utilities/create-on-drop-handler.ts +14 -0
  145. package/src/utilities/insert-items-at-target.ts +30 -0
  146. package/src/utilities/remove-items-from-parents.ts +21 -0
  147. package/src/utils.ts +68 -0
  148. package/tsconfig.json +7 -0
  149. package/typedoc.json +4 -0
@@ -0,0 +1,228 @@
1
+ import {
2
+ FeatureImplementation,
3
+ HotkeysConfig,
4
+ ItemInstance,
5
+ TreeConfig,
6
+ TreeInstance,
7
+ TreeState,
8
+ } from "../types/core";
9
+ import { MainFeatureDef } from "../features/main/types";
10
+ import { treeFeature } from "../features/tree/feature";
11
+ import { ItemMeta } from "../features/tree/types";
12
+
13
+ const buildItemInstance = (
14
+ features: FeatureImplementation[],
15
+ tree: TreeInstance<any>,
16
+ itemId: string
17
+ ) => {
18
+ const itemInstance = {} as ItemInstance<any>;
19
+ for (const feature of features) {
20
+ Object.assign(
21
+ itemInstance,
22
+ feature.createItemInstance?.(
23
+ { ...itemInstance },
24
+ itemInstance,
25
+ tree,
26
+ itemId
27
+ ) ?? {}
28
+ );
29
+ }
30
+ return itemInstance;
31
+ };
32
+
33
+ const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
34
+ const loadedFeatures = features?.map((feature) => feature.key);
35
+ for (const feature of features ?? []) {
36
+ const missingDependency = feature.deps?.find(
37
+ (dep) => !loadedFeatures?.includes(dep)
38
+ );
39
+ if (missingDependency) {
40
+ throw new Error(`${feature.key} needs ${missingDependency}`);
41
+ }
42
+ }
43
+ };
44
+
45
+ const compareFeatures = (
46
+ feature1: FeatureImplementation,
47
+ feature2: FeatureImplementation
48
+ ) => {
49
+ if (feature2.key && feature1.overwrites?.includes(feature2.key)) {
50
+ return 1;
51
+ }
52
+ return -1;
53
+ };
54
+
55
+ const sortFeatures = (features: FeatureImplementation[] = []) =>
56
+ features.sort(compareFeatures);
57
+
58
+ export const createTree = <T>(
59
+ initialConfig: TreeConfig<T>
60
+ ): TreeInstance<T> => {
61
+ const treeInstance: TreeInstance<T> = {} as any;
62
+
63
+ const additionalFeatures = [
64
+ treeFeature,
65
+ ...sortFeatures(initialConfig.features),
66
+ ];
67
+ verifyFeatures(additionalFeatures);
68
+
69
+ let state = additionalFeatures.reduce(
70
+ (acc, feature) => feature.getInitialState?.(acc, treeInstance) ?? acc,
71
+ initialConfig.initialState ?? initialConfig.state ?? {}
72
+ ) as TreeState<T>;
73
+ let config = additionalFeatures.reduce(
74
+ (acc, feature) => feature.getDefaultConfig?.(acc, treeInstance) ?? acc,
75
+ initialConfig
76
+ ) as TreeConfig<T>;
77
+ const stateHandlerNames = additionalFeatures.reduce(
78
+ (acc, feature) => ({ ...acc, ...feature.stateHandlerNames }),
79
+ {} as Record<string, string>
80
+ );
81
+
82
+ let treeElement: HTMLElement | undefined | null;
83
+ const treeDataRef: { current: any } = { current: {} };
84
+
85
+ const itemInstancesMap: Record<string, ItemInstance<T>> = {};
86
+ let itemInstances: ItemInstance<T>[] = [];
87
+ const itemElementsMap: Record<string, HTMLElement | undefined | null> = {};
88
+ const itemDataRefs: Record<string, { current: any }> = {};
89
+ let itemMetaMap: Record<string, ItemMeta> = {};
90
+
91
+ const hotkeyPresets = {} as HotkeysConfig<T>;
92
+
93
+ const rebuildItemMeta = (main: FeatureImplementation) => {
94
+ // TODO can we find a way to only run this for the changed substructure?
95
+ itemInstances = [];
96
+ itemMetaMap = {};
97
+
98
+ const rootInstance = buildItemInstance(
99
+ [main, ...additionalFeatures],
100
+ treeInstance,
101
+ config.rootItemId
102
+ );
103
+ itemInstancesMap[config.rootItemId] = rootInstance;
104
+ itemMetaMap[config.rootItemId] = {
105
+ itemId: config.rootItemId,
106
+ index: -1,
107
+ parentId: null!,
108
+ level: -1,
109
+ posInSet: 0,
110
+ setSize: 1,
111
+ };
112
+
113
+ for (const item of treeInstance.getItemsMeta()) {
114
+ itemMetaMap[item.itemId] = item;
115
+ if (!itemInstancesMap[item.itemId]) {
116
+ const instance = buildItemInstance(
117
+ [main, ...additionalFeatures],
118
+ treeInstance,
119
+ item.itemId
120
+ );
121
+ itemInstancesMap[item.itemId] = instance;
122
+ itemInstances.push(instance);
123
+ } else {
124
+ itemInstances.push(itemInstancesMap[item.itemId]);
125
+ }
126
+ }
127
+ console.log("REBUILT");
128
+ };
129
+
130
+ const eachFeature = (fn: (feature: FeatureImplementation<any>) => void) => {
131
+ for (const feature of additionalFeatures) {
132
+ fn(feature);
133
+ }
134
+ };
135
+
136
+ const mainFeature: FeatureImplementation<
137
+ T,
138
+ MainFeatureDef<T>,
139
+ MainFeatureDef<T>
140
+ > = {
141
+ key: "main",
142
+ createTreeInstance: (prev) => ({
143
+ ...prev,
144
+ getState: () => state,
145
+ setState: (updater) => {
146
+ // Not necessary, since I think the subupdate below keeps the state fresh anyways?
147
+ // state = typeof updater === "function" ? updater(state) : updater;
148
+ config.setState?.(state);
149
+ },
150
+ applySubStateUpdate: (stateName, updater) => {
151
+ state[stateName] =
152
+ typeof updater === "function" ? updater(state[stateName]) : updater;
153
+ config[stateHandlerNames[stateName]]!(state[stateName]);
154
+ },
155
+ rebuildTree: () => {
156
+ rebuildItemMeta(mainFeature);
157
+ config.setState?.(state);
158
+ },
159
+ getConfig: () => config,
160
+ setConfig: (updater) => {
161
+ config = typeof updater === "function" ? updater(config) : updater;
162
+
163
+ if (config.state) {
164
+ state = { ...state, ...config.state };
165
+ }
166
+ },
167
+ getItemInstance: (itemId) => itemInstancesMap[itemId],
168
+ getItems: () => itemInstances,
169
+ registerElement: (element) => {
170
+ if (treeElement === element) {
171
+ return;
172
+ }
173
+
174
+ if (treeElement && !element) {
175
+ eachFeature((feature) =>
176
+ feature.onTreeUnmount?.(treeInstance, treeElement!)
177
+ );
178
+ } else if (!treeElement && element) {
179
+ eachFeature((feature) =>
180
+ feature.onTreeMount?.(treeInstance, element)
181
+ );
182
+ }
183
+ treeElement = element;
184
+ },
185
+ getElement: () => treeElement,
186
+ getDataRef: () => treeDataRef,
187
+ getHotkeyPresets: () => hotkeyPresets,
188
+ }),
189
+ createItemInstance: (prev, instance, _, itemId) => ({
190
+ ...prev,
191
+ registerElement: (element) => {
192
+ if (itemElementsMap[itemId] === element) {
193
+ return;
194
+ }
195
+
196
+ const oldElement = itemElementsMap[itemId];
197
+ if (oldElement && !element) {
198
+ eachFeature((feature) =>
199
+ feature.onItemUnmount?.(instance, oldElement!, treeInstance)
200
+ );
201
+ } else if (!oldElement && element) {
202
+ eachFeature((feature) =>
203
+ feature.onItemMount?.(instance, element!, treeInstance)
204
+ );
205
+ }
206
+ itemElementsMap[itemId] = element;
207
+ },
208
+ getElement: () => itemElementsMap[itemId],
209
+ // eslint-disable-next-line no-return-assign
210
+ getDataRef: () => (itemDataRefs[itemId] ??= { current: {} }),
211
+ getItemMeta: () => itemMetaMap[itemId],
212
+ }),
213
+ };
214
+
215
+ const features = [mainFeature, ...additionalFeatures];
216
+
217
+ for (const feature of features) {
218
+ Object.assign(
219
+ treeInstance,
220
+ feature.createTreeInstance?.({ ...treeInstance }, treeInstance) ?? {}
221
+ );
222
+ Object.assign(hotkeyPresets, feature.hotkeys ?? {});
223
+ }
224
+
225
+ rebuildItemMeta(mainFeature);
226
+
227
+ return treeInstance;
228
+ };
@@ -0,0 +1,126 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ import { AsyncDataLoaderFeatureDef, AsyncDataLoaderRef } from "./types";
3
+ import { MainFeatureDef } from "../main/types";
4
+ import { makeStateUpdater } from "../../utils";
5
+ import { TreeFeatureDef } from "../tree/types";
6
+
7
+ export const asyncDataLoaderFeature: FeatureImplementation<
8
+ any,
9
+ AsyncDataLoaderFeatureDef<any>,
10
+ MainFeatureDef | TreeFeatureDef<any> | AsyncDataLoaderFeatureDef<any>
11
+ > = {
12
+ key: "async-data-loader",
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
+
28
+ createTreeInstance: (prev, instance) => ({
29
+ ...prev,
30
+
31
+ retrieveItemData: (itemId) => {
32
+ const config = instance.getConfig();
33
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
34
+ dataRef.current.itemData ??= {};
35
+ dataRef.current.childrenIds ??= {};
36
+
37
+ if (dataRef.current.itemData[itemId]) {
38
+ return dataRef.current.itemData[itemId];
39
+ }
40
+
41
+ if (!instance.getState().loadingItems.includes(itemId)) {
42
+ instance.applySubStateUpdate("loadingItems", (loadingItems) => [
43
+ ...loadingItems,
44
+ itemId,
45
+ ]);
46
+ config.asyncDataLoader?.getItem(itemId).then((item) => {
47
+ dataRef.current.itemData[itemId] = item;
48
+ config.onLoadedItem?.(itemId, item);
49
+ instance.applySubStateUpdate("loadingItems", (loadingItems) =>
50
+ loadingItems.filter((id) => id !== itemId)
51
+ );
52
+ });
53
+ }
54
+
55
+ return config.createLoadingItemData?.() ?? null;
56
+ },
57
+
58
+ retrieveChildrenIds: (itemId) => {
59
+ const config = instance.getConfig();
60
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
61
+ dataRef.current.itemData ??= {};
62
+ dataRef.current.childrenIds ??= {};
63
+ if (dataRef.current.childrenIds[itemId]) {
64
+ return dataRef.current.childrenIds[itemId];
65
+ }
66
+
67
+ if (instance.getState().loadingItems.includes(itemId)) {
68
+ return [];
69
+ }
70
+
71
+ instance.applySubStateUpdate("loadingItems", (loadingItems) => [
72
+ ...loadingItems,
73
+ itemId,
74
+ ]);
75
+
76
+ if (config.asyncDataLoader?.getChildrenWithData) {
77
+ config.asyncDataLoader?.getChildrenWithData(itemId).then((children) => {
78
+ for (const { id, data } of children) {
79
+ dataRef.current.itemData[id] = data;
80
+ config.onLoadedItem?.(id, data);
81
+ }
82
+ const childrenIds = children.map(({ id }) => id);
83
+ dataRef.current.childrenIds[itemId] = childrenIds;
84
+ config.onLoadedChildren?.(itemId, childrenIds);
85
+ instance.applySubStateUpdate("loadingItems", (loadingItems) =>
86
+ loadingItems.filter((id) => id !== itemId)
87
+ );
88
+ instance.rebuildTree();
89
+ });
90
+ } else {
91
+ config.asyncDataLoader?.getChildren(itemId).then((childrenIds) => {
92
+ dataRef.current.childrenIds[itemId] = childrenIds;
93
+ config.onLoadedChildren?.(itemId, childrenIds);
94
+ instance.applySubStateUpdate("loadingItems", (loadingItems) =>
95
+ loadingItems.filter((id) => id !== itemId)
96
+ );
97
+ instance.rebuildTree();
98
+ });
99
+ }
100
+
101
+ return [];
102
+ },
103
+
104
+ invalidateItemData: (itemId) => {
105
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
106
+ delete dataRef.current.itemData?.[itemId];
107
+ instance.retrieveItemData(itemId);
108
+ },
109
+
110
+ invalidateChildrenIds: (itemId) => {
111
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
112
+ delete dataRef.current.childrenIds?.[itemId];
113
+ instance.retrieveChildrenIds(itemId);
114
+ },
115
+ }),
116
+
117
+ createItemInstance: (prev, item, tree) => ({
118
+ ...prev,
119
+ isLoading: () =>
120
+ tree.getState().loadingItems.includes(item.getItemMeta().itemId),
121
+ invalidateItemData: () =>
122
+ tree.invalidateItemData(item.getItemMeta().itemId),
123
+ invalidateChildrenIds: () =>
124
+ tree.invalidateChildrenIds(item.getItemMeta().itemId),
125
+ }),
126
+ };
@@ -0,0 +1,41 @@
1
+ import { SetStateFn } from "../../types/core";
2
+ import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
3
+
4
+ export type AsyncTreeDataLoader<T> = {
5
+ getItem: (itemId: string) => Promise<T>;
6
+ getChildren: (itemId: string) => Promise<string[]>;
7
+ getChildrenWithData?: (itemId: string) => Promise<{ id: string; data: T }[]>;
8
+ };
9
+
10
+ export type AsyncDataLoaderRef<T = any> = {
11
+ itemData: Record<string, T>;
12
+ childrenIds: Record<string, string[]>;
13
+ };
14
+
15
+ /**
16
+ * @category Async Data Loader/General
17
+ * */
18
+ export type AsyncDataLoaderFeatureDef<T> = {
19
+ state: {
20
+ loadingItems: string[];
21
+ };
22
+ config: {
23
+ rootItemId: string;
24
+ createLoadingItemData?: () => T;
25
+ setLoadingItems?: SetStateFn<string[]>;
26
+ onLoadedItem?: (itemId: string, item: T) => void;
27
+ onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
28
+ asyncDataLoader?: AsyncTreeDataLoader<T>;
29
+ };
30
+ treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
31
+ /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
32
+ invalidateItemData: (itemId: string) => void;
33
+ invalidateChildrenIds: (itemId: string) => void;
34
+ };
35
+ itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
36
+ invalidateItemData: () => void;
37
+ invalidateChildrenIds: () => void;
38
+ isLoading: () => void;
39
+ };
40
+ hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
41
+ };
@@ -0,0 +1,214 @@
1
+ import { FeatureDefs, FeatureImplementation } from "../../types/core";
2
+ import { DndDataRef, DragAndDropFeatureDef, DragLineData } from "./types";
3
+ import { canDrop, getDragCode, getDropTarget } from "./utils";
4
+ import { makeStateUpdater } from "../../utils";
5
+
6
+ export const dragAndDropFeature: FeatureImplementation<
7
+ any,
8
+ DragAndDropFeatureDef<any>,
9
+ FeatureDefs<any>
10
+ > = {
11
+ key: "dragAndDrop",
12
+ deps: ["selection"],
13
+
14
+ getDefaultConfig: (defaultConfig, tree) => ({
15
+ canDrop: (_, target) => target.item.isFolder(),
16
+ canDropForeignDragObject: () => false,
17
+ setDndState: makeStateUpdater("dnd", tree),
18
+ ...defaultConfig,
19
+ }),
20
+
21
+ stateHandlerNames: {
22
+ dnd: "setDndState",
23
+ },
24
+
25
+ createTreeInstance: (prev, tree) => ({
26
+ ...prev,
27
+
28
+ getDropTarget: () => {
29
+ return tree.getState().dnd?.dragTarget ?? null;
30
+ },
31
+
32
+ getDragLineData: (): DragLineData | null => {
33
+ const target = tree.getDropTarget();
34
+ const intend = (target?.item.getItemMeta().level ?? 0) + 1;
35
+
36
+ if (!target || target.childIndex === null) return null;
37
+
38
+ const children = target.item.getChildren();
39
+
40
+ if (target.childIndex === children.length) {
41
+ const bb = children[target.childIndex - 1]
42
+ ?.getElement()
43
+ ?.getBoundingClientRect();
44
+
45
+ if (bb) {
46
+ return {
47
+ intend,
48
+ top: bb.bottom,
49
+ left: bb.left,
50
+ right: bb.right,
51
+ };
52
+ }
53
+ }
54
+
55
+ const bb = children[target.childIndex]
56
+ ?.getElement()
57
+ ?.getBoundingClientRect();
58
+
59
+ if (bb) {
60
+ return {
61
+ intend,
62
+ top: bb.top,
63
+ left: bb.left,
64
+ right: bb.right,
65
+ };
66
+ }
67
+
68
+ return null;
69
+ },
70
+ }),
71
+
72
+ createItemInstance: (prev, item, tree) => ({
73
+ ...prev,
74
+
75
+ getProps: () => ({
76
+ ...prev.getProps(),
77
+
78
+ draggable: tree.getConfig().isItemDraggable?.(item) ?? true,
79
+
80
+ onDragStart: item.getMemoizedProp("dnd/onDragStart", () => (e) => {
81
+ const selectedItems = tree.getSelectedItems();
82
+ const items = selectedItems.includes(item) ? selectedItems : [item];
83
+ const config = tree.getConfig();
84
+
85
+ if (!selectedItems.includes(item)) {
86
+ tree.setSelectedItems([item.getItemMeta().itemId]);
87
+ }
88
+
89
+ if (!(config.canDrag?.(items) ?? true)) {
90
+ e.preventDefault();
91
+ return;
92
+ }
93
+
94
+ if (config.createForeignDragObject) {
95
+ const { format, data } = config.createForeignDragObject(items);
96
+ e.dataTransfer?.setData(format, data);
97
+ }
98
+
99
+ tree.applySubStateUpdate("dnd", {
100
+ draggedItems: items,
101
+ draggingOverItem: tree.getFocusedItem(),
102
+ });
103
+ }),
104
+
105
+ onDragOver: item.getMemoizedProp("dnd/onDragOver", () => (e) => {
106
+ const target = getDropTarget(e, item, tree);
107
+ const dataRef = tree.getDataRef<DndDataRef>();
108
+
109
+ if (
110
+ !tree.getState().dnd?.draggedItems &&
111
+ !tree.getConfig().canDropForeignDragObject?.(e.dataTransfer, target)
112
+ ) {
113
+ return;
114
+ }
115
+
116
+ if (!canDrop(e.dataTransfer, target, tree)) {
117
+ return;
118
+ }
119
+
120
+ e.preventDefault();
121
+ const nextDragCode = getDragCode(target);
122
+
123
+ if (nextDragCode === dataRef.current.lastDragCode) {
124
+ return;
125
+ }
126
+
127
+ dataRef.current.lastDragCode = nextDragCode;
128
+
129
+ tree.applySubStateUpdate("dnd", (state) => ({
130
+ ...state,
131
+ dragTarget: target,
132
+ draggingOverItem: item,
133
+ }));
134
+ }),
135
+
136
+ onDragLeave: item.getMemoizedProp("dnd/onDragLeave", () => () => {
137
+ const dataRef = tree.getDataRef<DndDataRef>();
138
+ dataRef.current.lastDragCode = "no-drag";
139
+ tree.applySubStateUpdate("dnd", (state) => ({
140
+ ...state,
141
+ draggingOverItem: undefined,
142
+ dragTarget: undefined,
143
+ }));
144
+ }),
145
+
146
+ onDragEnd: item.getMemoizedProp("dnd/onDragEnd", () => (e) => {
147
+ const draggedItems = tree.getState().dnd?.draggedItems;
148
+ tree.applySubStateUpdate("dnd", null);
149
+
150
+ if (e.dataTransfer.dropEffect === "none" || !draggedItems) {
151
+ return;
152
+ }
153
+
154
+ tree.getConfig().onCompleteForeignDrop?.(draggedItems);
155
+ }),
156
+
157
+ onDrop: item.getMemoizedProp("dnd/onDrop", () => (e) => {
158
+ const dataRef = tree.getDataRef<DndDataRef>();
159
+ const target = getDropTarget(e, item, tree);
160
+
161
+ if (!canDrop(e.dataTransfer, target, tree)) {
162
+ return;
163
+ }
164
+
165
+ e.preventDefault();
166
+ const config = tree.getConfig();
167
+ const draggedItems = tree.getState().dnd?.draggedItems;
168
+
169
+ dataRef.current.lastDragCode = undefined;
170
+ tree.applySubStateUpdate("dnd", null);
171
+
172
+ if (draggedItems) {
173
+ config.onDrop?.(draggedItems, target);
174
+ } else {
175
+ config.onDropForeignDragObject?.(e.dataTransfer, target);
176
+ }
177
+ // TODO rebuild tree?
178
+ }),
179
+ }),
180
+
181
+ isDropTarget: () => {
182
+ const target = tree.getDropTarget();
183
+ return target ? target.item.getId() === item.getId() : false;
184
+ },
185
+
186
+ isDropTargetAbove: () => {
187
+ const target = tree.getDropTarget();
188
+
189
+ if (
190
+ !target ||
191
+ target.childIndex === null ||
192
+ target.item !== item.getParent()
193
+ )
194
+ return false;
195
+ return target.childIndex === item.getItemMeta().posInSet;
196
+ },
197
+
198
+ isDropTargetBelow: () => {
199
+ const target = tree.getDropTarget();
200
+
201
+ if (
202
+ !target ||
203
+ target.childIndex === null ||
204
+ target.item !== item.getParent()
205
+ )
206
+ return false;
207
+ return target.childIndex - 1 === item.getItemMeta().posInSet;
208
+ },
209
+
210
+ isDraggingOver: () => {
211
+ return tree.getState().dnd?.draggingOverItem?.getId() === item.getId();
212
+ },
213
+ }),
214
+ };
@@ -0,0 +1,89 @@
1
+ import { ItemInstance, SetStateFn } from "../../types/core";
2
+
3
+ export type DndDataRef = {
4
+ lastDragCode?: string;
5
+ };
6
+
7
+ export type DndState<T> = {
8
+ draggedItems?: ItemInstance<T>[]; // TODO not used anymore?
9
+ draggingOverItem?: ItemInstance<T>;
10
+ dragTarget?: DropTarget<T>;
11
+ };
12
+
13
+ export type DragLineData = {
14
+ intend: number;
15
+ top: number;
16
+ left: number;
17
+ right: number;
18
+ };
19
+
20
+ export type DropTarget<T> =
21
+ | {
22
+ item: ItemInstance<T>;
23
+ childIndex: number;
24
+ insertionIndex: number;
25
+ }
26
+ | {
27
+ item: ItemInstance<T>;
28
+ childIndex: null;
29
+ insertionIndex: null;
30
+ };
31
+
32
+ export enum DropTargetPosition {
33
+ Top = "top",
34
+ Bottom = "bottom",
35
+ Item = "item",
36
+ }
37
+
38
+ export type DragAndDropFeatureDef<T> = {
39
+ state: {
40
+ dnd?: DndState<T> | null;
41
+ };
42
+ config: {
43
+ setDndState?: SetStateFn<DndState<T> | null>;
44
+
45
+ topLinePercentage?: number;
46
+ bottomLinePercentage?: number;
47
+ canDropInbetween?: boolean;
48
+
49
+ isItemDraggable?: (item: ItemInstance<T>) => boolean;
50
+ canDrag?: (items: ItemInstance<T>[]) => boolean;
51
+ canDrop?: (items: ItemInstance<T>[], target: DropTarget<T>) => boolean;
52
+
53
+ createForeignDragObject?: (items: ItemInstance<T>[]) => {
54
+ format: string;
55
+ data: any;
56
+ };
57
+ canDropForeignDragObject?: (
58
+ dataTransfer: DataTransfer,
59
+ target: DropTarget<T>
60
+ ) => boolean;
61
+ onDrop?: (items: ItemInstance<T>[], target: DropTarget<T>) => void;
62
+ onDropForeignDragObject?: (
63
+ dataTransfer: DataTransfer,
64
+ target: DropTarget<T>
65
+ ) => void;
66
+
67
+ /** Runs in the onDragEnd event, if `ev.dataTransfer.dropEffect` is not `none`, i.e. the drop
68
+ * was not aborted. No target is provided as parameter since the target may be a foreign drop target.
69
+ * This is useful to seperate out the logic to move dragged items out of their previous parents.
70
+ * Use `onDrop` to handle drop-related logic.
71
+ *
72
+ * This ignores the `canDrop` handler, since the drop target is unknown in this handler.
73
+ */
74
+ // onSuccessfulDragEnd?: (items: ItemInstance<T>[]) => void;
75
+
76
+ onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
77
+ };
78
+ treeInstance: {
79
+ getDropTarget: () => DropTarget<T> | null;
80
+ getDragLineData: () => DragLineData | null;
81
+ };
82
+ itemInstance: {
83
+ isDropTarget: () => boolean;
84
+ isDropTargetAbove: () => boolean;
85
+ isDropTargetBelow: () => boolean;
86
+ isDraggingOver: () => boolean;
87
+ };
88
+ hotkeys: never;
89
+ };