@headless-tree/core 0.0.10 → 0.0.12
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.
- package/CHANGELOG.md +12 -0
- package/lib/cjs/core/build-proxified-instance.d.ts +2 -0
- package/lib/cjs/core/build-proxified-instance.js +58 -0
- package/lib/cjs/core/build-static-instance.d.ts +2 -0
- package/lib/cjs/core/build-static-instance.js +26 -0
- package/lib/cjs/core/create-tree.js +62 -40
- package/lib/cjs/features/async-data-loader/feature.d.ts +1 -4
- package/lib/cjs/features/async-data-loader/feature.js +35 -23
- package/lib/cjs/features/async-data-loader/types.d.ts +4 -6
- package/lib/cjs/features/drag-and-drop/feature.d.ts +2 -3
- package/lib/cjs/features/drag-and-drop/feature.js +79 -44
- package/lib/cjs/features/drag-and-drop/types.d.ts +15 -6
- package/lib/cjs/features/drag-and-drop/utils.d.ts +2 -3
- package/lib/cjs/features/drag-and-drop/utils.js +140 -37
- package/lib/cjs/features/expand-all/feature.d.ts +1 -5
- package/lib/cjs/features/expand-all/feature.js +12 -6
- package/lib/cjs/features/hotkeys-core/feature.d.ts +1 -3
- package/lib/cjs/features/main/types.d.ts +8 -2
- package/lib/cjs/features/prop-memoization/feature.d.ts +2 -0
- package/lib/cjs/features/prop-memoization/feature.js +48 -0
- package/lib/cjs/features/prop-memoization/types.d.ts +10 -0
- package/lib/cjs/features/prop-memoization/types.js +2 -0
- package/lib/cjs/features/renaming/feature.d.ts +1 -4
- package/lib/cjs/features/renaming/feature.js +36 -22
- package/lib/cjs/features/renaming/types.d.ts +2 -2
- package/lib/cjs/features/search/feature.d.ts +1 -4
- package/lib/cjs/features/search/feature.js +38 -24
- package/lib/cjs/features/search/types.d.ts +0 -1
- package/lib/cjs/features/selection/feature.d.ts +1 -4
- package/lib/cjs/features/selection/feature.js +54 -35
- package/lib/cjs/features/selection/types.d.ts +1 -1
- package/lib/cjs/features/sync-data-loader/feature.d.ts +1 -3
- package/lib/cjs/features/sync-data-loader/feature.js +7 -2
- package/lib/cjs/features/tree/feature.d.ts +1 -5
- package/lib/cjs/features/tree/feature.js +97 -92
- package/lib/cjs/features/tree/types.d.ts +5 -8
- package/lib/cjs/index.d.ts +5 -1
- package/lib/cjs/index.js +4 -1
- package/lib/cjs/mddocs-entry.d.ts +10 -0
- package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
- package/lib/cjs/test-utils/test-tree-do.js +99 -0
- package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
- package/lib/cjs/test-utils/test-tree-expect.js +62 -0
- package/lib/cjs/test-utils/test-tree.d.ts +47 -0
- package/lib/cjs/test-utils/test-tree.js +203 -0
- package/lib/cjs/types/core.d.ts +39 -24
- package/lib/cjs/utilities/errors.d.ts +1 -0
- package/lib/cjs/utilities/errors.js +5 -0
- package/lib/cjs/utilities/insert-items-at-target.js +10 -3
- package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
- package/lib/cjs/utils.d.ts +3 -3
- package/lib/cjs/utils.js +6 -6
- package/lib/esm/core/build-proxified-instance.d.ts +2 -0
- package/lib/esm/core/build-proxified-instance.js +54 -0
- package/lib/esm/core/build-static-instance.d.ts +2 -0
- package/lib/esm/core/build-static-instance.js +22 -0
- package/lib/esm/core/create-tree.js +62 -40
- package/lib/esm/features/async-data-loader/feature.d.ts +1 -4
- package/lib/esm/features/async-data-loader/feature.js +35 -23
- package/lib/esm/features/async-data-loader/types.d.ts +4 -6
- package/lib/esm/features/drag-and-drop/feature.d.ts +2 -3
- package/lib/esm/features/drag-and-drop/feature.js +79 -44
- package/lib/esm/features/drag-and-drop/types.d.ts +15 -6
- package/lib/esm/features/drag-and-drop/utils.d.ts +2 -3
- package/lib/esm/features/drag-and-drop/utils.js +138 -34
- package/lib/esm/features/expand-all/feature.d.ts +1 -5
- package/lib/esm/features/expand-all/feature.js +12 -6
- package/lib/esm/features/hotkeys-core/feature.d.ts +1 -3
- package/lib/esm/features/main/types.d.ts +8 -2
- package/lib/esm/features/prop-memoization/feature.d.ts +2 -0
- package/lib/esm/features/prop-memoization/feature.js +45 -0
- package/lib/esm/features/prop-memoization/types.d.ts +10 -0
- package/lib/esm/features/prop-memoization/types.js +1 -0
- package/lib/esm/features/renaming/feature.d.ts +1 -4
- package/lib/esm/features/renaming/feature.js +36 -22
- package/lib/esm/features/renaming/types.d.ts +2 -2
- package/lib/esm/features/search/feature.d.ts +1 -4
- package/lib/esm/features/search/feature.js +38 -24
- package/lib/esm/features/search/types.d.ts +0 -1
- package/lib/esm/features/selection/feature.d.ts +1 -4
- package/lib/esm/features/selection/feature.js +54 -35
- package/lib/esm/features/selection/types.d.ts +1 -1
- package/lib/esm/features/sync-data-loader/feature.d.ts +1 -3
- package/lib/esm/features/sync-data-loader/feature.js +7 -2
- package/lib/esm/features/tree/feature.d.ts +1 -5
- package/lib/esm/features/tree/feature.js +98 -93
- package/lib/esm/features/tree/types.d.ts +5 -8
- package/lib/esm/index.d.ts +5 -1
- package/lib/esm/index.js +4 -1
- package/lib/esm/mddocs-entry.d.ts +10 -0
- package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
- package/lib/esm/test-utils/test-tree-do.js +95 -0
- package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
- package/lib/esm/test-utils/test-tree-expect.js +58 -0
- package/lib/esm/test-utils/test-tree.d.ts +47 -0
- package/lib/esm/test-utils/test-tree.js +199 -0
- package/lib/esm/types/core.d.ts +39 -24
- package/lib/esm/utilities/errors.d.ts +1 -0
- package/lib/esm/utilities/errors.js +1 -0
- package/lib/esm/utilities/insert-items-at-target.js +10 -3
- package/lib/esm/utilities/remove-items-from-parents.js +14 -8
- package/lib/esm/utils.d.ts +3 -3
- package/lib/esm/utils.js +3 -3
- package/package.json +7 -3
- package/src/core/build-proxified-instance.ts +117 -0
- package/src/core/build-static-instance.ts +27 -0
- package/src/core/core.spec.ts +210 -0
- package/src/core/create-tree.ts +73 -78
- package/src/features/async-data-loader/async-data-loader.spec.ts +124 -0
- package/src/features/async-data-loader/feature.ts +34 -44
- package/src/features/async-data-loader/types.ts +4 -6
- package/src/features/drag-and-drop/drag-and-drop.spec.ts +717 -0
- package/src/features/drag-and-drop/feature.ts +88 -63
- package/src/features/drag-and-drop/types.ts +24 -10
- package/src/features/drag-and-drop/utils.ts +197 -56
- package/src/features/expand-all/expand-all.spec.ts +56 -0
- package/src/features/expand-all/feature.ts +9 -24
- package/src/features/hotkeys-core/feature.ts +5 -14
- package/src/features/main/types.ts +14 -1
- package/src/features/prop-memoization/feature.ts +51 -0
- package/src/features/prop-memoization/prop-memoization.spec.ts +68 -0
- package/src/features/prop-memoization/types.ts +11 -0
- package/src/features/renaming/feature.ts +37 -45
- package/src/features/renaming/renaming.spec.ts +127 -0
- package/src/features/renaming/types.ts +2 -2
- package/src/features/search/feature.ts +36 -46
- package/src/features/search/search.spec.ts +117 -0
- package/src/features/search/types.ts +0 -1
- package/src/features/selection/feature.ts +50 -53
- package/src/features/selection/selection.spec.ts +219 -0
- package/src/features/selection/types.ts +0 -2
- package/src/features/sync-data-loader/feature.ts +9 -18
- package/src/features/tree/feature.ts +101 -144
- package/src/features/tree/tree.spec.ts +475 -0
- package/src/features/tree/types.ts +5 -9
- package/src/index.ts +6 -1
- package/src/mddocs-entry.ts +13 -0
- package/src/test-utils/test-tree-do.ts +136 -0
- package/src/test-utils/test-tree-expect.ts +86 -0
- package/src/test-utils/test-tree.ts +227 -0
- package/src/types/core.ts +76 -108
- package/src/utilities/errors.ts +2 -0
- package/src/utilities/insert-items-at-target.ts +10 -3
- package/src/utilities/remove-items-from-parents.ts +15 -10
- package/src/utils.spec.ts +89 -0
- package/src/utils.ts +6 -6
- package/tsconfig.json +1 -0
- package/vitest.config.ts +6 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import { DragEvent } from "react";
|
|
3
|
+
import { Mock, expect, vi } from "vitest";
|
|
4
|
+
import { TestTree } from "./test-tree";
|
|
5
|
+
import { HotkeyName } from "../types/core";
|
|
6
|
+
import { HotkeyConfig } from "../features/hotkeys-core/types";
|
|
7
|
+
|
|
8
|
+
export class TestTreeDo<T> {
|
|
9
|
+
protected itemInstance(itemId: string) {
|
|
10
|
+
return this.tree.instance.getItemInstance(itemId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected itemProps(itemId: string) {
|
|
14
|
+
return this.itemInstance(itemId).getProps();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor(protected tree: TestTree<T>) {}
|
|
18
|
+
|
|
19
|
+
selectItem(id: string) {
|
|
20
|
+
this.itemProps(id).onClick({});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
shiftSelectItem(id: string) {
|
|
24
|
+
this.itemProps(id).onClick({ shiftKey: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ctrlSelectItem(id: string) {
|
|
28
|
+
this.itemProps(id).onClick({ ctrlKey: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ctrlShiftSelectItem(id: string) {
|
|
32
|
+
this.itemProps(id).onClick({ shiftKey: true, ctrlKey: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
selectMultiple(...ids: string[]) {
|
|
36
|
+
ids.forEach((id) => this.ctrlSelectItem(id));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
hotkey(hotkey: HotkeyName, e: Partial<KeyboardEvent> = {}) {
|
|
40
|
+
const hotkeyConfig: HotkeyConfig<any> = {
|
|
41
|
+
...this.tree.instance.getHotkeyPresets()[hotkey],
|
|
42
|
+
...this.tree.instance.getConfig().hotkeys?.[hotkey],
|
|
43
|
+
};
|
|
44
|
+
if (
|
|
45
|
+
hotkeyConfig.isEnabled &&
|
|
46
|
+
!hotkeyConfig.isEnabled?.(this.tree.instance)
|
|
47
|
+
) {
|
|
48
|
+
throw new Error(`Hotkey "${hotkey}" is disabled`);
|
|
49
|
+
}
|
|
50
|
+
if (!hotkeyConfig.handler) {
|
|
51
|
+
throw new Error(`Hotkey "${hotkey}" has no handler`);
|
|
52
|
+
}
|
|
53
|
+
hotkeyConfig.handler(
|
|
54
|
+
{
|
|
55
|
+
...e,
|
|
56
|
+
stopPropagation: () => {},
|
|
57
|
+
preventDefault: () => {},
|
|
58
|
+
} as any,
|
|
59
|
+
this.tree.instance,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
startDrag(itemId: string, event?: DragEvent) {
|
|
64
|
+
if (!this.itemProps(itemId).draggable) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Can't drag item ${itemId}, has attribute draggable=false`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const e = event ?? TestTree.dragEvent();
|
|
71
|
+
this.itemProps(itemId).onDragStart(e);
|
|
72
|
+
return e;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
dragOver(itemId: string, event?: DragEvent) {
|
|
76
|
+
const e = event ?? TestTree.dragEvent();
|
|
77
|
+
(e.preventDefault as Mock).mockClear();
|
|
78
|
+
this.itemProps(itemId).onDragOver(e);
|
|
79
|
+
this.itemProps(itemId).onDragOver(e);
|
|
80
|
+
this.itemProps(itemId).onDragOver(e);
|
|
81
|
+
expect(e.preventDefault).toBeCalledTimes(3);
|
|
82
|
+
|
|
83
|
+
this.consistentCalls(e.preventDefault);
|
|
84
|
+
this.consistentCalls(e.stopPropagation);
|
|
85
|
+
return e;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
dragOverNotAllowed(itemId: string, event?: DragEvent) {
|
|
89
|
+
const e = event ?? TestTree.dragEvent();
|
|
90
|
+
(e.preventDefault as Mock).mockClear();
|
|
91
|
+
this.itemProps(itemId).onDragOver(e);
|
|
92
|
+
this.itemProps(itemId).onDragOver(e);
|
|
93
|
+
this.itemProps(itemId).onDragOver(e);
|
|
94
|
+
expect(e.preventDefault).toBeCalledTimes(0);
|
|
95
|
+
|
|
96
|
+
this.consistentCalls(e.preventDefault);
|
|
97
|
+
this.consistentCalls(e.stopPropagation);
|
|
98
|
+
return e;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dragLeave(itemId: string) {
|
|
102
|
+
this.itemProps(itemId).onDragLeave({});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dragEnd(itemId: string, event?: DragEvent) {
|
|
106
|
+
const e = event ?? TestTree.dragEvent();
|
|
107
|
+
this.itemProps(itemId).onDragEnd(e);
|
|
108
|
+
return e;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
drop(itemId: string, event?: DragEvent) {
|
|
112
|
+
const e = event ?? TestTree.dragEvent();
|
|
113
|
+
this.itemProps(itemId).onDrop(e);
|
|
114
|
+
return e;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
dragOverAndDrop(itemId: string, event?: DragEvent) {
|
|
118
|
+
const e = event ?? TestTree.dragEvent();
|
|
119
|
+
this.dragOver(itemId, e);
|
|
120
|
+
return this.drop(itemId, e);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private consistentCalls(fn: any) {
|
|
124
|
+
if (!vi.isMockFunction(fn)) {
|
|
125
|
+
throw new Error("fn is not a mock");
|
|
126
|
+
}
|
|
127
|
+
expect(
|
|
128
|
+
fn.mock.calls.length,
|
|
129
|
+
"function called inconsistent times",
|
|
130
|
+
).toBeOneOf([0, 3]);
|
|
131
|
+
expect(
|
|
132
|
+
new Set(fn.mock.calls.map((call) => call.join("__"))).size,
|
|
133
|
+
"function called with inconsistent parameters",
|
|
134
|
+
).toBeOneOf([0, 1]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import { Mock, expect } from "vitest";
|
|
3
|
+
import { DragEvent } from "react";
|
|
4
|
+
import { TestTree } from "./test-tree";
|
|
5
|
+
import { DropTarget } from "../features/drag-and-drop/types";
|
|
6
|
+
|
|
7
|
+
export class TestTreeExpect<T> {
|
|
8
|
+
protected itemInstance(itemId: string) {
|
|
9
|
+
return this.tree.instance.getItemInstance(itemId);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected itemProps(itemId: string) {
|
|
13
|
+
return this.itemInstance(itemId).getProps();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor(private tree: TestTree<T>) {}
|
|
17
|
+
|
|
18
|
+
foldersExpanded(...itemIds: string[]) {
|
|
19
|
+
for (const itemId of itemIds) {
|
|
20
|
+
expect(
|
|
21
|
+
this.tree.instance.getItemInstance(itemId).isExpanded(),
|
|
22
|
+
`Expected ${itemId} to be expanded`,
|
|
23
|
+
).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
foldersCollapsed(...itemIds: string[]) {
|
|
28
|
+
for (const itemId of itemIds) {
|
|
29
|
+
expect(
|
|
30
|
+
this.tree.instance.getItemInstance(itemId).isExpanded(),
|
|
31
|
+
`Expected ${itemId} to be collapsed`,
|
|
32
|
+
).toBe(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
hasChildren(itemId: string, children: string[]) {
|
|
37
|
+
const item = this.tree.instance.getItemInstance(itemId);
|
|
38
|
+
const itemChildren = item.getChildren().map((child) => child.getId());
|
|
39
|
+
expect(itemChildren).toEqual(children);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
dropped(draggedItems: string[], target: DropTarget<any>) {
|
|
43
|
+
expect(this.tree.instance.getConfig().onDrop).toBeCalledWith(
|
|
44
|
+
draggedItems.map((id) => this.tree.item(id)),
|
|
45
|
+
target,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dragOverNotAllowed(itemId: string, event?: DragEvent) {
|
|
50
|
+
const e = event ?? TestTree.dragEvent();
|
|
51
|
+
(e.preventDefault as Mock).mockClear();
|
|
52
|
+
this.itemProps(itemId).onDragOver(e);
|
|
53
|
+
this.itemProps(itemId).onDragOver(e);
|
|
54
|
+
this.itemProps(itemId).onDragOver(e);
|
|
55
|
+
expect(
|
|
56
|
+
e.preventDefault,
|
|
57
|
+
"onDragOver shouldn't call e.preventDefault if drag is not allowed",
|
|
58
|
+
).not.toBeCalled();
|
|
59
|
+
|
|
60
|
+
this.itemProps(itemId).onDrop(e);
|
|
61
|
+
expect(
|
|
62
|
+
e.preventDefault,
|
|
63
|
+
"onDrop shouldn't call e.preventDefault if drag is not allowed",
|
|
64
|
+
).not.toBeCalled();
|
|
65
|
+
expect(
|
|
66
|
+
this.tree.instance.getConfig().onDrop,
|
|
67
|
+
"onDrop handler shouldn't be called if drag is not allowed",
|
|
68
|
+
).not.toBeCalled();
|
|
69
|
+
return e;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
defaultDragLineProps(indent = 0) {
|
|
73
|
+
expect(this.tree.instance.getDragLineData()).toEqual({
|
|
74
|
+
indent,
|
|
75
|
+
left: indent * 20,
|
|
76
|
+
width: 100 - indent * 20,
|
|
77
|
+
top: 0,
|
|
78
|
+
});
|
|
79
|
+
expect(this.tree.instance.getDragLineStyle(0, 0)).toEqual({
|
|
80
|
+
left: `${indent * 20}px`,
|
|
81
|
+
pointerEvents: "none",
|
|
82
|
+
top: "0px",
|
|
83
|
+
width: `${100 - indent * 20}px`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import { beforeEach, describe, vi } from "vitest";
|
|
3
|
+
import { DragEvent } from "react";
|
|
4
|
+
import { TreeConfig, TreeInstance } from "../types/core";
|
|
5
|
+
import { createTree } from "../core/create-tree";
|
|
6
|
+
import { TestTreeDo } from "./test-tree-do";
|
|
7
|
+
import { TestTreeExpect } from "./test-tree-expect";
|
|
8
|
+
import { syncDataLoaderFeature } from "../features/sync-data-loader/feature";
|
|
9
|
+
import { asyncDataLoaderFeature } from "../features/async-data-loader/feature";
|
|
10
|
+
import { buildProxiedInstance } from "../core/build-proxified-instance";
|
|
11
|
+
|
|
12
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
13
|
+
|
|
14
|
+
export class TestTree<T = string> {
|
|
15
|
+
public readonly do = new TestTreeDo(this);
|
|
16
|
+
|
|
17
|
+
public readonly expect = new TestTreeExpect(this);
|
|
18
|
+
|
|
19
|
+
private treeInstance: TreeInstance<T> | null = null;
|
|
20
|
+
|
|
21
|
+
private static asyncLoaderResolvers: (() => void)[] = [];
|
|
22
|
+
|
|
23
|
+
suits = {
|
|
24
|
+
sync: () => ({
|
|
25
|
+
tree: this.withFeatures(syncDataLoaderFeature),
|
|
26
|
+
title: "Synchronous Data Loader",
|
|
27
|
+
}),
|
|
28
|
+
async: () => ({
|
|
29
|
+
tree: this.withFeatures(asyncDataLoaderFeature),
|
|
30
|
+
title: "Asynchronous Data Loader",
|
|
31
|
+
}),
|
|
32
|
+
proxifiedSync: () => ({
|
|
33
|
+
tree: this.withFeatures(syncDataLoaderFeature).with({
|
|
34
|
+
instanceBuilder: buildProxiedInstance,
|
|
35
|
+
}),
|
|
36
|
+
title: "Proxified Synchronous Data Loader",
|
|
37
|
+
}),
|
|
38
|
+
proxifiedAsync: () => ({
|
|
39
|
+
tree: this.withFeatures(asyncDataLoaderFeature).with({
|
|
40
|
+
instanceBuilder: buildProxiedInstance,
|
|
41
|
+
}),
|
|
42
|
+
title: "Proxified Asynchronous Data Loader",
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
forSuits(runSuite: (tree: TestTree<T>) => void) {
|
|
47
|
+
describe.for([
|
|
48
|
+
this.suits.sync(),
|
|
49
|
+
this.suits.async(),
|
|
50
|
+
this.suits.proxifiedSync(),
|
|
51
|
+
this.suits.proxifiedAsync(),
|
|
52
|
+
])("$title", ({ tree }) => {
|
|
53
|
+
tree.resetBeforeEach();
|
|
54
|
+
runSuite(tree);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get instance() {
|
|
59
|
+
if (!this.treeInstance) {
|
|
60
|
+
this.treeInstance = createTree(this.config);
|
|
61
|
+
}
|
|
62
|
+
return this.treeInstance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private constructor(private config: TreeConfig<T>) {}
|
|
66
|
+
|
|
67
|
+
static async resolveAsyncLoaders() {
|
|
68
|
+
while (TestTree.asyncLoaderResolvers.length) {
|
|
69
|
+
TestTree.asyncLoaderResolvers.shift()?.();
|
|
70
|
+
await new Promise<void>((r) => {
|
|
71
|
+
setTimeout(r);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async resolveAsyncVisibleItems() {
|
|
77
|
+
this.instance.getItems();
|
|
78
|
+
await TestTree.resolveAsyncLoaders();
|
|
79
|
+
this.instance.getItems().forEach((i) => i.getItemName());
|
|
80
|
+
await TestTree.resolveAsyncLoaders();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static default(config: Partial<TreeConfig<string>>) {
|
|
84
|
+
return new TestTree({
|
|
85
|
+
rootItemId: "x",
|
|
86
|
+
createLoadingItemData: () => "loading",
|
|
87
|
+
dataLoader: {
|
|
88
|
+
getItem: (id) => id,
|
|
89
|
+
getChildren: (id) => [`${id}1`, `${id}2`, `${id}3`, `${id}4`],
|
|
90
|
+
},
|
|
91
|
+
asyncDataLoader: {
|
|
92
|
+
getItem: async (id) => {
|
|
93
|
+
await new Promise<void>((r) => {
|
|
94
|
+
(r as any).debugName = `Loading getItem ${id}`;
|
|
95
|
+
TestTree.asyncLoaderResolvers.push(r);
|
|
96
|
+
});
|
|
97
|
+
return id;
|
|
98
|
+
},
|
|
99
|
+
getChildren: async (id) => {
|
|
100
|
+
await new Promise<void>((r) => {
|
|
101
|
+
(r as any).debugName = `Loading getChildren ${id}`;
|
|
102
|
+
TestTree.asyncLoaderResolvers.push(r);
|
|
103
|
+
});
|
|
104
|
+
return [`${id}1`, `${id}2`, `${id}3`, `${id}4`];
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
getItemName: (item) => item.getItemData(),
|
|
108
|
+
indent: 20,
|
|
109
|
+
isItemFolder: (item) => item.getItemMeta().level < 2,
|
|
110
|
+
initialState: {
|
|
111
|
+
expandedItems: ["x1", "x11"],
|
|
112
|
+
},
|
|
113
|
+
features: [],
|
|
114
|
+
...config,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
with(config: Partial<TreeConfig<T>>) {
|
|
119
|
+
return new TestTree({ ...this.config, ...config });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resetBeforeEach() {
|
|
123
|
+
beforeEach(async () => {
|
|
124
|
+
await this.createTestCaseTree();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async createTestCaseTree() {
|
|
129
|
+
this.reset();
|
|
130
|
+
vi.clearAllMocks();
|
|
131
|
+
// trigger instance creation
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
133
|
+
this.instance;
|
|
134
|
+
await this.resolveAsyncVisibleItems();
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
withFeatures(...features: any) {
|
|
139
|
+
return this.with({
|
|
140
|
+
features: [...(this.config.features ?? []), ...features],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
mockedHandler(handlerName: keyof TreeConfig<T>) {
|
|
145
|
+
const mock = vi.fn();
|
|
146
|
+
if (this.treeInstance) {
|
|
147
|
+
this.treeInstance?.setConfig((prev) => ({
|
|
148
|
+
...prev,
|
|
149
|
+
[handlerName as any]: mock,
|
|
150
|
+
}));
|
|
151
|
+
} else {
|
|
152
|
+
(this.config as any)[handlerName as any] = mock;
|
|
153
|
+
}
|
|
154
|
+
return mock;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
item(itemId: string) {
|
|
158
|
+
return this.instance.getItemInstance(itemId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
reset() {
|
|
162
|
+
this.treeInstance = null;
|
|
163
|
+
TestTree.asyncLoaderResolvers = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
debug() {
|
|
167
|
+
console.log(
|
|
168
|
+
this.instance
|
|
169
|
+
.getItems()
|
|
170
|
+
.map((item) =>
|
|
171
|
+
[
|
|
172
|
+
" ".repeat(item.getItemMeta().level),
|
|
173
|
+
'"',
|
|
174
|
+
item.getItemName(),
|
|
175
|
+
'"',
|
|
176
|
+
].join(""),
|
|
177
|
+
)
|
|
178
|
+
.join("\n"),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setElementBoundingBox(
|
|
183
|
+
itemId: string,
|
|
184
|
+
bb: Partial<DOMRect> = {
|
|
185
|
+
left: 0,
|
|
186
|
+
width: 100,
|
|
187
|
+
top: 0,
|
|
188
|
+
height: 20,
|
|
189
|
+
},
|
|
190
|
+
) {
|
|
191
|
+
this.instance.registerElement({
|
|
192
|
+
getBoundingClientRect: () =>
|
|
193
|
+
({
|
|
194
|
+
left: 0,
|
|
195
|
+
width: 100,
|
|
196
|
+
top: 0,
|
|
197
|
+
height: 10000,
|
|
198
|
+
}) as DOMRect,
|
|
199
|
+
} as HTMLElement);
|
|
200
|
+
|
|
201
|
+
this.instance.getItemInstance(itemId).registerElement({
|
|
202
|
+
getBoundingClientRect: () => bb as DOMRect,
|
|
203
|
+
} as HTMLElement);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static dragEvent(pageX = 1000, pageY = 0) {
|
|
207
|
+
return {
|
|
208
|
+
preventDefault: vi.fn(),
|
|
209
|
+
stopPropagation: vi.fn(),
|
|
210
|
+
dataTransfer: {
|
|
211
|
+
setData: vi.fn(),
|
|
212
|
+
getData: vi.fn(),
|
|
213
|
+
dropEffect: "unchaged-from-test",
|
|
214
|
+
},
|
|
215
|
+
pageX,
|
|
216
|
+
pageY,
|
|
217
|
+
} as unknown as DragEvent;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
createTopDragEvent(indent = 0) {
|
|
221
|
+
return TestTree.dragEvent(indent * 20, 1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
createBottomDragEvent(indent = 0) {
|
|
225
|
+
return TestTree.dragEvent(indent * 20, 19);
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/types/core.ts
CHANGED
|
@@ -11,15 +11,16 @@ import { AsyncDataLoaderFeatureDef } from "../features/async-data-loader/types";
|
|
|
11
11
|
import { SearchFeatureDef } from "../features/search/types";
|
|
12
12
|
import { RenamingFeatureDef } from "../features/renaming/types";
|
|
13
13
|
import { ExpandAllFeatureDef } from "../features/expand-all/types";
|
|
14
|
+
import { PropMemoizationFeatureDef } from "../features/prop-memoization/types";
|
|
14
15
|
|
|
15
16
|
export type Updater<T> = T | ((old: T) => T);
|
|
16
17
|
export type SetStateFn<T> = (updaterOrValue: Updater<T>) => void;
|
|
17
18
|
|
|
18
19
|
export type FeatureDef = {
|
|
19
|
-
state:
|
|
20
|
-
config:
|
|
21
|
-
treeInstance:
|
|
22
|
-
itemInstance:
|
|
20
|
+
state: any;
|
|
21
|
+
config: any;
|
|
22
|
+
treeInstance: any;
|
|
23
|
+
itemInstance: any;
|
|
23
24
|
hotkeys: string;
|
|
24
25
|
};
|
|
25
26
|
|
|
@@ -37,10 +38,18 @@ type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
|
|
|
37
38
|
? R
|
|
38
39
|
: never;
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
type MergedFeatures<F extends FeatureDef> = {
|
|
42
|
+
// type can't be removed because it's used for individual feature sets as feature deps in feature implementations
|
|
43
|
+
// to my future self, yes I'm already aware this sounds dumb when I first write this
|
|
44
|
+
state: UnionToIntersection<F["state"]>;
|
|
45
|
+
config: UnionToIntersection<F["config"]>;
|
|
46
|
+
treeInstance: UnionToIntersection<F["treeInstance"]>;
|
|
47
|
+
itemInstance: UnionToIntersection<F["itemInstance"]>;
|
|
48
|
+
hotkeys: F["hotkeys"];
|
|
49
|
+
};
|
|
41
50
|
|
|
42
|
-
export type
|
|
43
|
-
| MainFeatureDef
|
|
51
|
+
export type RegisteredFeatures<T> =
|
|
52
|
+
| MainFeatureDef<T>
|
|
44
53
|
| TreeFeatureDef<T>
|
|
45
54
|
| SelectionFeatureDef<T>
|
|
46
55
|
| DragAndDropFeatureDef<T>
|
|
@@ -49,136 +58,95 @@ export type FeatureDefs<T> =
|
|
|
49
58
|
| AsyncDataLoaderFeatureDef<T>
|
|
50
59
|
| SearchFeatureDef<T>
|
|
51
60
|
| RenamingFeatureDef<T>
|
|
52
|
-
| ExpandAllFeatureDef
|
|
53
|
-
|
|
54
|
-
type MergedFeatures<F extends FeatureDef> = {
|
|
55
|
-
// TODO remove in favor of types below
|
|
56
|
-
state: UnionToIntersection<F["state"]>;
|
|
57
|
-
config: UnionToIntersection<F["config"]>;
|
|
58
|
-
treeInstance: UnionToIntersection<F["treeInstance"]>;
|
|
59
|
-
itemInstance: UnionToIntersection<F["itemInstance"]>;
|
|
60
|
-
hotkeys: F["hotkeys"];
|
|
61
|
-
};
|
|
61
|
+
| ExpandAllFeatureDef
|
|
62
|
+
| PropMemoizationFeatureDef;
|
|
62
63
|
|
|
63
|
-
type TreeStateType<T> =
|
|
64
|
-
TreeFeatureDef<T>["state"] &
|
|
65
|
-
SelectionFeatureDef<T>["state"] &
|
|
66
|
-
DragAndDropFeatureDef<T>["state"] &
|
|
67
|
-
HotkeysCoreFeatureDef<T>["state"] &
|
|
68
|
-
SyncDataLoaderFeatureDef<T>["state"] &
|
|
69
|
-
AsyncDataLoaderFeatureDef<T>["state"] &
|
|
70
|
-
SearchFeatureDef<T>["state"] &
|
|
71
|
-
RenamingFeatureDef<T>["state"] &
|
|
72
|
-
ExpandAllFeatureDef["state"];
|
|
64
|
+
type TreeStateType<T> = MergedFeatures<RegisteredFeatures<T>>["state"];
|
|
73
65
|
export interface TreeState<T> extends TreeStateType<T> {}
|
|
74
66
|
|
|
75
|
-
type TreeConfigType<T> =
|
|
76
|
-
TreeFeatureDef<T>["config"] &
|
|
77
|
-
SelectionFeatureDef<T>["config"] &
|
|
78
|
-
DragAndDropFeatureDef<T>["config"] &
|
|
79
|
-
HotkeysCoreFeatureDef<T>["config"] &
|
|
80
|
-
SyncDataLoaderFeatureDef<T>["config"] &
|
|
81
|
-
AsyncDataLoaderFeatureDef<T>["config"] &
|
|
82
|
-
SearchFeatureDef<T>["config"] &
|
|
83
|
-
RenamingFeatureDef<T>["config"] &
|
|
84
|
-
ExpandAllFeatureDef["config"];
|
|
67
|
+
type TreeConfigType<T> = MergedFeatures<RegisteredFeatures<T>>["config"];
|
|
85
68
|
export interface TreeConfig<T> extends TreeConfigType<T> {}
|
|
86
69
|
|
|
87
|
-
type TreeInstanceType<T> =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
DragAndDropFeatureDef<T>["treeInstance"] &
|
|
91
|
-
HotkeysCoreFeatureDef<T>["treeInstance"] &
|
|
92
|
-
SyncDataLoaderFeatureDef<T>["treeInstance"] &
|
|
93
|
-
AsyncDataLoaderFeatureDef<T>["treeInstance"] &
|
|
94
|
-
SearchFeatureDef<T>["treeInstance"] &
|
|
95
|
-
RenamingFeatureDef<T>["treeInstance"] &
|
|
96
|
-
ExpandAllFeatureDef["treeInstance"];
|
|
70
|
+
type TreeInstanceType<T> = MergedFeatures<
|
|
71
|
+
RegisteredFeatures<T>
|
|
72
|
+
>["treeInstance"];
|
|
97
73
|
export interface TreeInstance<T> extends TreeInstanceType<T> {}
|
|
98
74
|
|
|
99
|
-
type ItemInstanceType<T> =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
DragAndDropFeatureDef<T>["itemInstance"] &
|
|
103
|
-
HotkeysCoreFeatureDef<T>["itemInstance"] &
|
|
104
|
-
SyncDataLoaderFeatureDef<T>["itemInstance"] &
|
|
105
|
-
AsyncDataLoaderFeatureDef<T>["itemInstance"] &
|
|
106
|
-
SearchFeatureDef<T>["itemInstance"] &
|
|
107
|
-
RenamingFeatureDef<T>["itemInstance"] &
|
|
108
|
-
ExpandAllFeatureDef["itemInstance"];
|
|
75
|
+
type ItemInstanceType<T> = MergedFeatures<
|
|
76
|
+
RegisteredFeatures<T>
|
|
77
|
+
>["itemInstance"];
|
|
109
78
|
export interface ItemInstance<T> extends ItemInstanceType<T> {}
|
|
110
79
|
|
|
111
|
-
export type HotkeyName
|
|
112
|
-
MergedFeatures<F>["hotkeys"];
|
|
80
|
+
export type HotkeyName = MergedFeatures<RegisteredFeatures<any>>["hotkeys"];
|
|
113
81
|
|
|
114
|
-
export type HotkeysConfig<T
|
|
115
|
-
HotkeyName<F>,
|
|
116
|
-
HotkeyConfig<T>
|
|
117
|
-
>;
|
|
82
|
+
export type HotkeysConfig<T> = Record<HotkeyName, HotkeyConfig<T>>;
|
|
118
83
|
|
|
119
|
-
export type CustomHotkeysConfig<
|
|
120
|
-
T
|
|
121
|
-
F extends FeatureDef = FeatureDefs<T>,
|
|
122
|
-
> = Partial<
|
|
123
|
-
Record<HotkeyName<F> | `custom${string}`, Partial<HotkeyConfig<T>>>
|
|
84
|
+
export type CustomHotkeysConfig<T> = Partial<
|
|
85
|
+
Record<HotkeyName | `custom${string}`, Partial<HotkeyConfig<T>>>
|
|
124
86
|
>;
|
|
125
87
|
|
|
126
|
-
|
|
127
|
-
T
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
88
|
+
type MayReturnNull<T extends (...x: any[]) => any> = (
|
|
89
|
+
...args: Parameters<T>
|
|
90
|
+
) => ReturnType<T> | null;
|
|
91
|
+
|
|
92
|
+
export type ItemInstanceOpts<Key extends keyof ItemInstance<any>> = {
|
|
93
|
+
item: ItemInstance<any>;
|
|
94
|
+
tree: TreeInstance<any>;
|
|
95
|
+
itemId: string;
|
|
96
|
+
prev?: MayReturnNull<ItemInstance<any>[Key]>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type TreeInstanceOpts<Key extends keyof TreeInstance<any>> = {
|
|
100
|
+
tree: TreeInstance<any>;
|
|
101
|
+
prev?: MayReturnNull<TreeInstance<any>[Key]>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type FeatureImplementation<T = any> = {
|
|
131
105
|
key?: string;
|
|
132
106
|
deps?: string[];
|
|
133
107
|
overwrites?: string[];
|
|
134
108
|
|
|
135
|
-
stateHandlerNames?: Partial<
|
|
136
|
-
Record<keyof MergedFeatures<F>["state"], keyof MergedFeatures<F>["config"]>
|
|
137
|
-
>;
|
|
109
|
+
stateHandlerNames?: Partial<Record<keyof TreeState<T>, keyof TreeConfig<T>>>;
|
|
138
110
|
|
|
139
111
|
getInitialState?: (
|
|
140
|
-
initialState: Partial<
|
|
141
|
-
tree:
|
|
142
|
-
) => Partial<
|
|
112
|
+
initialState: Partial<TreeState<T>>,
|
|
113
|
+
tree: TreeInstance<T>,
|
|
114
|
+
) => Partial<TreeState<T>>;
|
|
143
115
|
|
|
144
116
|
getDefaultConfig?: (
|
|
145
|
-
defaultConfig: Partial<
|
|
146
|
-
tree:
|
|
147
|
-
) => Partial<
|
|
148
|
-
|
|
149
|
-
createTreeInstance?: (
|
|
150
|
-
prev: MergedFeatures<F>["treeInstance"],
|
|
151
|
-
instance: MergedFeatures<F>["treeInstance"],
|
|
152
|
-
) => D["treeInstance"] & MergedFeatures<F>["treeInstance"];
|
|
153
|
-
|
|
154
|
-
createItemInstance?: (
|
|
155
|
-
prev: MergedFeatures<F>["itemInstance"],
|
|
156
|
-
item: MergedFeatures<F>["itemInstance"],
|
|
157
|
-
tree: MergedFeatures<F>["treeInstance"],
|
|
158
|
-
itemId: string,
|
|
159
|
-
) => D["itemInstance"] & MergedFeatures<F>["itemInstance"];
|
|
160
|
-
|
|
161
|
-
onTreeMount?: (
|
|
162
|
-
instance: MergedFeatures<F>["treeInstance"],
|
|
163
|
-
treeElement: HTMLElement,
|
|
164
|
-
) => void;
|
|
117
|
+
defaultConfig: Partial<TreeConfig<T>>,
|
|
118
|
+
tree: TreeInstance<T>,
|
|
119
|
+
) => Partial<TreeConfig<T>>;
|
|
165
120
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
121
|
+
treeInstance?: {
|
|
122
|
+
[key in keyof TreeInstance<T>]?: (
|
|
123
|
+
opts: TreeInstanceOpts<key>,
|
|
124
|
+
...args: Parameters<TreeInstance<T>[key]>
|
|
125
|
+
) => void;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
itemInstance?: {
|
|
129
|
+
[key in keyof ItemInstance<T>]?: (
|
|
130
|
+
opts: ItemInstanceOpts<key>,
|
|
131
|
+
...args: Parameters<ItemInstance<T>[key]>
|
|
132
|
+
) => void;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
onTreeMount?: (instance: TreeInstance<T>, treeElement: HTMLElement) => void;
|
|
136
|
+
|
|
137
|
+
onTreeUnmount?: (instance: TreeInstance<T>, treeElement: HTMLElement) => void;
|
|
170
138
|
|
|
171
139
|
onItemMount?: (
|
|
172
|
-
instance:
|
|
140
|
+
instance: ItemInstance<T>,
|
|
173
141
|
itemElement: HTMLElement,
|
|
174
|
-
tree:
|
|
142
|
+
tree: TreeInstance<T>,
|
|
175
143
|
) => void;
|
|
176
144
|
|
|
177
145
|
onItemUnmount?: (
|
|
178
|
-
instance:
|
|
146
|
+
instance: ItemInstance<T>,
|
|
179
147
|
itemElement: HTMLElement,
|
|
180
|
-
tree:
|
|
148
|
+
tree: TreeInstance<T>,
|
|
181
149
|
) => void;
|
|
182
150
|
|
|
183
|
-
hotkeys?: HotkeysConfig<T
|
|
151
|
+
hotkeys?: Partial<HotkeysConfig<T>>;
|
|
184
152
|
};
|