@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
@@ -7,6 +7,10 @@ export type ItemMeta = {
7
7
  setSize: number;
8
8
  posInSet: number;
9
9
  };
10
+ export type TreeItemDataRef = {
11
+ memoizedValues: Record<string, any>;
12
+ memoizedDeps: Record<string, any[] | undefined>;
13
+ };
10
14
  export type TreeFeatureDef<T> = {
11
15
  state: {
12
16
  expandedItems: string[];
@@ -30,8 +34,7 @@ export type TreeFeatureDef<T> = {
30
34
  getFocusedItem: () => ItemInstance<any>;
31
35
  focusNextItem: () => void;
32
36
  focusPreviousItem: () => void;
33
- scrollToItem: (item: ItemInstance<any>) => void;
34
- updateDomFocus: (scrollIntoView?: boolean) => void;
37
+ updateDomFocus: () => void;
35
38
  getContainerProps: () => Record<string, any>;
36
39
  };
37
40
  itemInstance: {
@@ -52,6 +55,8 @@ export type TreeFeatureDef<T> = {
52
55
  getTree: () => TreeInstance<T>;
53
56
  getItemAbove: () => ItemInstance<T> | null;
54
57
  getItemBelow: () => ItemInstance<T> | null;
58
+ getMemoizedProp: <X>(name: string, create: () => X, deps?: any[]) => X;
59
+ scrollTo: (scrollIntoViewArg?: boolean | ScrollIntoViewOptions) => Promise<void>;
55
60
  };
56
61
  hotkeys: "focusNextItem" | "focusPreviousItem" | "expandOrDown" | "collapseOrUp" | "focusFirstItem" | "focusLastItem";
57
62
  };
@@ -18,4 +18,6 @@ export * from "./features/drag-and-drop/feature";
18
18
  export * from "./features/search/feature";
19
19
  export * from "./features/renaming/feature";
20
20
  export * from "./features/expand-all/feature";
21
- export * from "./data-adapters/nested-data-adapter";
21
+ export * from "./utilities/create-on-drop-handler";
22
+ export * from "./utilities/insert-items-at-target";
23
+ export * from "./utilities/remove-items-from-parents";
package/lib/esm/index.js CHANGED
@@ -18,4 +18,6 @@ export * from "./features/drag-and-drop/feature";
18
18
  export * from "./features/search/feature";
19
19
  export * from "./features/renaming/feature";
20
20
  export * from "./features/expand-all/feature";
21
- export * from "./data-adapters/nested-data-adapter";
21
+ export * from "./utilities/create-on-drop-handler";
22
+ export * from "./utilities/insert-items-at-target";
23
+ export * from "./utilities/remove-items-from-parents";
@@ -52,6 +52,7 @@ export type CustomHotkeysConfig<T, F extends FeatureDef = FeatureDefs<T>> = Part
52
52
  export type FeatureImplementation<T = any, D extends FeatureDef = any, F extends FeatureDef = EmptyFeatureDef> = {
53
53
  key?: string;
54
54
  dependingFeatures?: string[];
55
+ stateHandlerNames?: Partial<Record<keyof MergedFeatures<F>["state"], keyof MergedFeatures<F>["config"]>>;
55
56
  getInitialState?: (initialState: Partial<MergedFeatures<F>["state"]>, tree: MergedFeatures<F>["treeInstance"]) => Partial<D["state"] & MergedFeatures<F>["state"]>;
56
57
  getDefaultConfig?: (defaultConfig: Partial<MergedFeatures<F>["config"]>, tree: MergedFeatures<F>["treeInstance"]) => Partial<D["config"] & MergedFeatures<F>["config"]>;
57
58
  createTreeInstance?: (prev: MergedFeatures<F>["treeInstance"], instance: MergedFeatures<F>["treeInstance"]) => D["treeInstance"] & MergedFeatures<F>["treeInstance"];
@@ -60,9 +61,6 @@ export type FeatureImplementation<T = any, D extends FeatureDef = any, F extends
60
61
  onTreeUnmount?: (instance: MergedFeatures<F>["treeInstance"], treeElement: HTMLElement) => void;
61
62
  onItemMount?: (instance: MergedFeatures<F>["itemInstance"], itemElement: HTMLElement, tree: MergedFeatures<F>["treeInstance"]) => void;
62
63
  onItemUnmount?: (instance: MergedFeatures<F>["itemInstance"], itemElement: HTMLElement, tree: MergedFeatures<F>["treeInstance"]) => void;
63
- setState?: (instance: MergedFeatures<F>["treeInstance"]) => void;
64
- onConfigChange?: (instance: MergedFeatures<F>["treeInstance"]) => void;
65
- onStateOrConfigChange?: (instance: MergedFeatures<F>["treeInstance"]) => void;
66
64
  hotkeys?: HotkeysConfig<T, D>;
67
65
  };
68
66
  export {};
@@ -0,0 +1,3 @@
1
+ import { ItemInstance } from "../types/core";
2
+ import { DropTarget } from "../features/drag-and-drop/types";
3
+ export declare const createOnDropHandler: <T>(onChangeChildren: (item: ItemInstance<T>, newChildren: string[]) => void) => (items: ItemInstance<T>[], target: DropTarget<T>) => void;
@@ -0,0 +1,7 @@
1
+ import { removeItemsFromParents } from "./remove-items-from-parents";
2
+ import { insertItemsAtTarget } from "./insert-items-at-target";
3
+ export const createOnDropHandler = (onChangeChildren) => (items, target) => {
4
+ const itemIds = items.map((item) => item.getId());
5
+ removeItemsFromParents(items, onChangeChildren);
6
+ insertItemsAtTarget(itemIds, target, onChangeChildren);
7
+ };
@@ -0,0 +1,3 @@
1
+ import { ItemInstance } from "../types/core";
2
+ import { DropTarget } from "../features/drag-and-drop/types";
3
+ export declare const insertItemsAtTarget: <T>(itemIds: string[], target: DropTarget<T>, onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => void) => void;
@@ -0,0 +1,20 @@
1
+ export const insertItemsAtTarget = (itemIds, target, onChangeChildren) => {
2
+ // add moved items to new common parent, if dropped onto parent
3
+ if (target.childIndex === null) {
4
+ onChangeChildren(target.item, [
5
+ ...target.item.getChildren().map((item) => item.getId()),
6
+ ...itemIds,
7
+ ]);
8
+ // TODO items[0].getTree().rebuildTree();
9
+ return;
10
+ }
11
+ // add moved items to new common parent, if dropped between siblings
12
+ const oldChildren = target.item.getChildren();
13
+ const newChildren = [
14
+ ...oldChildren.slice(0, target.insertionIndex).map((item) => item.getId()),
15
+ ...itemIds,
16
+ ...oldChildren.slice(target.insertionIndex).map((item) => item.getId()),
17
+ ];
18
+ onChangeChildren(target.item, newChildren);
19
+ target.item.getTree().rebuildTree();
20
+ };
@@ -0,0 +1,2 @@
1
+ import { ItemInstance } from "../types/core";
2
+ export declare const removeItemsFromParents: <T>(movedItems: ItemInstance<T>[], onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => void) => void;
@@ -0,0 +1,13 @@
1
+ export const removeItemsFromParents = (movedItems, onChangeChildren) => {
2
+ var _a;
3
+ // TODO bulk sibling changes together
4
+ for (const item of movedItems) {
5
+ const siblings = (_a = item.getParent()) === null || _a === void 0 ? void 0 : _a.getChildren();
6
+ if (siblings) {
7
+ onChangeChildren(item.getParent(), siblings
8
+ .filter((sibling) => sibling.getId() !== item.getId())
9
+ .map((i) => i.getId()));
10
+ }
11
+ }
12
+ movedItems[0].getTree().rebuildTree();
13
+ };
@@ -1,9 +1,6 @@
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
  export type NoInfer<T> = [T][T extends any ? 0 : never];
4
3
  export declare const memo: <D extends readonly any[], R>(fn: (...args_0: D) => R, deps: () => [...D]) => () => R;
5
4
  export declare function functionalUpdate<T>(updater: Updater<T>, input: T): T;
6
5
  export declare function makeStateUpdater<K extends keyof TreeState<any>>(key: K, instance: unknown): (updater: Updater<TreeState<any>[K]>) => void;
7
- export declare const scrollIntoView: (element: Element | undefined | null) => void;
8
- export declare const performItemsMove: <T>(items: ItemInstance<T>[], target: DropTarget<T>, onChangeChildren: (item: ItemInstance<T>, newChildren: ItemInstance<T>[]) => void) => void;
9
6
  export declare const poll: (fn: () => boolean, interval?: number, timeout?: number) => Promise<void>;
package/lib/esm/utils.js CHANGED
@@ -31,56 +31,6 @@ export function makeStateUpdater(key, instance) {
31
31
  });
32
32
  };
33
33
  }
34
- export const scrollIntoView = (element) => {
35
- if (!element) {
36
- return;
37
- }
38
- if (element.scrollIntoViewIfNeeded) {
39
- element.scrollIntoViewIfNeeded();
40
- }
41
- else {
42
- const boundingBox = element.getBoundingClientRect();
43
- const isElementInViewport = boundingBox.top >= 0 &&
44
- boundingBox.left >= 0 &&
45
- boundingBox.bottom <=
46
- (window.innerHeight || document.documentElement.clientHeight) &&
47
- boundingBox.right <=
48
- (window.innerWidth || document.documentElement.clientWidth);
49
- if (!isElementInViewport) {
50
- element.scrollIntoView();
51
- }
52
- }
53
- };
54
- export const performItemsMove = (items, target, onChangeChildren) => {
55
- var _a;
56
- const numberOfDragItemsBeforeTarget = !target.childIndex
57
- ? 0
58
- : target.item
59
- .getChildren()
60
- .slice(0, target.childIndex)
61
- .filter((child) => items.some((item) => item.getId() === child.getId()))
62
- .length;
63
- // TODO bulk sibling changes together
64
- for (const item of items) {
65
- const siblings = (_a = item.getParent()) === null || _a === void 0 ? void 0 : _a.getChildren();
66
- if (siblings) {
67
- onChangeChildren(item.getParent(), siblings.filter((sibling) => sibling.getId() !== item.getId()));
68
- }
69
- }
70
- if (target.childIndex === null) {
71
- onChangeChildren(target.item, [...target.item.getChildren(), ...items]);
72
- items[0].getTree().rebuildTree();
73
- return;
74
- }
75
- const oldChildren = target.item.getChildren();
76
- const newChildren = [
77
- ...oldChildren.slice(0, target.childIndex - numberOfDragItemsBeforeTarget),
78
- ...items,
79
- ...oldChildren.slice(target.childIndex - numberOfDragItemsBeforeTarget),
80
- ];
81
- onChangeChildren(target.item, newChildren);
82
- items[0].getTree().rebuildTree();
83
- };
84
34
  export const poll = (fn, interval = 100, timeout = 1000) => new Promise((resolve) => {
85
35
  let clear;
86
36
  const i = setInterval(() => {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "main": "lib/cjs/index.js",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/esm/index.d.ts",
7
7
  "sideEffects": false,
8
8
  "scripts": {
9
- "build:cjs": "tsc",
10
- "build:esm": "tsc -m es2015 --outDir lib/esm",
9
+ "build:cjs": "tsc -m commonjs --outDir lib/cjs",
10
+ "build:esm": "tsc",
11
11
  "start": "tsc -w"
12
12
  },
13
13
  "repository": {
@@ -38,12 +38,16 @@ export const createTree = <T>(
38
38
  const additionalFeatures = [treeFeature, ...(initialConfig.features ?? [])];
39
39
  let state = additionalFeatures.reduce(
40
40
  (acc, feature) => feature.getInitialState?.(acc, treeInstance) ?? acc,
41
- initialConfig.state ?? {}
41
+ initialConfig.initialState ?? initialConfig.state ?? {}
42
42
  ) as TreeState<T>;
43
43
  let config = additionalFeatures.reduce(
44
44
  (acc, feature) => feature.getDefaultConfig?.(acc, treeInstance) ?? acc,
45
45
  initialConfig
46
46
  ) as TreeConfig<T>;
47
+ const stateHandlerNames = additionalFeatures.reduce(
48
+ (acc, feature) => ({ ...acc, ...feature.stateHandlerNames }),
49
+ {} as Record<string, string>
50
+ );
47
51
 
48
52
  let treeElement: HTMLElement | undefined | null;
49
53
  const treeDataRef: { current: any } = { current: {} };
@@ -109,10 +113,14 @@ export const createTree = <T>(
109
113
  ...prev,
110
114
  getState: () => state,
111
115
  setState: (updater) => {
112
- state = typeof updater === "function" ? updater(state) : updater;
116
+ // Not necessary, since I think the subupdate below keeps the state fresh anyways?
117
+ // state = typeof updater === "function" ? updater(state) : updater;
113
118
  config.setState?.(state);
114
- eachFeature((feature) => feature.setState?.(treeInstance));
115
- eachFeature((feature) => feature.onStateOrConfigChange?.(treeInstance));
119
+ },
120
+ applySubStateUpdate: (stateName, updater) => {
121
+ state[stateName] =
122
+ typeof updater === "function" ? updater(state[stateName]) : updater;
123
+ config[stateHandlerNames[stateName]]!(state[stateName]);
116
124
  },
117
125
  rebuildTree: () => {
118
126
  rebuildItemMeta(mainFeature);
@@ -124,11 +132,7 @@ export const createTree = <T>(
124
132
 
125
133
  if (config.state) {
126
134
  state = { ...state, ...config.state };
127
- eachFeature((feature) => feature.setState?.(treeInstance));
128
135
  }
129
-
130
- eachFeature((feature) => feature.onConfigChange?.(treeInstance));
131
- eachFeature((feature) => feature.onStateOrConfigChange?.(treeInstance));
132
136
  },
133
137
  getItemInstance: (itemId) => itemInstancesMap[itemId],
134
138
  getItems: () => itemInstances,
@@ -22,6 +22,10 @@ export const asyncDataLoaderFeature: FeatureImplementation<
22
22
  ...defaultConfig,
23
23
  }),
24
24
 
25
+ stateHandlerNames: {
26
+ loadingItems: "setLoadingItems",
27
+ },
28
+
25
29
  createTreeInstance: (prev, instance) => ({
26
30
  ...prev,
27
31
 
@@ -36,11 +40,14 @@ export const asyncDataLoaderFeature: FeatureImplementation<
36
40
  }
37
41
 
38
42
  if (!instance.getState().loadingItems.includes(itemId)) {
39
- config.setLoadingItems?.((loadingItems) => [...loadingItems, itemId]);
43
+ instance.applySubStateUpdate("loadingItems", (loadingItems) => [
44
+ ...loadingItems,
45
+ itemId,
46
+ ]);
40
47
  config.asyncDataLoader?.getItem(itemId).then((item) => {
41
48
  dataRef.current.itemData[itemId] = item;
42
49
  config.onLoadedItem?.(itemId, item);
43
- config.setLoadingItems?.((loadingItems) =>
50
+ instance.applySubStateUpdate("loadingItems", (loadingItems) =>
44
51
  loadingItems.filter((id) => id !== itemId)
45
52
  );
46
53
  });
@@ -62,7 +69,10 @@ export const asyncDataLoaderFeature: FeatureImplementation<
62
69
  return [];
63
70
  }
64
71
 
65
- config.setLoadingItems?.((loadingItems) => [...loadingItems, itemId]);
72
+ instance.applySubStateUpdate("loadingItems", (loadingItems) => [
73
+ ...loadingItems,
74
+ itemId,
75
+ ]);
66
76
 
67
77
  if (config.asyncDataLoader?.getChildrenWithData) {
68
78
  config.asyncDataLoader?.getChildrenWithData(itemId).then((children) => {
@@ -73,7 +83,7 @@ export const asyncDataLoaderFeature: FeatureImplementation<
73
83
  const childrenIds = children.map(({ id }) => id);
74
84
  dataRef.current.childrenIds[itemId] = childrenIds;
75
85
  config.onLoadedChildren?.(itemId, childrenIds);
76
- config.setLoadingItems?.((loadingItems) =>
86
+ instance.applySubStateUpdate("loadingItems", (loadingItems) =>
77
87
  loadingItems.filter((id) => id !== itemId)
78
88
  );
79
89
  instance.rebuildTree();
@@ -82,7 +92,7 @@ export const asyncDataLoaderFeature: FeatureImplementation<
82
92
  config.asyncDataLoader?.getChildren(itemId).then((childrenIds) => {
83
93
  dataRef.current.childrenIds[itemId] = childrenIds;
84
94
  config.onLoadedChildren?.(itemId, childrenIds);
85
- config.setLoadingItems?.((loadingItems) =>
95
+ instance.applySubStateUpdate("loadingItems", (loadingItems) =>
86
96
  loadingItems.filter((id) => id !== itemId)
87
97
  );
88
98
  instance.rebuildTree();
@@ -13,10 +13,15 @@ export const dragAndDropFeature: FeatureImplementation<
13
13
 
14
14
  getDefaultConfig: (defaultConfig, tree) => ({
15
15
  canDrop: (_, target) => target.item.isFolder(),
16
+ canDropForeignDragObject: () => false,
16
17
  setDndState: makeStateUpdater("dnd", tree),
17
18
  ...defaultConfig,
18
19
  }),
19
20
 
21
+ stateHandlerNames: {
22
+ dnd: "setDndState",
23
+ },
24
+
20
25
  createTreeInstance: (prev, tree) => ({
21
26
  ...prev,
22
27
 
@@ -33,7 +38,7 @@ export const dragAndDropFeature: FeatureImplementation<
33
38
 
34
39
  draggable: tree.getConfig().isItemDraggable?.(item) ?? true,
35
40
 
36
- onDragStart: (e) => {
41
+ onDragStart: item.getMemoizedProp("dnd/onDragStart", () => (e) => {
37
42
  const selectedItems = tree.getSelectedItems();
38
43
  const items = selectedItems.includes(item) ? selectedItems : [item];
39
44
  const config = tree.getConfig();
@@ -52,13 +57,13 @@ export const dragAndDropFeature: FeatureImplementation<
52
57
  e.dataTransfer?.setData(format, data);
53
58
  }
54
59
 
55
- tree.getConfig().setDndState?.({
60
+ tree.applySubStateUpdate("dnd", {
56
61
  draggedItems: items,
57
62
  draggingOverItem: tree.getFocusedItem(),
58
63
  });
59
- },
64
+ }),
60
65
 
61
- onDragOver: (e) => {
66
+ onDragOver: item.getMemoizedProp("dnd/onDragOver", () => (e) => {
62
67
  const target = getDropTarget(e, item, tree);
63
68
  const dataRef = tree.getDataRef<DndDataRef>();
64
69
 
@@ -82,24 +87,35 @@ export const dragAndDropFeature: FeatureImplementation<
82
87
 
83
88
  dataRef.current.lastDragCode = nextDragCode;
84
89
 
85
- tree.getConfig().setDndState?.((state) => ({
90
+ tree.applySubStateUpdate("dnd", (state) => ({
86
91
  ...state,
87
92
  dragTarget: target,
88
93
  draggingOverItem: item,
89
94
  }));
90
- },
95
+ }),
91
96
 
92
- onDragLeave: () => {
97
+ onDragLeave: item.getMemoizedProp("dnd/onDragLeave", () => () => {
93
98
  const dataRef = tree.getDataRef<DndDataRef>();
94
99
  dataRef.current.lastDragCode = "no-drag";
95
- tree.getConfig().setDndState?.((state) => ({
100
+ tree.applySubStateUpdate("dnd", (state) => ({
96
101
  ...state,
97
102
  draggingOverItem: undefined,
98
103
  dragTarget: undefined,
99
104
  }));
100
- },
105
+ }),
106
+
107
+ onDragEnd: item.getMemoizedProp("dnd/onDragEnd", () => (e) => {
108
+ const draggedItems = tree.getState().dnd?.draggedItems;
109
+ tree.applySubStateUpdate("dnd", null);
101
110
 
102
- onDrop: (e) => {
111
+ if (e.dataTransfer.dropEffect === "none" || !draggedItems) {
112
+ return;
113
+ }
114
+
115
+ tree.getConfig().onCompleteForeignDrop?.(draggedItems);
116
+ }),
117
+
118
+ onDrop: item.getMemoizedProp("dnd/onDrop", () => (e) => {
103
119
  const dataRef = tree.getDataRef<DndDataRef>();
104
120
  const target = getDropTarget(e, item, tree);
105
121
 
@@ -112,7 +128,7 @@ export const dragAndDropFeature: FeatureImplementation<
112
128
  const draggedItems = tree.getState().dnd?.draggedItems;
113
129
 
114
130
  dataRef.current.lastDragCode = undefined;
115
- tree.getConfig().setDndState?.(null);
131
+ tree.applySubStateUpdate("dnd", null);
116
132
 
117
133
  if (draggedItems) {
118
134
  config.onDrop?.(draggedItems, target);
@@ -120,7 +136,7 @@ export const dragAndDropFeature: FeatureImplementation<
120
136
  config.onDropForeignDragObject?.(e.dataTransfer, target);
121
137
  }
122
138
  // TODO rebuild tree?
123
- },
139
+ }),
124
140
  }),
125
141
 
126
142
  isDropTarget: () => {
@@ -131,19 +147,25 @@ export const dragAndDropFeature: FeatureImplementation<
131
147
  isDropTargetAbove: () => {
132
148
  const target = tree.getDropTarget();
133
149
 
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;
150
+ if (
151
+ !target ||
152
+ target.childIndex === null ||
153
+ target.item !== item.getParent()
154
+ )
155
+ return false;
156
+ return target.childIndex === item.getItemMeta().posInSet;
138
157
  },
139
158
 
140
159
  isDropTargetBelow: () => {
141
160
  const target = tree.getDropTarget();
142
161
 
143
- if (!target || target.childIndex === null) return false;
144
- const targetIndex = target.item.getItemMeta().index;
145
-
146
- return targetIndex + target.childIndex === item.getItemMeta().index;
162
+ if (
163
+ !target ||
164
+ target.childIndex === null ||
165
+ target.item !== item.getParent()
166
+ )
167
+ return false;
168
+ return target.childIndex - 1 === item.getItemMeta().posInSet;
147
169
  },
148
170
 
149
171
  isDraggingOver: () => {
@@ -5,15 +5,22 @@ export type DndDataRef = {
5
5
  };
6
6
 
7
7
  export type DndState<T> = {
8
- draggedItems?: ItemInstance<T>[];
8
+ draggedItems?: ItemInstance<T>[]; // TODO not used anymore?
9
9
  draggingOverItem?: ItemInstance<T>;
10
10
  dragTarget?: DropTarget<T>;
11
11
  };
12
12
 
13
- export type DropTarget<T> = {
14
- item: ItemInstance<T>;
15
- childIndex: number | null;
16
- };
13
+ export type DropTarget<T> =
14
+ | {
15
+ item: ItemInstance<T>;
16
+ childIndex: number;
17
+ insertionIndex: number;
18
+ }
19
+ | {
20
+ item: ItemInstance<T>;
21
+ childIndex: null;
22
+ insertionIndex: null;
23
+ };
17
24
 
18
25
  export enum DropTargetPosition {
19
26
  Top = "top",
@@ -44,12 +51,22 @@ export type DragAndDropFeatureDef<T> = {
44
51
  dataTransfer: DataTransfer,
45
52
  target: DropTarget<T>
46
53
  ) => boolean;
47
-
48
54
  onDrop?: (items: ItemInstance<T>[], target: DropTarget<T>) => void;
49
55
  onDropForeignDragObject?: (
50
56
  dataTransfer: DataTransfer,
51
57
  target: DropTarget<T>
52
58
  ) => void;
59
+
60
+ /** Runs in the onDragEnd event, if `ev.dataTransfer.dropEffect` is not `none`, i.e. the drop
61
+ * was not aborted. No target is provided as parameter since the target may be a foreign drop target.
62
+ * This is useful to seperate out the logic to move dragged items out of their previous parents.
63
+ * Use `onDrop` to handle drop-related logic.
64
+ *
65
+ * This ignores the `canDrop` handler, since the drop target is unknown in this handler.
66
+ */
67
+ // onSuccessfulDragEnd?: (items: ItemInstance<T>[]) => void;
68
+
69
+ onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
53
70
  };
54
71
  treeInstance: {
55
72
  getDropTarget: () => DropTarget<T> | null;
@@ -1,5 +1,10 @@
1
1
  import { ItemInstance, TreeInstance } from "../../types/core";
2
- import { DropTarget, DropTargetPosition } from "./types";
2
+ import {
3
+ DndState,
4
+ DragAndDropFeatureDef,
5
+ DropTarget,
6
+ DropTargetPosition,
7
+ } from "./types";
3
8
 
4
9
  export const getDragCode = ({ item, childIndex }: DropTarget<any>) =>
5
10
  `${item.getId()}__${childIndex ?? "none"}`;
@@ -49,40 +54,64 @@ const getDropTargetPosition = (
49
54
  export const getDropTarget = (
50
55
  e: any,
51
56
  item: ItemInstance<any>,
52
- tree: TreeInstance<any>
57
+ tree: TreeInstance<any>,
58
+ canDropInbetween = tree.getConfig().canDropInbetween
53
59
  ): DropTarget<any> => {
54
60
  const config = tree.getConfig();
55
- const offset = getDropOffset(e, item);
61
+ const draggedItems = tree.getState().dnd?.draggedItems ?? [];
62
+ const itemTarget = { item, childIndex: null, insertionIndex: null };
63
+ const parentTarget = {
64
+ item: item.getParent(),
65
+ childIndex: null,
66
+ insertionIndex: null,
67
+ };
56
68
 
57
- const dropOnItemTarget = { item, childIndex: null };
69
+ if (!canDropInbetween) {
70
+ if (!canDrop(e.dataTransfer, parentTarget, tree)) {
71
+ return getDropTarget(e, item.getParent(), tree, false);
72
+ }
73
+ return itemTarget;
74
+ }
58
75
 
59
- const pos = getDropTargetPosition(
60
- offset,
61
- config.topLinePercentage ?? 0.3,
62
- config.bottomLinePercentage ?? 0.7
63
- );
64
- const inbetweenPos = getDropTargetPosition(offset, 0.5, 0.5);
76
+ const canDropInside = canDrop(e.dataTransfer, itemTarget, tree);
65
77
 
66
- if (!config.canDropInbetween) {
67
- return dropOnItemTarget;
68
- }
78
+ const offset = getDropOffset(e, item);
69
79
 
70
- if (!canDrop(e.dataTransfer, dropOnItemTarget, tree)) {
71
- return {
72
- item: item.getParent(),
73
- childIndex:
74
- item.getIndexInParent() +
75
- (inbetweenPos === DropTargetPosition.Top ? 0 : 1),
76
- };
77
- }
80
+ const pos = canDropInside
81
+ ? getDropTargetPosition(
82
+ offset,
83
+ config.topLinePercentage ?? 0.3,
84
+ config.bottomLinePercentage ?? 0.7
85
+ )
86
+ : getDropTargetPosition(offset, 0.5, 0.5);
78
87
 
79
88
  if (pos === DropTargetPosition.Item) {
80
- return dropOnItemTarget;
89
+ return itemTarget;
90
+ }
91
+
92
+ if (!canDrop(e.dataTransfer, parentTarget, tree)) {
93
+ return getDropTarget(e, item.getParent(), tree, false);
81
94
  }
82
95
 
96
+ const childIndex =
97
+ item.getIndexInParent() + (pos === DropTargetPosition.Top ? 0 : 1);
98
+
99
+ const numberOfDragItemsBeforeTarget = item
100
+ .getParent()
101
+ .getChildren()
102
+ .slice(0, childIndex)
103
+ .reduce(
104
+ (counter, child) =>
105
+ child && draggedItems?.some((i) => i.getId() === child.getId())
106
+ ? ++counter
107
+ : counter,
108
+ 0
109
+ );
110
+
83
111
  return {
84
112
  item: item.getParent(),
85
- childIndex:
86
- item.getIndexInParent() + (pos === DropTargetPosition.Top ? 0 : 1),
113
+ childIndex,
114
+ // TODO performance could be improved by computing this only when dragcode changed
115
+ insertionIndex: childIndex - numberOfDragItemsBeforeTarget,
87
116
  };
88
117
  };
@@ -25,8 +25,9 @@ export const expandAllFeature: FeatureImplementation<
25
25
  );
26
26
  },
27
27
 
28
- collapseAll: async () => {
29
- tree.getConfig().setExpandedItems?.([]);
28
+ collapseAll: () => {
29
+ tree.applySubStateUpdate("expandedItems", []);
30
+ tree.rebuildTree();
30
31
  },
31
32
  }),
32
33
 
@@ -37,6 +38,9 @@ export const expandAllFeature: FeatureImplementation<
37
38
  if (cancelToken?.current) {
38
39
  return;
39
40
  }
41
+ if (!item.isFolder()) {
42
+ return;
43
+ }
40
44
 
41
45
  item.expand();
42
46
  await poll(() => !tree.getState().loadingItems.includes(item.getId()));
@@ -50,12 +54,10 @@ export const expandAllFeature: FeatureImplementation<
50
54
  );
51
55
  },
52
56
 
53
- collapseAll: async () => {
54
- await Promise.all(
55
- item.getChildren().map(async (child) => {
56
- await child?.collapseAll();
57
- })
58
- );
57
+ collapseAll: () => {
58
+ for (const child of item.getChildren()) {
59
+ child?.collapseAll();
60
+ }
59
61
  item.collapse();
60
62
  },
61
63
  }),
@@ -3,11 +3,11 @@ export type ExpandAllFeatureDef = {
3
3
  config: {};
4
4
  treeInstance: {
5
5
  expandAll: (cancelToken?: { current: boolean }) => Promise<void>;
6
- collapseAll: () => Promise<void>;
6
+ collapseAll: () => void;
7
7
  };
8
8
  itemInstance: {
9
9
  expandAll: (cancelToken?: { current: boolean }) => Promise<void>;
10
- collapseAll: () => Promise<void>;
10
+ collapseAll: () => void;
11
11
  };
12
12
  hotkeys: never;
13
13
  };