@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/cjs/core/build-proxified-instance.d.ts +2 -0
  3. package/lib/cjs/core/build-proxified-instance.js +58 -0
  4. package/lib/cjs/core/build-static-instance.d.ts +2 -0
  5. package/lib/cjs/core/build-static-instance.js +26 -0
  6. package/lib/cjs/core/create-tree.js +62 -40
  7. package/lib/cjs/features/async-data-loader/feature.d.ts +1 -4
  8. package/lib/cjs/features/async-data-loader/feature.js +35 -23
  9. package/lib/cjs/features/async-data-loader/types.d.ts +4 -6
  10. package/lib/cjs/features/drag-and-drop/feature.d.ts +2 -3
  11. package/lib/cjs/features/drag-and-drop/feature.js +79 -44
  12. package/lib/cjs/features/drag-and-drop/types.d.ts +15 -6
  13. package/lib/cjs/features/drag-and-drop/utils.d.ts +2 -3
  14. package/lib/cjs/features/drag-and-drop/utils.js +140 -37
  15. package/lib/cjs/features/expand-all/feature.d.ts +1 -5
  16. package/lib/cjs/features/expand-all/feature.js +12 -6
  17. package/lib/cjs/features/hotkeys-core/feature.d.ts +1 -3
  18. package/lib/cjs/features/main/types.d.ts +8 -2
  19. package/lib/cjs/features/prop-memoization/feature.d.ts +2 -0
  20. package/lib/cjs/features/prop-memoization/feature.js +48 -0
  21. package/lib/cjs/features/prop-memoization/types.d.ts +10 -0
  22. package/lib/cjs/features/prop-memoization/types.js +2 -0
  23. package/lib/cjs/features/renaming/feature.d.ts +1 -4
  24. package/lib/cjs/features/renaming/feature.js +36 -22
  25. package/lib/cjs/features/renaming/types.d.ts +2 -2
  26. package/lib/cjs/features/search/feature.d.ts +1 -4
  27. package/lib/cjs/features/search/feature.js +38 -24
  28. package/lib/cjs/features/search/types.d.ts +0 -1
  29. package/lib/cjs/features/selection/feature.d.ts +1 -4
  30. package/lib/cjs/features/selection/feature.js +54 -35
  31. package/lib/cjs/features/selection/types.d.ts +1 -1
  32. package/lib/cjs/features/sync-data-loader/feature.d.ts +1 -3
  33. package/lib/cjs/features/sync-data-loader/feature.js +7 -2
  34. package/lib/cjs/features/tree/feature.d.ts +1 -5
  35. package/lib/cjs/features/tree/feature.js +97 -92
  36. package/lib/cjs/features/tree/types.d.ts +5 -8
  37. package/lib/cjs/index.d.ts +5 -1
  38. package/lib/cjs/index.js +4 -1
  39. package/lib/cjs/mddocs-entry.d.ts +10 -0
  40. package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
  41. package/lib/cjs/test-utils/test-tree-do.js +99 -0
  42. package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
  43. package/lib/cjs/test-utils/test-tree-expect.js +62 -0
  44. package/lib/cjs/test-utils/test-tree.d.ts +47 -0
  45. package/lib/cjs/test-utils/test-tree.js +203 -0
  46. package/lib/cjs/types/core.d.ts +39 -24
  47. package/lib/cjs/utilities/errors.d.ts +1 -0
  48. package/lib/cjs/utilities/errors.js +5 -0
  49. package/lib/cjs/utilities/insert-items-at-target.js +10 -3
  50. package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
  51. package/lib/cjs/utils.d.ts +3 -3
  52. package/lib/cjs/utils.js +6 -6
  53. package/lib/esm/core/build-proxified-instance.d.ts +2 -0
  54. package/lib/esm/core/build-proxified-instance.js +54 -0
  55. package/lib/esm/core/build-static-instance.d.ts +2 -0
  56. package/lib/esm/core/build-static-instance.js +22 -0
  57. package/lib/esm/core/create-tree.js +62 -40
  58. package/lib/esm/features/async-data-loader/feature.d.ts +1 -4
  59. package/lib/esm/features/async-data-loader/feature.js +35 -23
  60. package/lib/esm/features/async-data-loader/types.d.ts +4 -6
  61. package/lib/esm/features/drag-and-drop/feature.d.ts +2 -3
  62. package/lib/esm/features/drag-and-drop/feature.js +79 -44
  63. package/lib/esm/features/drag-and-drop/types.d.ts +15 -6
  64. package/lib/esm/features/drag-and-drop/utils.d.ts +2 -3
  65. package/lib/esm/features/drag-and-drop/utils.js +138 -34
  66. package/lib/esm/features/expand-all/feature.d.ts +1 -5
  67. package/lib/esm/features/expand-all/feature.js +12 -6
  68. package/lib/esm/features/hotkeys-core/feature.d.ts +1 -3
  69. package/lib/esm/features/main/types.d.ts +8 -2
  70. package/lib/esm/features/prop-memoization/feature.d.ts +2 -0
  71. package/lib/esm/features/prop-memoization/feature.js +45 -0
  72. package/lib/esm/features/prop-memoization/types.d.ts +10 -0
  73. package/lib/esm/features/prop-memoization/types.js +1 -0
  74. package/lib/esm/features/renaming/feature.d.ts +1 -4
  75. package/lib/esm/features/renaming/feature.js +36 -22
  76. package/lib/esm/features/renaming/types.d.ts +2 -2
  77. package/lib/esm/features/search/feature.d.ts +1 -4
  78. package/lib/esm/features/search/feature.js +38 -24
  79. package/lib/esm/features/search/types.d.ts +0 -1
  80. package/lib/esm/features/selection/feature.d.ts +1 -4
  81. package/lib/esm/features/selection/feature.js +54 -35
  82. package/lib/esm/features/selection/types.d.ts +1 -1
  83. package/lib/esm/features/sync-data-loader/feature.d.ts +1 -3
  84. package/lib/esm/features/sync-data-loader/feature.js +7 -2
  85. package/lib/esm/features/tree/feature.d.ts +1 -5
  86. package/lib/esm/features/tree/feature.js +98 -93
  87. package/lib/esm/features/tree/types.d.ts +5 -8
  88. package/lib/esm/index.d.ts +5 -1
  89. package/lib/esm/index.js +4 -1
  90. package/lib/esm/mddocs-entry.d.ts +10 -0
  91. package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
  92. package/lib/esm/test-utils/test-tree-do.js +95 -0
  93. package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
  94. package/lib/esm/test-utils/test-tree-expect.js +58 -0
  95. package/lib/esm/test-utils/test-tree.d.ts +47 -0
  96. package/lib/esm/test-utils/test-tree.js +199 -0
  97. package/lib/esm/types/core.d.ts +39 -24
  98. package/lib/esm/utilities/errors.d.ts +1 -0
  99. package/lib/esm/utilities/errors.js +1 -0
  100. package/lib/esm/utilities/insert-items-at-target.js +10 -3
  101. package/lib/esm/utilities/remove-items-from-parents.js +14 -8
  102. package/lib/esm/utils.d.ts +3 -3
  103. package/lib/esm/utils.js +3 -3
  104. package/package.json +7 -3
  105. package/src/core/build-proxified-instance.ts +117 -0
  106. package/src/core/build-static-instance.ts +27 -0
  107. package/src/core/core.spec.ts +210 -0
  108. package/src/core/create-tree.ts +73 -78
  109. package/src/features/async-data-loader/async-data-loader.spec.ts +124 -0
  110. package/src/features/async-data-loader/feature.ts +34 -44
  111. package/src/features/async-data-loader/types.ts +4 -6
  112. package/src/features/drag-and-drop/drag-and-drop.spec.ts +717 -0
  113. package/src/features/drag-and-drop/feature.ts +88 -63
  114. package/src/features/drag-and-drop/types.ts +24 -10
  115. package/src/features/drag-and-drop/utils.ts +197 -56
  116. package/src/features/expand-all/expand-all.spec.ts +56 -0
  117. package/src/features/expand-all/feature.ts +9 -24
  118. package/src/features/hotkeys-core/feature.ts +5 -14
  119. package/src/features/main/types.ts +14 -1
  120. package/src/features/prop-memoization/feature.ts +51 -0
  121. package/src/features/prop-memoization/prop-memoization.spec.ts +68 -0
  122. package/src/features/prop-memoization/types.ts +11 -0
  123. package/src/features/renaming/feature.ts +37 -45
  124. package/src/features/renaming/renaming.spec.ts +127 -0
  125. package/src/features/renaming/types.ts +2 -2
  126. package/src/features/search/feature.ts +36 -46
  127. package/src/features/search/search.spec.ts +117 -0
  128. package/src/features/search/types.ts +0 -1
  129. package/src/features/selection/feature.ts +50 -53
  130. package/src/features/selection/selection.spec.ts +219 -0
  131. package/src/features/selection/types.ts +0 -2
  132. package/src/features/sync-data-loader/feature.ts +9 -18
  133. package/src/features/tree/feature.ts +101 -144
  134. package/src/features/tree/tree.spec.ts +475 -0
  135. package/src/features/tree/types.ts +5 -9
  136. package/src/index.ts +6 -1
  137. package/src/mddocs-entry.ts +13 -0
  138. package/src/test-utils/test-tree-do.ts +136 -0
  139. package/src/test-utils/test-tree-expect.ts +86 -0
  140. package/src/test-utils/test-tree.ts +227 -0
  141. package/src/types/core.ts +76 -108
  142. package/src/utilities/errors.ts +2 -0
  143. package/src/utilities/insert-items-at-target.ts +10 -3
  144. package/src/utilities/remove-items-from-parents.ts +15 -10
  145. package/src/utils.spec.ts +89 -0
  146. package/src/utils.ts +6 -6
  147. package/tsconfig.json +1 -0
  148. 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: object;
20
- config: object;
21
- treeInstance: object;
22
- itemInstance: object;
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
- export type DefaultFeatures<T> = MainFeatureDef | TreeFeatureDef<T>;
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 FeatureDefs<T> =
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> = MainFeatureDef["state"] &
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> = MainFeatureDef["config"] &
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> = MainFeatureDef["treeInstance"] &
88
- TreeFeatureDef<T>["treeInstance"] &
89
- SelectionFeatureDef<T>["treeInstance"] &
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> = MainFeatureDef["itemInstance"] &
100
- TreeFeatureDef<T>["itemInstance"] &
101
- SelectionFeatureDef<T>["itemInstance"] &
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<F extends FeatureDef = FeatureDefs<any>> =
112
- MergedFeatures<F>["hotkeys"];
80
+ export type HotkeyName = MergedFeatures<RegisteredFeatures<any>>["hotkeys"];
113
81
 
114
- export type HotkeysConfig<T, F extends FeatureDef = FeatureDefs<T>> = Record<
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
- export type FeatureImplementation<
127
- T = any,
128
- D extends FeatureDef = any,
129
- F extends FeatureDef = EmptyFeatureDef,
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<MergedFeatures<F>["state"]>,
141
- tree: MergedFeatures<F>["treeInstance"],
142
- ) => Partial<D["state"] & MergedFeatures<F>["state"]>;
112
+ initialState: Partial<TreeState<T>>,
113
+ tree: TreeInstance<T>,
114
+ ) => Partial<TreeState<T>>;
143
115
 
144
116
  getDefaultConfig?: (
145
- defaultConfig: Partial<MergedFeatures<F>["config"]>,
146
- tree: MergedFeatures<F>["treeInstance"],
147
- ) => Partial<D["config"] & MergedFeatures<F>["config"]>;
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
- onTreeUnmount?: (
167
- instance: MergedFeatures<F>["treeInstance"],
168
- treeElement: HTMLElement,
169
- ) => void;
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: MergedFeatures<F>["itemInstance"],
140
+ instance: ItemInstance<T>,
173
141
  itemElement: HTMLElement,
174
- tree: MergedFeatures<F>["treeInstance"],
142
+ tree: TreeInstance<T>,
175
143
  ) => void;
176
144
 
177
145
  onItemUnmount?: (
178
- instance: MergedFeatures<F>["itemInstance"],
146
+ instance: ItemInstance<T>,
179
147
  itemElement: HTMLElement,
180
- tree: MergedFeatures<F>["treeInstance"],
148
+ tree: TreeInstance<T>,
181
149
  ) => void;
182
150
 
183
- hotkeys?: HotkeysConfig<T, D>;
151
+ hotkeys?: Partial<HotkeysConfig<T>>;
184
152
  };
@@ -0,0 +1,2 @@
1
+ export const throwError = (message: string) =>
2
+ Error(`Headless Tree: ${message}`);