@headless-tree/core 1.3.0 → 1.5.0

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.
@@ -1,17 +1,51 @@
1
- import { FeatureImplementation } from "../../types/core";
1
+ import {
2
+ FeatureImplementation,
3
+ type ItemInstance,
4
+ type TreeInstance,
5
+ } from "../../types/core";
2
6
  import { DndDataRef, DragLineData, DragTarget } from "./types";
3
7
  import {
8
+ PlacementType,
9
+ type TargetPlacement,
4
10
  canDrop,
5
11
  getDragCode,
6
12
  getDragTarget,
13
+ getTargetPlacement,
7
14
  isOrderedDragTarget,
8
15
  } from "./utils";
9
16
  import { makeStateUpdater } from "../../utils";
10
17
 
18
+ const handleAutoOpenFolder = (
19
+ dataRef: { current: DndDataRef },
20
+ tree: TreeInstance<any>,
21
+ item: ItemInstance<any>,
22
+ placement: TargetPlacement,
23
+ ) => {
24
+ const { openOnDropDelay } = tree.getConfig();
25
+ const dragCode = dataRef.current.lastDragCode;
26
+
27
+ if (
28
+ !openOnDropDelay ||
29
+ !item.isFolder() ||
30
+ item.isExpanded() ||
31
+ placement.type !== PlacementType.MakeChild
32
+ ) {
33
+ return;
34
+ }
35
+ clearTimeout(dataRef.current.autoExpandTimeout);
36
+ dataRef.current.autoExpandTimeout = setTimeout(() => {
37
+ if (
38
+ dragCode !== dataRef.current.lastDragCode ||
39
+ !dataRef.current.lastAllowDrop
40
+ )
41
+ return;
42
+ item.expand();
43
+ }, openOnDropDelay);
44
+ };
45
+
11
46
  const defaultCanDropForeignDragObject = () => false;
12
47
  export const dragAndDropFeature: FeatureImplementation = {
13
48
  key: "drag-and-drop",
14
- deps: ["selection"],
15
49
 
16
50
  getDefaultConfig: (defaultConfig, tree) => ({
17
51
  canDrop: (_, target) => target.item.isFolder(),
@@ -22,6 +56,7 @@ export const dragAndDropFeature: FeatureImplementation = {
22
56
  : () => false,
23
57
  setDndState: makeStateUpdater("dnd", tree),
24
58
  canReorder: true,
59
+ openOnDropDelay: 800,
25
60
  ...defaultConfig,
26
61
  }),
27
62
 
@@ -74,7 +109,7 @@ export const dragAndDropFeature: FeatureImplementation = {
74
109
  }
75
110
  }
76
111
 
77
- const bb = targetItem.getElement()?.getBoundingClientRect();
112
+ const bb = targetItem?.getElement()?.getBoundingClientRect();
78
113
 
79
114
  if (bb) {
80
115
  return {
@@ -149,12 +184,14 @@ export const dragAndDropFeature: FeatureImplementation = {
149
184
  onDragEnter: (e: DragEvent) => e.preventDefault(),
150
185
 
151
186
  onDragStart: (e: DragEvent) => {
152
- const selectedItems = tree.getSelectedItems();
187
+ const selectedItems = tree.getSelectedItems
188
+ ? tree.getSelectedItems()
189
+ : [tree.getFocusedItem()];
153
190
  const items = selectedItems.includes(item) ? selectedItems : [item];
154
191
  const config = tree.getConfig();
155
192
 
156
193
  if (!selectedItems.includes(item)) {
157
- tree.setSelectedItems([item.getItemMeta().itemId]);
194
+ tree.setSelectedItems?.([item.getItemMeta().itemId]);
158
195
  }
159
196
 
160
197
  if (!(config.canDrag?.(items) ?? true)) {
@@ -185,7 +222,9 @@ export const dragAndDropFeature: FeatureImplementation = {
185
222
  onDragOver: (e: DragEvent) => {
186
223
  e.stopPropagation(); // don't bubble up to container dragover
187
224
  const dataRef = tree.getDataRef<DndDataRef>();
188
- const nextDragCode = getDragCode(e, item, tree);
225
+ const placement = getTargetPlacement(e, item, tree, true);
226
+ const nextDragCode = getDragCode(item, placement);
227
+
189
228
  if (nextDragCode === dataRef.current.lastDragCode) {
190
229
  if (dataRef.current.lastAllowDrop) {
191
230
  e.preventDefault();
@@ -195,6 +234,8 @@ export const dragAndDropFeature: FeatureImplementation = {
195
234
  dataRef.current.lastDragCode = nextDragCode;
196
235
  dataRef.current.lastDragEnter = Date.now();
197
236
 
237
+ handleAutoOpenFolder(dataRef, tree, item, placement);
238
+
198
239
  const target = getDragTarget(e, item, tree);
199
240
 
200
241
  if (
@@ -260,14 +301,21 @@ export const dragAndDropFeature: FeatureImplementation = {
260
301
  e.stopPropagation();
261
302
  const dataRef = tree.getDataRef<DndDataRef>();
262
303
  const target = getDragTarget(e, item, tree);
304
+ const draggedItems = tree.getState().dnd?.draggedItems;
305
+ const isValidDrop = canDrop(e.dataTransfer, target, tree);
263
306
 
264
- if (!canDrop(e.dataTransfer, target, tree)) {
307
+ tree.applySubStateUpdate("dnd", {
308
+ draggedItems: undefined,
309
+ draggingOverItem: undefined,
310
+ dragTarget: undefined,
311
+ });
312
+
313
+ if (!isValidDrop) {
265
314
  return;
266
315
  }
267
316
 
268
317
  e.preventDefault();
269
318
  const config = tree.getConfig();
270
- const draggedItems = tree.getState().dnd?.draggedItems;
271
319
 
272
320
  dataRef.current.lastDragCode = undefined;
273
321
 
@@ -284,6 +332,13 @@ export const dragAndDropFeature: FeatureImplementation = {
284
332
  return target ? target.item.getId() === item.getId() : false;
285
333
  },
286
334
 
335
+ isUnorderedDragTarget: ({ tree, item }) => {
336
+ const target = tree.getDragTarget();
337
+ return target
338
+ ? !isOrderedDragTarget(target) && target.item.getId() === item.getId()
339
+ : false;
340
+ },
341
+
287
342
  isDragTargetAbove: ({ tree, item }) => {
288
343
  const target = tree.getDragTarget();
289
344
 
@@ -4,6 +4,7 @@ export interface DndDataRef {
4
4
  lastDragCode?: string;
5
5
  lastAllowDrop?: boolean;
6
6
  lastDragEnter?: number;
7
+ autoExpandTimeout?: any;
7
8
  windowDragEndListener?: () => void;
8
9
  }
9
10
 
@@ -93,6 +94,9 @@ export type DragAndDropFeatureDef<T> = {
93
94
  target: DragTarget<T>,
94
95
  ) => void | Promise<void>;
95
96
  onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
97
+
98
+ /** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
99
+ openOnDropDelay?: number;
96
100
  };
97
101
  treeInstance: {
98
102
  getDragTarget: () => DragTarget<T> | null;
@@ -104,7 +108,13 @@ export type DragAndDropFeatureDef<T> = {
104
108
  ) => Record<string, any>;
105
109
  };
106
110
  itemInstance: {
111
+ /** Checks if the user is dragging in a way which makes this the new parent of the dragged items, either by dragging on top of
112
+ * this item, or by dragging inbetween children of this item. See @{isUnorderedDragTarget} if the latter is undesirable. */
107
113
  isDragTarget: () => boolean;
114
+
115
+ /** As opposed to @{isDragTarget}, this will not be true if the target is inbetween children of this item. This returns only true
116
+ * if the user is dragging directly on top of this item. */
117
+ isUnorderedDragTarget: () => boolean;
108
118
  isDragTargetAbove: () => boolean;
109
119
  isDragTargetBelow: () => boolean;
110
120
  isDraggingOver: () => boolean;
@@ -7,14 +7,14 @@ export enum ItemDropCategory {
7
7
  LastInGroup,
8
8
  }
9
9
 
10
- enum PlacementType {
10
+ export enum PlacementType {
11
11
  ReorderAbove,
12
12
  ReorderBelow,
13
13
  MakeChild,
14
14
  Reparent,
15
15
  }
16
16
 
17
- type TargetPlacement =
17
+ export type TargetPlacement =
18
18
  | {
19
19
  type:
20
20
  | PlacementType.ReorderAbove
@@ -95,7 +95,7 @@ export const getInsertionIndex = <T>(
95
95
  return childIndex - numberOfDragItemsBeforeTarget;
96
96
  };
97
97
 
98
- const getTargetPlacement = (
98
+ export const getTargetPlacement = (
99
99
  e: any,
100
100
  item: ItemInstance<any>,
101
101
  tree: TreeInstance<any>,
@@ -153,11 +153,9 @@ const getTargetPlacement = (
153
153
  };
154
154
 
155
155
  export const getDragCode = (
156
- e: any,
157
156
  item: ItemInstance<any>,
158
- tree: TreeInstance<any>,
157
+ placement: TargetPlacement,
159
158
  ) => {
160
- const placement = getTargetPlacement(e, item, tree, true);
161
159
  return [
162
160
  item.getId(),
163
161
  placement.type,
@@ -222,6 +220,10 @@ export const getDragTarget = (
222
220
  canBecomeSibling &&
223
221
  placement.type !== PlacementType.MakeChild
224
222
  ) {
223
+ if (draggedItems?.some((item) => item.isDescendentOf(parent.getId()))) {
224
+ // dropping on itself should be illegal, return item, canDrop will then return false
225
+ return itemTarget;
226
+ }
225
227
  return parentTarget;
226
228
  }
227
229
 
@@ -13,6 +13,7 @@ const specialKeys: Record<string, RegExp> = {
13
13
  minus: /^(NumpadSubtract|Minus)$/,
14
14
  control: /^(ControlLeft|ControlRight)$/,
15
15
  shift: /^(ShiftLeft|ShiftRight)$/,
16
+ metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
16
17
  };
17
18
 
18
19
  const testHotkeyMatch = (
@@ -191,7 +191,9 @@ export const keyboardDragAndDropFeature: FeatureImplementation = {
191
191
  preventDefault: true,
192
192
  isEnabled: (tree) => !tree.getState().dnd,
193
193
  handler: (_, tree) => {
194
- const selectedItems = tree.getSelectedItems();
194
+ const selectedItems = tree.getSelectedItems?.() ?? [
195
+ tree.getFocusedItem(),
196
+ ];
195
197
  const focusedItem = tree.getFocusedItem();
196
198
 
197
199
  tree.startKeyboardDrag(
@@ -10,6 +10,11 @@ import {
10
10
  } from "../../types/core";
11
11
  import { ItemMeta } from "../tree/types";
12
12
 
13
+ export interface TreeDataRef {
14
+ isMounted?: boolean;
15
+ waitingForMount?: (() => void)[];
16
+ }
17
+
13
18
  export type InstanceTypeMap = {
14
19
  itemInstance: ItemInstance<any>;
15
20
  treeInstance: TreeInstance<any>;
@@ -49,6 +54,10 @@ export type MainFeatureDef<T = any> = {
49
54
  /* @internal */
50
55
  getHotkeyPresets: () => HotkeysConfig<T>;
51
56
  rebuildTree: () => void;
57
+ /** @deprecated Experimental feature, might get removed or changed in the future. */
58
+ scheduleRebuildTree: () => void;
59
+ /** @internal */
60
+ setMounted: (isMounted: boolean) => void;
52
61
  };
53
62
  itemInstance: {
54
63
  registerElement: (element: HTMLElement | null) => void;
@@ -18,7 +18,13 @@ export type SyncDataLoaderFeatureDef<T> = {
18
18
  };
19
19
  treeInstance: {
20
20
  retrieveItemData: (itemId: string) => T;
21
- retrieveChildrenIds: (itemId: string) => string[];
21
+
22
+ /** Retrieve children Ids. If an async data loader is used, skipFetch is set to true, and children have not been retrieved
23
+ * yet for this item, this will initiate fetching the children, and return an empty array. Once the children have loaded,
24
+ * a rerender will be triggered.
25
+ * @param skipFetch - Defaults to false.
26
+ */
27
+ retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
22
28
  };
23
29
  itemInstance: {
24
30
  isLoading: () => boolean;
@@ -10,6 +10,7 @@ import { SyncDataLoaderFeatureDef } from "./features/sync-data-loader/types";
10
10
  import { TreeFeatureDef } from "./features/tree/types";
11
11
  import { PropMemoizationFeatureDef } from "./features/prop-memoization/types";
12
12
  import { KeyboardDragAndDropFeatureDef } from "./features/keyboard-drag-and-drop/types";
13
+ import type { CheckboxesFeatureDef } from "./features/checkboxes/types";
13
14
 
14
15
  export * from ".";
15
16
 
@@ -167,3 +168,15 @@ export type TreeFeatureTreeInstance<T> = TreeFeatureDef<T>["treeInstance"];
167
168
  /** @interface */
168
169
  export type TreeFeatureItemInstance<T> = TreeFeatureDef<T>["itemInstance"];
169
170
  export type TreeFeatureHotkeys<T> = TreeFeatureDef<T>["hotkeys"];
171
+
172
+ /** @interface */
173
+ export type CheckboxesFeatureConfig<T> = CheckboxesFeatureDef<T>["config"];
174
+ /** @interface */
175
+ export type CheckboxesFeatureState<T> = CheckboxesFeatureDef<T>["state"];
176
+ /** @interface */
177
+ export type CheckboxesFeatureTreeInstance<T> =
178
+ CheckboxesFeatureDef<T>["treeInstance"];
179
+ /** @interface */
180
+ export type CheckboxesFeatureItemInstance<T> =
181
+ CheckboxesFeatureDef<T>["itemInstance"];
182
+ export type CheckboxesFeatureHotkeys<T> = CheckboxesFeatureDef<T>["hotkeys"];
@@ -135,4 +135,10 @@ export class TestTreeDo<T> {
135
135
  "function called with inconsistent parameters",
136
136
  ).toBeOneOf([0, 1]);
137
137
  }
138
+
139
+ async awaitNextTick() {
140
+ await new Promise((r) => {
141
+ setTimeout(r);
142
+ });
143
+ }
138
144
  }
@@ -78,6 +78,7 @@ export class TestTree<T = string> {
78
78
  get instance() {
79
79
  if (!this.treeInstance) {
80
80
  this.treeInstance = createTree(this.config);
81
+ this.treeInstance.setMounted(true);
81
82
  this.treeInstance.rebuildTree();
82
83
  }
83
84
  return this.treeInstance;
@@ -87,10 +88,9 @@ export class TestTree<T = string> {
87
88
 
88
89
  static async resolveAsyncLoaders() {
89
90
  do {
91
+ await vi.advanceTimersToNextTimerAsync();
90
92
  TestTree.asyncLoaderResolvers.shift()?.();
91
- await new Promise<void>((r) => {
92
- setTimeout(r);
93
- });
93
+ await vi.advanceTimersToNextTimerAsync();
94
94
  } while (TestTree.asyncLoaderResolvers.length);
95
95
  }
96
96
 
@@ -101,6 +101,17 @@ export class TestTree<T = string> {
101
101
  await TestTree.resolveAsyncLoaders();
102
102
  }
103
103
 
104
+ async runWhileResolvingItems(cb: () => Promise<void>) {
105
+ const interval = setInterval(() => {
106
+ TestTree.resolveAsyncLoaders();
107
+ }, 5);
108
+ try {
109
+ await cb();
110
+ } finally {
111
+ clearInterval(interval);
112
+ }
113
+ }
114
+
104
115
  static default(config: Partial<TreeConfig<string>>) {
105
116
  return new TestTree({
106
117
  rootItemId: "x",
package/src/types/core.ts CHANGED
@@ -93,11 +93,11 @@ type MayReturnNull<T extends (...x: any[]) => any> = (
93
93
  ...args: Parameters<T>
94
94
  ) => ReturnType<T> | null;
95
95
 
96
- export type ItemInstanceOpts<Key extends keyof ItemInstance<any>> = {
97
- item: ItemInstance<any>;
98
- tree: TreeInstance<any>;
96
+ export type ItemInstanceOpts<T, Key extends keyof ItemInstance<any>> = {
97
+ item: ItemInstance<T>;
98
+ tree: TreeInstance<T>;
99
99
  itemId: string;
100
- prev?: MayReturnNull<ItemInstance<any>[Key]>;
100
+ prev?: MayReturnNull<ItemInstance<T>[Key]>;
101
101
  };
102
102
 
103
103
  export type TreeInstanceOpts<Key extends keyof TreeInstance<any>> = {
@@ -131,7 +131,7 @@ export type FeatureImplementation<T = any> = {
131
131
 
132
132
  itemInstance?: {
133
133
  [key in keyof ItemInstance<T>]?: (
134
- opts: ItemInstanceOpts<key>,
134
+ opts: ItemInstanceOpts<T, key>,
135
135
  ...args: Parameters<ItemInstance<T>[key]>
136
136
  ) => void;
137
137
  };