@headless-tree/core 0.0.5 → 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 (90) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/cjs/core/create-tree.js +7 -6
  3. package/lib/cjs/features/async-data-loader/feature.js +30 -21
  4. package/lib/cjs/features/drag-and-drop/feature.js +29 -17
  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 +3 -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 +5 -3
  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 +23 -23
  18. package/lib/cjs/features/tree/types.d.ts +2 -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 +7 -6
  31. package/lib/esm/features/async-data-loader/feature.js +30 -21
  32. package/lib/esm/features/drag-and-drop/feature.js +29 -17
  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 +3 -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 +5 -3
  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 +23 -23
  46. package/lib/esm/features/tree/types.d.ts +2 -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 +1 -1
  59. package/src/core/create-tree.ts +11 -7
  60. package/src/features/async-data-loader/feature.ts +15 -5
  61. package/src/features/drag-and-drop/feature.ts +34 -12
  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 +6 -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 +6 -1
  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 +23 -21
  74. package/src/features/tree/types.ts +4 -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/lib/cjs/data-adapters/nested-data-adapter.d.ts +0 -9
  82. package/lib/cjs/data-adapters/nested-data-adapter.js +0 -32
  83. package/lib/cjs/data-adapters/types.d.ts +0 -7
  84. package/lib/cjs/data-adapters/types.js +0 -2
  85. package/lib/esm/data-adapters/nested-data-adapter.d.ts +0 -9
  86. package/lib/esm/data-adapters/nested-data-adapter.js +0 -28
  87. package/lib/esm/data-adapters/types.d.ts +0 -7
  88. package/lib/esm/data-adapters/types.js +0 -1
  89. package/src/data-adapters/nested-data-adapter.ts +0 -48
  90. package/src/data-adapters/types.ts +0 -9
@@ -44,6 +44,10 @@ export const createTree = <T>(
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
 
@@ -52,7 +57,7 @@ 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
  });
@@ -82,7 +87,7 @@ 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,
@@ -92,13 +97,24 @@ export const dragAndDropFeature: FeatureImplementation<
92
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
  }),
101
106
 
107
+ onDragEnd: item.getMemoizedProp("dnd/onDragEnd", () => (e) => {
108
+ const draggedItems = tree.getState().dnd?.draggedItems;
109
+ tree.applySubStateUpdate("dnd", null);
110
+
111
+ if (e.dataTransfer.dropEffect === "none" || !draggedItems) {
112
+ return;
113
+ }
114
+
115
+ tree.getConfig().onCompleteForeignDrop?.(draggedItems);
116
+ }),
117
+
102
118
  onDrop: item.getMemoizedProp("dnd/onDrop", () => (e) => {
103
119
  const dataRef = tree.getDataRef<DndDataRef>();
104
120
  const target = getDropTarget(e, item, tree);
@@ -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);
@@ -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
  };
@@ -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
 
@@ -17,6 +18,11 @@ export type MainFeatureDef<T = any> = {
17
18
  setState?: SetStateFn<TreeState<T>>;
18
19
  };
19
20
  treeInstance: {
21
+ /** @internal */
22
+ applySubStateUpdate: <K extends keyof TreeState<any>>(
23
+ stateName: K,
24
+ updater: Updater<TreeState<T>[K]>
25
+ ) => void;
20
26
  setState: SetStateFn<TreeState<T>>;
21
27
  getState: () => TreeState<T>;
22
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,6 +95,7 @@ export const selectionFeature: FeatureImplementation<
91
95
 
92
96
  getProps: () => ({
93
97
  ...prev.getProps(),
98
+ "aria-selected": item.isSelected() ? "true" : "false",
94
99
  onClick: item.getMemoizedProp("selection/onClick", () => (e) => {
95
100
  if (e.shiftKey) {
96
101
  item.selectUpTo(e.ctrlKey || e.metaKey);
@@ -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;