@headless-tree/core 0.0.1

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 (87) hide show
  1. package/lib/core/create-tree.d.ts +2 -0
  2. package/lib/core/create-tree.js +116 -0
  3. package/lib/data-adapters/nested-data-adapter.d.ts +9 -0
  4. package/lib/data-adapters/nested-data-adapter.js +32 -0
  5. package/lib/data-adapters/types.d.ts +7 -0
  6. package/lib/data-adapters/types.js +2 -0
  7. package/lib/features/async-data-loader/feature.d.ts +5 -0
  8. package/lib/features/async-data-loader/feature.js +80 -0
  9. package/lib/features/async-data-loader/types.d.ts +41 -0
  10. package/lib/features/async-data-loader/types.js +2 -0
  11. package/lib/features/drag-and-drop/feature.d.ts +3 -0
  12. package/lib/features/drag-and-drop/feature.js +144 -0
  13. package/lib/features/drag-and-drop/types.d.ts +49 -0
  14. package/lib/features/drag-and-drop/types.js +9 -0
  15. package/lib/features/drag-and-drop/utils.d.ts +7 -0
  16. package/lib/features/drag-and-drop/utils.js +121 -0
  17. package/lib/features/expand-all/feature.d.ts +6 -0
  18. package/lib/features/expand-all/feature.js +39 -0
  19. package/lib/features/expand-all/types.d.ts +17 -0
  20. package/lib/features/expand-all/types.js +2 -0
  21. package/lib/features/hotkeys-core/feature.d.ts +4 -0
  22. package/lib/features/hotkeys-core/feature.js +73 -0
  23. package/lib/features/hotkeys-core/types.d.ts +25 -0
  24. package/lib/features/hotkeys-core/types.js +2 -0
  25. package/lib/features/main/types.d.ts +36 -0
  26. package/lib/features/main/types.js +2 -0
  27. package/lib/features/renaming/feature.d.ts +5 -0
  28. package/lib/features/renaming/feature.js +65 -0
  29. package/lib/features/renaming/types.d.ts +27 -0
  30. package/lib/features/renaming/types.js +2 -0
  31. package/lib/features/search/feature.d.ts +5 -0
  32. package/lib/features/search/feature.js +90 -0
  33. package/lib/features/search/types.d.ts +33 -0
  34. package/lib/features/search/types.js +2 -0
  35. package/lib/features/selection/feature.d.ts +5 -0
  36. package/lib/features/selection/feature.js +112 -0
  37. package/lib/features/selection/types.d.ts +21 -0
  38. package/lib/features/selection/types.js +2 -0
  39. package/lib/features/sync-data-loader/feature.d.ts +4 -0
  40. package/lib/features/sync-data-loader/feature.js +9 -0
  41. package/lib/features/sync-data-loader/types.d.ts +19 -0
  42. package/lib/features/sync-data-loader/types.js +2 -0
  43. package/lib/features/tree/feature.d.ts +6 -0
  44. package/lib/features/tree/feature.js +216 -0
  45. package/lib/features/tree/types.d.ts +57 -0
  46. package/lib/features/tree/types.js +2 -0
  47. package/lib/index.d.ts +21 -0
  48. package/lib/index.js +37 -0
  49. package/lib/mddocs-entry.d.ts +21 -0
  50. package/lib/mddocs-entry.js +17 -0
  51. package/lib/types/core.d.ts +68 -0
  52. package/lib/types/core.js +2 -0
  53. package/lib/types/deep-merge.d.ts +13 -0
  54. package/lib/types/deep-merge.js +2 -0
  55. package/lib/utils.d.ts +9 -0
  56. package/lib/utils.js +105 -0
  57. package/package.json +15 -0
  58. package/src/core/create-tree.ts +195 -0
  59. package/src/data-adapters/nested-data-adapter.ts +48 -0
  60. package/src/data-adapters/types.ts +9 -0
  61. package/src/features/async-data-loader/feature.ts +117 -0
  62. package/src/features/async-data-loader/types.ts +41 -0
  63. package/src/features/drag-and-drop/feature.ts +153 -0
  64. package/src/features/drag-and-drop/types.ts +64 -0
  65. package/src/features/drag-and-drop/utils.ts +88 -0
  66. package/src/features/expand-all/feature.ts +62 -0
  67. package/src/features/expand-all/types.ts +13 -0
  68. package/src/features/hotkeys-core/feature.ts +111 -0
  69. package/src/features/hotkeys-core/types.ts +36 -0
  70. package/src/features/main/types.ts +41 -0
  71. package/src/features/renaming/feature.ts +102 -0
  72. package/src/features/renaming/types.ts +28 -0
  73. package/src/features/search/feature.ts +142 -0
  74. package/src/features/search/types.ts +39 -0
  75. package/src/features/selection/feature.ts +153 -0
  76. package/src/features/selection/types.ts +28 -0
  77. package/src/features/sync-data-loader/feature.ts +27 -0
  78. package/src/features/sync-data-loader/types.ts +20 -0
  79. package/src/features/tree/feature.ts +307 -0
  80. package/src/features/tree/types.ts +70 -0
  81. package/src/index.ts +23 -0
  82. package/src/mddocs-entry.ts +26 -0
  83. package/src/types/core.ts +182 -0
  84. package/src/types/deep-merge.ts +31 -0
  85. package/src/utils.ts +136 -0
  86. package/tsconfig.json +7 -0
  87. package/typedoc.json +4 -0
@@ -0,0 +1,195 @@
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
+ export const createTree = <T>(
34
+ initialConfig: TreeConfig<T>
35
+ ): TreeInstance<T> => {
36
+ const treeInstance: TreeInstance<T> = {} as any;
37
+
38
+ const additionalFeatures = [treeFeature, ...(initialConfig.features ?? [])];
39
+ let state = additionalFeatures.reduce(
40
+ (acc, feature) => feature.getInitialState?.(acc, treeInstance) ?? acc,
41
+ initialConfig.state ?? {}
42
+ ) as TreeState<T>;
43
+ let config = additionalFeatures.reduce(
44
+ (acc, feature) => feature.getDefaultConfig?.(acc, treeInstance) ?? acc,
45
+ initialConfig
46
+ ) as TreeConfig<T>;
47
+
48
+ let treeElement: HTMLElement | undefined | null;
49
+ const treeDataRef: { current: any } = { current: {} };
50
+
51
+ const itemInstancesMap: Record<string, ItemInstance<T>> = {};
52
+ let itemInstances: ItemInstance<T>[] = [];
53
+ const itemElementsMap: Record<string, HTMLElement | undefined | null> = {};
54
+ const itemDataRefs: Record<string, { current: any }> = {};
55
+ let itemMetaMap: Record<string, ItemMeta> = {};
56
+
57
+ const hotkeyPresets = {} as HotkeysConfig<T>;
58
+
59
+ const rebuildItemMeta = (main: FeatureImplementation) => {
60
+ // TODO can we find a way to only run this for the changed substructure?
61
+ itemInstances = [];
62
+ itemMetaMap = {};
63
+
64
+ const rootInstance = buildItemInstance(
65
+ [main, ...additionalFeatures],
66
+ treeInstance,
67
+ config.rootItemId
68
+ );
69
+ itemInstancesMap[config.rootItemId] = rootInstance;
70
+ itemMetaMap[config.rootItemId] = {
71
+ itemId: config.rootItemId,
72
+ index: -1,
73
+ parentId: null!,
74
+ level: -1,
75
+ posInSet: 0,
76
+ setSize: 1,
77
+ };
78
+
79
+ for (const item of treeInstance.getItemsMeta()) {
80
+ itemMetaMap[item.itemId] = item;
81
+ if (!itemInstancesMap[item.itemId]) {
82
+ const instance = buildItemInstance(
83
+ [main, ...additionalFeatures],
84
+ treeInstance,
85
+ item.itemId
86
+ );
87
+ itemInstancesMap[item.itemId] = instance;
88
+ itemInstances.push(instance);
89
+ } else {
90
+ itemInstances.push(itemInstancesMap[item.itemId]);
91
+ }
92
+ }
93
+ console.log("REBUILT");
94
+ };
95
+
96
+ const eachFeature = (fn: (feature: FeatureImplementation<any>) => void) => {
97
+ for (const feature of additionalFeatures) {
98
+ fn(feature);
99
+ }
100
+ };
101
+
102
+ const mainFeature: FeatureImplementation<
103
+ T,
104
+ MainFeatureDef<T>,
105
+ MainFeatureDef<T>
106
+ > = {
107
+ key: "main",
108
+ createTreeInstance: (prev) => ({
109
+ ...prev,
110
+ getState: () => state,
111
+ setState: (updater) => {
112
+ state = typeof updater === "function" ? updater(state) : updater;
113
+ config.setState?.(state);
114
+ eachFeature((feature) => feature.setState?.(treeInstance));
115
+ eachFeature((feature) => feature.onStateOrConfigChange?.(treeInstance));
116
+ },
117
+ rebuildTree: () => {
118
+ rebuildItemMeta(mainFeature);
119
+ config.setState?.(state);
120
+ },
121
+ getConfig: () => config,
122
+ setConfig: (updater) => {
123
+ config = typeof updater === "function" ? updater(config) : updater;
124
+
125
+ if (config.state) {
126
+ state = { ...state, ...config.state };
127
+ eachFeature((feature) => feature.setState?.(treeInstance));
128
+ }
129
+
130
+ eachFeature((feature) => feature.onConfigChange?.(treeInstance));
131
+ eachFeature((feature) => feature.onStateOrConfigChange?.(treeInstance));
132
+ },
133
+ getItemInstance: (itemId) => itemInstancesMap[itemId],
134
+ getItems: () => itemInstances,
135
+ registerElement: (element) => {
136
+ if (treeElement === element) {
137
+ return;
138
+ }
139
+
140
+ if (treeElement && !element) {
141
+ eachFeature((feature) =>
142
+ feature.onTreeUnmount?.(treeInstance, treeElement!)
143
+ );
144
+ } else if (!treeElement && element) {
145
+ eachFeature((feature) =>
146
+ feature.onTreeMount?.(treeInstance, element)
147
+ );
148
+ }
149
+ treeElement = element;
150
+ },
151
+ getElement: () => treeElement,
152
+ getDataRef: () => treeDataRef,
153
+ getHotkeyPresets: () => hotkeyPresets,
154
+ }),
155
+ createItemInstance: (prev, instance, _, itemId) => ({
156
+ ...prev,
157
+ registerElement: (element) => {
158
+ if (itemElementsMap[itemId] === element) {
159
+ return;
160
+ }
161
+
162
+ const oldElement = itemElementsMap[itemId];
163
+ if (oldElement && !element) {
164
+ eachFeature((feature) =>
165
+ feature.onItemUnmount?.(instance, oldElement!, treeInstance)
166
+ );
167
+ } else if (!oldElement && element) {
168
+ eachFeature((feature) =>
169
+ feature.onItemMount?.(instance, element!, treeInstance)
170
+ );
171
+ }
172
+ itemElementsMap[itemId] = element;
173
+ },
174
+ getElement: () => itemElementsMap[itemId],
175
+ // eslint-disable-next-line no-return-assign
176
+ getDataRef: () => (itemDataRefs[itemId] ??= { current: {} }),
177
+ getItemMeta: () => itemMetaMap[itemId],
178
+ }),
179
+ };
180
+
181
+ // todo sort features
182
+ const features = [mainFeature, ...additionalFeatures];
183
+
184
+ for (const feature of features) {
185
+ Object.assign(
186
+ treeInstance,
187
+ feature.createTreeInstance?.({ ...treeInstance }, treeInstance) ?? {}
188
+ );
189
+ Object.assign(hotkeyPresets, feature.hotkeys ?? {});
190
+ }
191
+
192
+ rebuildItemMeta(mainFeature);
193
+
194
+ return treeInstance;
195
+ };
@@ -0,0 +1,48 @@
1
+ import { DataAdapterConfig } from "./types";
2
+ import { ItemInstance } from "../types/core";
3
+ import { DropTarget } from "../features/drag-and-drop/types";
4
+ import { performItemsMove } from "../utils";
5
+
6
+ interface NestedDataAdapterProps<T> {
7
+ rootItem: T;
8
+ getItemId: (item: T) => string;
9
+ getChildren: (item: T) => T[] | undefined;
10
+ changeChildren?: (item: T, children: T[]) => void;
11
+ }
12
+
13
+ const createItemMap = <T>(
14
+ props: NestedDataAdapterProps<T>,
15
+ item: T,
16
+ map: Record<string, T> = {}
17
+ ) => {
18
+ map[props.getItemId(item)] = item;
19
+ props.getChildren(item)?.forEach((child) => {
20
+ createItemMap(props, child, map);
21
+ });
22
+ return map;
23
+ };
24
+
25
+ export const nestedDataAdapter = <T = any>(
26
+ props: NestedDataAdapterProps<T>
27
+ ): DataAdapterConfig<T> => {
28
+ const itemMap = createItemMap(props, props.rootItem);
29
+ return {
30
+ rootItemId: props.getItemId(props.rootItem),
31
+ dataLoader: {
32
+ getItem: (itemId) => itemMap[itemId],
33
+ getChildren: (itemId) =>
34
+ props.getChildren(itemMap[itemId])?.map(props.getItemId) ?? [],
35
+ },
36
+ onDrop: (items: ItemInstance<T>[], target: DropTarget<T>) => {
37
+ if (!props.changeChildren) {
38
+ return;
39
+ }
40
+ performItemsMove(items, target, (item, newChildren) => {
41
+ props.changeChildren?.(
42
+ item.getItemData(),
43
+ newChildren.map((child) => child.getItemData())
44
+ );
45
+ });
46
+ },
47
+ };
48
+ };
@@ -0,0 +1,9 @@
1
+ import { DragAndDropFeatureDef } from "../features/drag-and-drop/types";
2
+ import { SyncDataLoaderFeatureDef } from "../features/sync-data-loader/types";
3
+
4
+ export type DataAdapterConfig<T> = {
5
+ rootItemId: SyncDataLoaderFeatureDef<T>["config"]["rootItemId"];
6
+ dataLoader: SyncDataLoaderFeatureDef<T>["config"]["dataLoader"];
7
+ onDrop: DragAndDropFeatureDef<T>["config"]["onDrop"];
8
+ // TODO onDropForeignDragObject: DragAndDropFeatureDef<T>["config"]["onDropForeignDragObject"];
9
+ };
@@ -0,0 +1,117 @@
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
+ dependingFeatures: ["main"],
14
+
15
+ getInitialState: (initialState) => ({
16
+ loadingItems: [],
17
+ ...initialState,
18
+ }),
19
+
20
+ getDefaultConfig: (defaultConfig, tree) => ({
21
+ setLoadingItems: makeStateUpdater("loadingItems", tree),
22
+ ...defaultConfig,
23
+ }),
24
+
25
+ createTreeInstance: (prev, instance) => ({
26
+ ...prev,
27
+
28
+ retrieveItemData: (itemId) => {
29
+ const config = instance.getConfig();
30
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
31
+ dataRef.current.itemData ??= {};
32
+ dataRef.current.childrenIds ??= {};
33
+
34
+ if (dataRef.current.itemData[itemId]) {
35
+ return dataRef.current.itemData[itemId];
36
+ }
37
+
38
+ if (!instance.getState().loadingItems.includes(itemId)) {
39
+ config.setLoadingItems?.((loadingItems) => [...loadingItems, itemId]);
40
+ config.asyncDataLoader?.getItem(itemId).then((item) => {
41
+ dataRef.current.itemData[itemId] = item;
42
+ config.onLoadedItem?.(itemId, item);
43
+ config.setLoadingItems?.((loadingItems) =>
44
+ loadingItems.filter((id) => id !== itemId)
45
+ );
46
+ });
47
+ }
48
+
49
+ return config.createLoadingItemData?.() ?? null;
50
+ },
51
+
52
+ retrieveChildrenIds: (itemId) => {
53
+ const config = instance.getConfig();
54
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
55
+ dataRef.current.itemData ??= {};
56
+ dataRef.current.childrenIds ??= {};
57
+ if (dataRef.current.childrenIds[itemId]) {
58
+ return dataRef.current.childrenIds[itemId];
59
+ }
60
+
61
+ if (instance.getState().loadingItems.includes(itemId)) {
62
+ return [];
63
+ }
64
+
65
+ config.setLoadingItems?.((loadingItems) => [...loadingItems, itemId]);
66
+
67
+ if (config.asyncDataLoader?.getChildrenWithData) {
68
+ config.asyncDataLoader?.getChildrenWithData(itemId).then((children) => {
69
+ for (const { id, data } of children) {
70
+ dataRef.current.itemData[id] = data;
71
+ config.onLoadedItem?.(id, data);
72
+ }
73
+ const childrenIds = children.map(({ id }) => id);
74
+ dataRef.current.childrenIds[itemId] = childrenIds;
75
+ config.onLoadedChildren?.(itemId, childrenIds);
76
+ config.setLoadingItems?.((loadingItems) =>
77
+ loadingItems.filter((id) => id !== itemId)
78
+ );
79
+ instance.rebuildTree();
80
+ });
81
+ } else {
82
+ config.asyncDataLoader?.getChildren(itemId).then((childrenIds) => {
83
+ dataRef.current.childrenIds[itemId] = childrenIds;
84
+ config.onLoadedChildren?.(itemId, childrenIds);
85
+ config.setLoadingItems?.((loadingItems) =>
86
+ loadingItems.filter((id) => id !== itemId)
87
+ );
88
+ instance.rebuildTree();
89
+ });
90
+ }
91
+
92
+ return [];
93
+ },
94
+
95
+ invalidateItemData: (itemId) => {
96
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
97
+ delete dataRef.current.itemData?.[itemId];
98
+ instance.retrieveItemData(itemId);
99
+ },
100
+
101
+ invalidateChildrenIds: (itemId) => {
102
+ const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
103
+ delete dataRef.current.childrenIds?.[itemId];
104
+ instance.retrieveChildrenIds(itemId);
105
+ },
106
+ }),
107
+
108
+ createItemInstance: (prev, item, tree) => ({
109
+ ...prev,
110
+ isLoading: () =>
111
+ tree.getState().loadingItems.includes(item.getItemMeta().itemId),
112
+ invalidateItemData: () =>
113
+ tree.invalidateItemData(item.getItemMeta().itemId),
114
+ invalidateChildrenIds: () =>
115
+ tree.invalidateChildrenIds(item.getItemMeta().itemId),
116
+ }),
117
+ };
@@ -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,153 @@
1
+ import { FeatureDefs, FeatureImplementation } from "../../types/core";
2
+ import { DndDataRef, DragAndDropFeatureDef } 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
+ dependingFeatures: ["main", "tree", "selection"],
13
+
14
+ getDefaultConfig: (defaultConfig, tree) => ({
15
+ canDrop: (_, target) => target.item.isFolder(),
16
+ setDndState: makeStateUpdater("dnd", tree),
17
+ ...defaultConfig,
18
+ }),
19
+
20
+ createTreeInstance: (prev, tree) => ({
21
+ ...prev,
22
+
23
+ getDropTarget: () => {
24
+ return tree.getState().dnd?.dragTarget ?? null;
25
+ },
26
+ }),
27
+
28
+ createItemInstance: (prev, item, tree) => ({
29
+ ...prev,
30
+
31
+ getProps: () => ({
32
+ ...prev.getProps(),
33
+
34
+ draggable: tree.getConfig().isItemDraggable?.(item) ?? true,
35
+
36
+ onDragStart: (e) => {
37
+ const selectedItems = tree.getSelectedItems();
38
+ const items = selectedItems.includes(item) ? selectedItems : [item];
39
+ const config = tree.getConfig();
40
+
41
+ if (!selectedItems.includes(item)) {
42
+ tree.setSelectedItems([item.getItemMeta().itemId]);
43
+ }
44
+
45
+ if (!(config.canDrag?.(items) ?? true)) {
46
+ e.preventDefault();
47
+ return;
48
+ }
49
+
50
+ if (config.createForeignDragObject) {
51
+ const { format, data } = config.createForeignDragObject(items);
52
+ e.dataTransfer?.setData(format, data);
53
+ }
54
+
55
+ tree.getConfig().setDndState?.({
56
+ draggedItems: items,
57
+ draggingOverItem: tree.getFocusedItem(),
58
+ });
59
+ },
60
+
61
+ onDragOver: (e) => {
62
+ const target = getDropTarget(e, item, tree);
63
+ const dataRef = tree.getDataRef<DndDataRef>();
64
+
65
+ if (
66
+ !tree.getState().dnd?.draggedItems &&
67
+ !tree.getConfig().canDropForeignDragObject?.(e.dataTransfer, target)
68
+ ) {
69
+ return;
70
+ }
71
+
72
+ if (!canDrop(e.dataTransfer, target, tree)) {
73
+ return;
74
+ }
75
+
76
+ e.preventDefault();
77
+ const nextDragCode = getDragCode(target);
78
+
79
+ if (nextDragCode === dataRef.current.lastDragCode) {
80
+ return;
81
+ }
82
+
83
+ dataRef.current.lastDragCode = nextDragCode;
84
+
85
+ tree.getConfig().setDndState?.((state) => ({
86
+ ...state,
87
+ dragTarget: target,
88
+ draggingOverItem: item,
89
+ }));
90
+ },
91
+
92
+ onDragLeave: () => {
93
+ const dataRef = tree.getDataRef<DndDataRef>();
94
+ dataRef.current.lastDragCode = "no-drag";
95
+ tree.getConfig().setDndState?.((state) => ({
96
+ ...state,
97
+ draggingOverItem: undefined,
98
+ dragTarget: undefined,
99
+ }));
100
+ },
101
+
102
+ onDrop: (e) => {
103
+ const dataRef = tree.getDataRef<DndDataRef>();
104
+ const target = getDropTarget(e, item, tree);
105
+
106
+ if (!canDrop(e.dataTransfer, target, tree)) {
107
+ return;
108
+ }
109
+
110
+ e.preventDefault();
111
+ const config = tree.getConfig();
112
+ const draggedItems = tree.getState().dnd?.draggedItems;
113
+
114
+ dataRef.current.lastDragCode = undefined;
115
+ tree.getConfig().setDndState?.(null);
116
+
117
+ if (draggedItems) {
118
+ config.onDrop?.(draggedItems, target);
119
+ } else {
120
+ config.onDropForeignDragObject?.(e.dataTransfer, target);
121
+ }
122
+ // TODO rebuild tree?
123
+ },
124
+ }),
125
+
126
+ isDropTarget: () => {
127
+ const target = tree.getDropTarget();
128
+ return target ? target.item.getId() === item.getId() : false;
129
+ },
130
+
131
+ isDropTargetAbove: () => {
132
+ const target = tree.getDropTarget();
133
+
134
+ if (!target || target.childIndex === null) return false;
135
+ const targetIndex = target.item.getItemMeta().index;
136
+
137
+ return targetIndex + target.childIndex + 1 === item.getItemMeta().index;
138
+ },
139
+
140
+ isDropTargetBelow: () => {
141
+ const target = tree.getDropTarget();
142
+
143
+ if (!target || target.childIndex === null) return false;
144
+ const targetIndex = target.item.getItemMeta().index;
145
+
146
+ return targetIndex + target.childIndex === item.getItemMeta().index;
147
+ },
148
+
149
+ isDraggingOver: () => {
150
+ return tree.getState().dnd?.draggingOverItem?.getId() === item.getId();
151
+ },
152
+ }),
153
+ };
@@ -0,0 +1,64 @@
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>[];
9
+ draggingOverItem?: ItemInstance<T>;
10
+ dragTarget?: DropTarget<T>;
11
+ };
12
+
13
+ export type DropTarget<T> = {
14
+ item: ItemInstance<T>;
15
+ childIndex: number | null;
16
+ };
17
+
18
+ export enum DropTargetPosition {
19
+ Top = "top",
20
+ Bottom = "bottom",
21
+ Item = "item",
22
+ }
23
+
24
+ export type DragAndDropFeatureDef<T> = {
25
+ state: {
26
+ dnd?: DndState<T> | null;
27
+ };
28
+ config: {
29
+ setDndState?: SetStateFn<DndState<T> | null>;
30
+
31
+ topLinePercentage?: number;
32
+ bottomLinePercentage?: number;
33
+ canDropInbetween?: boolean;
34
+
35
+ isItemDraggable?: (item: ItemInstance<T>) => boolean;
36
+ canDrag?: (items: ItemInstance<T>[]) => boolean;
37
+ canDrop?: (items: ItemInstance<T>[], target: DropTarget<T>) => boolean;
38
+
39
+ createForeignDragObject?: (items: ItemInstance<T>[]) => {
40
+ format: string;
41
+ data: any;
42
+ };
43
+ canDropForeignDragObject?: (
44
+ dataTransfer: DataTransfer,
45
+ target: DropTarget<T>
46
+ ) => boolean;
47
+
48
+ onDrop?: (items: ItemInstance<T>[], target: DropTarget<T>) => void;
49
+ onDropForeignDragObject?: (
50
+ dataTransfer: DataTransfer,
51
+ target: DropTarget<T>
52
+ ) => void;
53
+ };
54
+ treeInstance: {
55
+ getDropTarget: () => DropTarget<T> | null;
56
+ };
57
+ itemInstance: {
58
+ isDropTarget: () => boolean;
59
+ isDropTargetAbove: () => boolean;
60
+ isDropTargetBelow: () => boolean;
61
+ isDraggingOver: () => boolean;
62
+ };
63
+ hotkeys: never;
64
+ };