@headless-tree/core 0.0.9 → 0.0.11
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 +27 -0
- package/lib/cjs/core/create-tree.js +55 -36
- package/lib/cjs/features/async-data-loader/feature.js +37 -23
- package/lib/cjs/features/async-data-loader/types.d.ts +2 -1
- package/lib/cjs/features/drag-and-drop/feature.js +64 -32
- package/lib/cjs/features/drag-and-drop/types.d.ts +13 -4
- package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -2
- package/lib/cjs/features/drag-and-drop/utils.js +140 -37
- package/lib/cjs/features/expand-all/feature.js +12 -6
- package/lib/cjs/features/main/types.d.ts +8 -2
- package/lib/cjs/features/renaming/feature.js +33 -18
- package/lib/cjs/features/renaming/types.d.ts +1 -1
- 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.js +23 -14
- package/lib/cjs/features/sync-data-loader/feature.js +7 -2
- package/lib/cjs/features/tree/feature.d.ts +2 -1
- package/lib/cjs/features/tree/feature.js +85 -63
- package/lib/cjs/features/tree/types.d.ts +5 -3
- package/lib/cjs/index.d.ts +3 -1
- package/lib/cjs/index.js +2 -1
- 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 +195 -0
- package/lib/cjs/types/core.d.ts +31 -15
- 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 +23 -0
- package/lib/esm/core/create-tree.js +55 -36
- package/lib/esm/features/async-data-loader/feature.js +37 -23
- package/lib/esm/features/async-data-loader/types.d.ts +2 -1
- package/lib/esm/features/drag-and-drop/feature.js +64 -32
- package/lib/esm/features/drag-and-drop/types.d.ts +13 -4
- package/lib/esm/features/drag-and-drop/utils.d.ts +1 -2
- package/lib/esm/features/drag-and-drop/utils.js +138 -34
- package/lib/esm/features/expand-all/feature.js +12 -6
- package/lib/esm/features/main/types.d.ts +8 -2
- package/lib/esm/features/renaming/feature.js +33 -18
- package/lib/esm/features/renaming/types.d.ts +1 -1
- 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.js +23 -14
- package/lib/esm/features/sync-data-loader/feature.js +7 -2
- package/lib/esm/features/tree/feature.d.ts +2 -1
- package/lib/esm/features/tree/feature.js +86 -64
- package/lib/esm/features/tree/types.d.ts +5 -3
- package/lib/esm/index.d.ts +3 -1
- package/lib/esm/index.js +2 -1
- 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 +191 -0
- package/lib/esm/types/core.d.ts +31 -15
- 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 +115 -0
- package/src/core/build-static-instance.ts +28 -0
- package/src/core/create-tree.ts +60 -62
- package/src/features/async-data-loader/async-data-loader.spec.ts +143 -0
- package/src/features/async-data-loader/feature.ts +33 -31
- package/src/features/async-data-loader/types.ts +3 -1
- package/src/features/drag-and-drop/drag-and-drop.spec.ts +716 -0
- package/src/features/drag-and-drop/feature.ts +109 -85
- package/src/features/drag-and-drop/types.ts +21 -7
- package/src/features/drag-and-drop/utils.ts +196 -55
- package/src/features/expand-all/expand-all.spec.ts +52 -0
- package/src/features/expand-all/feature.ts +8 -12
- package/src/features/hotkeys-core/feature.ts +1 -1
- package/src/features/main/types.ts +14 -1
- package/src/features/renaming/feature.ts +30 -29
- package/src/features/renaming/renaming.spec.ts +125 -0
- package/src/features/renaming/types.ts +1 -1
- package/src/features/search/feature.ts +34 -38
- package/src/features/search/search.spec.ts +115 -0
- package/src/features/search/types.ts +0 -1
- package/src/features/selection/feature.ts +29 -30
- package/src/features/selection/selection.spec.ts +220 -0
- package/src/features/sync-data-loader/feature.ts +8 -11
- package/src/features/tree/feature.ts +82 -87
- package/src/features/tree/tree.spec.ts +515 -0
- package/src/features/tree/types.ts +5 -3
- package/src/index.ts +4 -1
- 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 +217 -0
- package/src/types/core.ts +92 -33
- 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,95 @@
|
|
|
1
|
+
import { expect, vi } from "vitest";
|
|
2
|
+
import { TestTree } from "./test-tree";
|
|
3
|
+
export class TestTreeDo {
|
|
4
|
+
itemInstance(itemId) {
|
|
5
|
+
return this.tree.instance.getItemInstance(itemId);
|
|
6
|
+
}
|
|
7
|
+
itemProps(itemId) {
|
|
8
|
+
return this.itemInstance(itemId).getProps();
|
|
9
|
+
}
|
|
10
|
+
constructor(tree) {
|
|
11
|
+
this.tree = tree;
|
|
12
|
+
}
|
|
13
|
+
selectItem(id) {
|
|
14
|
+
this.itemProps(id).onClick({});
|
|
15
|
+
}
|
|
16
|
+
shiftSelectItem(id) {
|
|
17
|
+
this.itemProps(id).onClick({ shiftKey: true });
|
|
18
|
+
}
|
|
19
|
+
ctrlSelectItem(id) {
|
|
20
|
+
this.itemProps(id).onClick({ ctrlKey: true });
|
|
21
|
+
}
|
|
22
|
+
ctrlShiftSelectItem(id) {
|
|
23
|
+
this.itemProps(id).onClick({ shiftKey: true, ctrlKey: true });
|
|
24
|
+
}
|
|
25
|
+
selectMultiple(...ids) {
|
|
26
|
+
ids.forEach((id) => this.ctrlSelectItem(id));
|
|
27
|
+
}
|
|
28
|
+
hotkey(hotkey, e = {}) {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
const hotkeyConfig = Object.assign(Object.assign({}, this.tree.instance.getHotkeyPresets()[hotkey]), (_a = this.tree.instance.getConfig().hotkeys) === null || _a === void 0 ? void 0 : _a[hotkey]);
|
|
31
|
+
if (hotkeyConfig.isEnabled &&
|
|
32
|
+
!((_b = hotkeyConfig.isEnabled) === null || _b === void 0 ? void 0 : _b.call(hotkeyConfig, this.tree.instance))) {
|
|
33
|
+
throw new Error(`Hotkey "${hotkey}" is disabled`);
|
|
34
|
+
}
|
|
35
|
+
if (!hotkeyConfig.handler) {
|
|
36
|
+
throw new Error(`Hotkey "${hotkey}" has no handler`);
|
|
37
|
+
}
|
|
38
|
+
hotkeyConfig.handler(Object.assign(Object.assign({}, e), { stopPropagation: () => { }, preventDefault: () => { } }), this.tree.instance);
|
|
39
|
+
}
|
|
40
|
+
startDrag(itemId, event) {
|
|
41
|
+
if (!this.itemProps(itemId).draggable) {
|
|
42
|
+
throw new Error(`Can't drag item ${itemId}, has attribute draggable=false`);
|
|
43
|
+
}
|
|
44
|
+
const e = event !== null && event !== void 0 ? event : TestTree.dragEvent();
|
|
45
|
+
this.itemProps(itemId).onDragStart(e);
|
|
46
|
+
return e;
|
|
47
|
+
}
|
|
48
|
+
dragOver(itemId, event) {
|
|
49
|
+
const e = event !== null && event !== void 0 ? event : TestTree.dragEvent();
|
|
50
|
+
e.preventDefault.mockClear();
|
|
51
|
+
this.itemProps(itemId).onDragOver(e);
|
|
52
|
+
this.itemProps(itemId).onDragOver(e);
|
|
53
|
+
this.itemProps(itemId).onDragOver(e);
|
|
54
|
+
expect(e.preventDefault).toBeCalledTimes(3);
|
|
55
|
+
this.consistentCalls(e.preventDefault);
|
|
56
|
+
this.consistentCalls(e.stopPropagation);
|
|
57
|
+
return e;
|
|
58
|
+
}
|
|
59
|
+
dragOverNotAllowed(itemId, event) {
|
|
60
|
+
const e = event !== null && event !== void 0 ? event : TestTree.dragEvent();
|
|
61
|
+
e.preventDefault.mockClear();
|
|
62
|
+
this.itemProps(itemId).onDragOver(e);
|
|
63
|
+
this.itemProps(itemId).onDragOver(e);
|
|
64
|
+
this.itemProps(itemId).onDragOver(e);
|
|
65
|
+
expect(e.preventDefault).toBeCalledTimes(0);
|
|
66
|
+
this.consistentCalls(e.preventDefault);
|
|
67
|
+
this.consistentCalls(e.stopPropagation);
|
|
68
|
+
return e;
|
|
69
|
+
}
|
|
70
|
+
dragLeave(itemId) {
|
|
71
|
+
this.itemProps(itemId).onDragLeave({});
|
|
72
|
+
}
|
|
73
|
+
dragEnd(itemId, event) {
|
|
74
|
+
const e = event !== null && event !== void 0 ? event : TestTree.dragEvent();
|
|
75
|
+
this.itemProps(itemId).onDragEnd(e);
|
|
76
|
+
return e;
|
|
77
|
+
}
|
|
78
|
+
drop(itemId, event) {
|
|
79
|
+
const e = event !== null && event !== void 0 ? event : TestTree.dragEvent();
|
|
80
|
+
this.itemProps(itemId).onDrop(e);
|
|
81
|
+
return e;
|
|
82
|
+
}
|
|
83
|
+
dragOverAndDrop(itemId, event) {
|
|
84
|
+
const e = event !== null && event !== void 0 ? event : TestTree.dragEvent();
|
|
85
|
+
this.dragOver(itemId, e);
|
|
86
|
+
return this.drop(itemId, e);
|
|
87
|
+
}
|
|
88
|
+
consistentCalls(fn) {
|
|
89
|
+
if (!vi.isMockFunction(fn)) {
|
|
90
|
+
throw new Error("fn is not a mock");
|
|
91
|
+
}
|
|
92
|
+
expect(fn.mock.calls.length, "function called inconsistent times").toBeOneOf([0, 3]);
|
|
93
|
+
expect(new Set(fn.mock.calls.map((call) => call.join("__"))).size, "function called with inconsistent parameters").toBeOneOf([0, 1]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DragEvent } from "react";
|
|
2
|
+
import { TestTree } from "./test-tree";
|
|
3
|
+
import { DropTarget } from "../features/drag-and-drop/types";
|
|
4
|
+
export declare class TestTreeExpect<T> {
|
|
5
|
+
private tree;
|
|
6
|
+
protected itemInstance(itemId: string): import("..").ItemInstance<any>;
|
|
7
|
+
protected itemProps(itemId: string): Record<string, any>;
|
|
8
|
+
constructor(tree: TestTree<T>);
|
|
9
|
+
foldersExpanded(...itemIds: string[]): void;
|
|
10
|
+
foldersCollapsed(...itemIds: string[]): void;
|
|
11
|
+
hasChildren(itemId: string, children: string[]): void;
|
|
12
|
+
dropped(draggedItems: string[], target: DropTarget<any>): void;
|
|
13
|
+
dragOverNotAllowed(itemId: string, event?: DragEvent): DragEvent<Element>;
|
|
14
|
+
defaultDragLineProps(indent?: number): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import { expect } from "vitest";
|
|
3
|
+
import { TestTree } from "./test-tree";
|
|
4
|
+
export class TestTreeExpect {
|
|
5
|
+
itemInstance(itemId) {
|
|
6
|
+
return this.tree.instance.getItemInstance(itemId);
|
|
7
|
+
}
|
|
8
|
+
itemProps(itemId) {
|
|
9
|
+
return this.itemInstance(itemId).getProps();
|
|
10
|
+
}
|
|
11
|
+
constructor(tree) {
|
|
12
|
+
this.tree = tree;
|
|
13
|
+
}
|
|
14
|
+
foldersExpanded(...itemIds) {
|
|
15
|
+
for (const itemId of itemIds) {
|
|
16
|
+
expect(this.tree.instance.getItemInstance(itemId).isExpanded(), `Expected ${itemId} to be expanded`).toBe(true);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
foldersCollapsed(...itemIds) {
|
|
20
|
+
for (const itemId of itemIds) {
|
|
21
|
+
expect(this.tree.instance.getItemInstance(itemId).isExpanded(), `Expected ${itemId} to be collapsed`).toBe(false);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
hasChildren(itemId, children) {
|
|
25
|
+
const item = this.tree.instance.getItemInstance(itemId);
|
|
26
|
+
const itemChildren = item.getChildren().map((child) => child.getId());
|
|
27
|
+
expect(itemChildren).toEqual(children);
|
|
28
|
+
}
|
|
29
|
+
dropped(draggedItems, target) {
|
|
30
|
+
expect(this.tree.instance.getConfig().onDrop).toBeCalledWith(draggedItems.map((id) => this.tree.item(id)), target);
|
|
31
|
+
}
|
|
32
|
+
dragOverNotAllowed(itemId, event) {
|
|
33
|
+
const e = event !== null && event !== void 0 ? event : TestTree.dragEvent();
|
|
34
|
+
e.preventDefault.mockClear();
|
|
35
|
+
this.itemProps(itemId).onDragOver(e);
|
|
36
|
+
this.itemProps(itemId).onDragOver(e);
|
|
37
|
+
this.itemProps(itemId).onDragOver(e);
|
|
38
|
+
expect(e.preventDefault, "onDragOver shouldn't call e.preventDefault if drag is not allowed").not.toBeCalled();
|
|
39
|
+
this.itemProps(itemId).onDrop(e);
|
|
40
|
+
expect(e.preventDefault, "onDrop shouldn't call e.preventDefault if drag is not allowed").not.toBeCalled();
|
|
41
|
+
expect(this.tree.instance.getConfig().onDrop, "onDrop handler shouldn't be called if drag is not allowed").not.toBeCalled();
|
|
42
|
+
return e;
|
|
43
|
+
}
|
|
44
|
+
defaultDragLineProps(indent = 0) {
|
|
45
|
+
expect(this.tree.instance.getDragLineData()).toEqual({
|
|
46
|
+
indent,
|
|
47
|
+
left: indent * 20,
|
|
48
|
+
right: 100,
|
|
49
|
+
top: 0,
|
|
50
|
+
});
|
|
51
|
+
expect(this.tree.instance.getDragLineStyle(0, 0)).toEqual({
|
|
52
|
+
left: `${indent * 20}px`,
|
|
53
|
+
pointerEvents: "none",
|
|
54
|
+
top: "0px",
|
|
55
|
+
width: `${100 - indent * 20}px`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DragEvent } from "react";
|
|
2
|
+
import { TreeConfig, TreeInstance } from "../types/core";
|
|
3
|
+
import { TestTreeDo } from "./test-tree-do";
|
|
4
|
+
import { TestTreeExpect } from "./test-tree-expect";
|
|
5
|
+
export declare class TestTree<T = string> {
|
|
6
|
+
private config;
|
|
7
|
+
readonly do: TestTreeDo<T>;
|
|
8
|
+
readonly expect: TestTreeExpect<T>;
|
|
9
|
+
private treeInstance;
|
|
10
|
+
private static asyncLoaderResolvers;
|
|
11
|
+
suits: {
|
|
12
|
+
sync: () => {
|
|
13
|
+
tree: TestTree<T>;
|
|
14
|
+
title: string;
|
|
15
|
+
};
|
|
16
|
+
async: () => {
|
|
17
|
+
tree: TestTree<T>;
|
|
18
|
+
title: string;
|
|
19
|
+
};
|
|
20
|
+
proxifiedSync: () => {
|
|
21
|
+
tree: TestTree<T>;
|
|
22
|
+
title: string;
|
|
23
|
+
};
|
|
24
|
+
proxifiedAsync: () => {
|
|
25
|
+
tree: TestTree<T>;
|
|
26
|
+
title: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
forSuits(runSuite: (tree: TestTree<T>) => void): void;
|
|
30
|
+
get instance(): TreeInstance<T>;
|
|
31
|
+
private constructor();
|
|
32
|
+
static resolveAsyncLoaders(): Promise<void>;
|
|
33
|
+
resolveAsyncVisibleItems(): Promise<void>;
|
|
34
|
+
static default(config: Partial<TreeConfig<string>>): TestTree<string>;
|
|
35
|
+
with(config: Partial<TreeConfig<T>>): TestTree<T>;
|
|
36
|
+
resetBeforeEach(): void;
|
|
37
|
+
createTestCaseTree(): Promise<this>;
|
|
38
|
+
withFeatures(...features: any): TestTree<T>;
|
|
39
|
+
mockedHandler(handlerName: keyof TreeConfig<T>): import("vitest").Mock<(...args: any[]) => any>;
|
|
40
|
+
item(itemId: string): import("../types/core").ItemInstance<any>;
|
|
41
|
+
reset(): void;
|
|
42
|
+
debug(): void;
|
|
43
|
+
setElementBoundingBox(itemId: string, bb?: Partial<DOMRect>): void;
|
|
44
|
+
static dragEvent(pageX?: number, pageY?: number): DragEvent;
|
|
45
|
+
createTopDragEvent(indent?: number): DragEvent<Element>;
|
|
46
|
+
createBottomDragEvent(indent?: number): DragEvent<Element>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
11
|
+
import { beforeEach, describe, vi } from "vitest";
|
|
12
|
+
import { createTree } from "../core/create-tree";
|
|
13
|
+
import { TestTreeDo } from "./test-tree-do";
|
|
14
|
+
import { TestTreeExpect } from "./test-tree-expect";
|
|
15
|
+
import { syncDataLoaderFeature } from "../features/sync-data-loader/feature";
|
|
16
|
+
import { asyncDataLoaderFeature } from "../features/async-data-loader/feature";
|
|
17
|
+
import { buildProxiedInstance } from "../core/build-proxified-instance";
|
|
18
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
19
|
+
export class TestTree {
|
|
20
|
+
forSuits(runSuite) {
|
|
21
|
+
describe.for([
|
|
22
|
+
this.suits.sync(),
|
|
23
|
+
this.suits.async(),
|
|
24
|
+
this.suits.proxifiedSync(),
|
|
25
|
+
this.suits.proxifiedAsync(),
|
|
26
|
+
])("$title", ({ tree }) => {
|
|
27
|
+
tree.resetBeforeEach();
|
|
28
|
+
runSuite(tree);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
get instance() {
|
|
32
|
+
if (!this.treeInstance) {
|
|
33
|
+
this.treeInstance = createTree(this.config);
|
|
34
|
+
}
|
|
35
|
+
return this.treeInstance;
|
|
36
|
+
}
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.do = new TestTreeDo(this);
|
|
40
|
+
this.expect = new TestTreeExpect(this);
|
|
41
|
+
this.treeInstance = null;
|
|
42
|
+
this.suits = {
|
|
43
|
+
sync: () => ({
|
|
44
|
+
tree: this.withFeatures(syncDataLoaderFeature),
|
|
45
|
+
title: "Synchronous Data Loader",
|
|
46
|
+
}),
|
|
47
|
+
async: () => ({
|
|
48
|
+
tree: this.withFeatures(asyncDataLoaderFeature),
|
|
49
|
+
title: "Asynchronous Data Loader",
|
|
50
|
+
}),
|
|
51
|
+
proxifiedSync: () => ({
|
|
52
|
+
tree: this.withFeatures(syncDataLoaderFeature).with({
|
|
53
|
+
instanceBuilder: buildProxiedInstance,
|
|
54
|
+
}),
|
|
55
|
+
title: "Proxified Synchronous Data Loader",
|
|
56
|
+
}),
|
|
57
|
+
proxifiedAsync: () => ({
|
|
58
|
+
tree: this.withFeatures(asyncDataLoaderFeature).with({
|
|
59
|
+
instanceBuilder: buildProxiedInstance,
|
|
60
|
+
}),
|
|
61
|
+
title: "Proxified Asynchronous Data Loader",
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
static resolveAsyncLoaders() {
|
|
66
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
var _a;
|
|
68
|
+
while (TestTree.asyncLoaderResolvers.length) {
|
|
69
|
+
(_a = TestTree.asyncLoaderResolvers.shift()) === null || _a === void 0 ? void 0 : _a();
|
|
70
|
+
yield new Promise((r) => {
|
|
71
|
+
setTimeout(r);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
resolveAsyncVisibleItems() {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
this.instance.getItems();
|
|
79
|
+
yield TestTree.resolveAsyncLoaders();
|
|
80
|
+
this.instance.getItems().forEach((i) => i.getItemName());
|
|
81
|
+
yield TestTree.resolveAsyncLoaders();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
static default(config) {
|
|
85
|
+
return new TestTree(Object.assign({ rootItemId: "x", createLoadingItemData: () => "loading", dataLoader: {
|
|
86
|
+
getItem: (id) => id,
|
|
87
|
+
getChildren: (id) => [`${id}1`, `${id}2`, `${id}3`, `${id}4`],
|
|
88
|
+
}, asyncDataLoader: {
|
|
89
|
+
getItem: (id) => __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
yield new Promise((r) => {
|
|
91
|
+
r.debugName = `Loading getItem ${id}`;
|
|
92
|
+
TestTree.asyncLoaderResolvers.push(r);
|
|
93
|
+
});
|
|
94
|
+
return id;
|
|
95
|
+
}),
|
|
96
|
+
getChildren: (id) => __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
yield new Promise((r) => {
|
|
98
|
+
r.debugName = `Loading getChildren ${id}`;
|
|
99
|
+
TestTree.asyncLoaderResolvers.push(r);
|
|
100
|
+
});
|
|
101
|
+
return [`${id}1`, `${id}2`, `${id}3`, `${id}4`];
|
|
102
|
+
}),
|
|
103
|
+
}, getItemName: (item) => item.getItemData(), indent: 20, isItemFolder: (item) => item.getItemMeta().level < 2, initialState: {
|
|
104
|
+
expandedItems: ["x1", "x11"],
|
|
105
|
+
}, features: [] }, config));
|
|
106
|
+
}
|
|
107
|
+
with(config) {
|
|
108
|
+
return new TestTree(Object.assign(Object.assign({}, this.config), config));
|
|
109
|
+
}
|
|
110
|
+
resetBeforeEach() {
|
|
111
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
112
|
+
yield this.createTestCaseTree();
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
createTestCaseTree() {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
this.reset();
|
|
118
|
+
vi.clearAllMocks();
|
|
119
|
+
// trigger instance creation
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
121
|
+
this.instance;
|
|
122
|
+
yield this.resolveAsyncVisibleItems();
|
|
123
|
+
return this;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
withFeatures(...features) {
|
|
127
|
+
var _a;
|
|
128
|
+
return this.with({
|
|
129
|
+
features: [...((_a = this.config.features) !== null && _a !== void 0 ? _a : []), ...features],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
mockedHandler(handlerName) {
|
|
133
|
+
var _a;
|
|
134
|
+
const mock = vi.fn();
|
|
135
|
+
if (this.treeInstance) {
|
|
136
|
+
(_a = this.treeInstance) === null || _a === void 0 ? void 0 : _a.setConfig((prev) => (Object.assign(Object.assign({}, prev), { [handlerName]: mock })));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.config[handlerName] = mock;
|
|
140
|
+
}
|
|
141
|
+
return mock;
|
|
142
|
+
}
|
|
143
|
+
item(itemId) {
|
|
144
|
+
return this.instance.getItemInstance(itemId);
|
|
145
|
+
}
|
|
146
|
+
reset() {
|
|
147
|
+
this.treeInstance = null;
|
|
148
|
+
TestTree.asyncLoaderResolvers = [];
|
|
149
|
+
}
|
|
150
|
+
debug() {
|
|
151
|
+
console.log(this.instance
|
|
152
|
+
.getItems()
|
|
153
|
+
.map((item) => [
|
|
154
|
+
" ".repeat(item.getItemMeta().level),
|
|
155
|
+
'"',
|
|
156
|
+
item.getItemName(),
|
|
157
|
+
'"',
|
|
158
|
+
].join(""))
|
|
159
|
+
.join("\n"));
|
|
160
|
+
}
|
|
161
|
+
setElementBoundingBox(itemId, bb = {
|
|
162
|
+
left: 0,
|
|
163
|
+
right: 100,
|
|
164
|
+
top: 0,
|
|
165
|
+
height: 20,
|
|
166
|
+
}) {
|
|
167
|
+
this.instance.getItemInstance(itemId).registerElement({
|
|
168
|
+
getBoundingClientRect: () => bb,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
static dragEvent(pageX = 1000, pageY = 0) {
|
|
172
|
+
return {
|
|
173
|
+
preventDefault: vi.fn(),
|
|
174
|
+
stopPropagation: vi.fn(),
|
|
175
|
+
dataTransfer: {
|
|
176
|
+
setData: vi.fn(),
|
|
177
|
+
getData: vi.fn(),
|
|
178
|
+
dropEffect: "unchaged-from-test",
|
|
179
|
+
},
|
|
180
|
+
pageX,
|
|
181
|
+
pageY,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
createTopDragEvent(indent = 0) {
|
|
185
|
+
return TestTree.dragEvent(indent * 20, 1);
|
|
186
|
+
}
|
|
187
|
+
createBottomDragEvent(indent = 0) {
|
|
188
|
+
return TestTree.dragEvent(indent * 20, 19);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
TestTree.asyncLoaderResolvers = [];
|
package/lib/esm/types/core.d.ts
CHANGED
|
@@ -10,11 +10,12 @@ import { RenamingFeatureDef } from "../features/renaming/types";
|
|
|
10
10
|
import { ExpandAllFeatureDef } from "../features/expand-all/types";
|
|
11
11
|
export type Updater<T> = T | ((old: T) => T);
|
|
12
12
|
export type SetStateFn<T> = (updaterOrValue: Updater<T>) => void;
|
|
13
|
+
type FunctionMap = Record<string, (...args: any[]) => any>;
|
|
13
14
|
export type FeatureDef = {
|
|
14
|
-
state:
|
|
15
|
-
config:
|
|
16
|
-
treeInstance:
|
|
17
|
-
itemInstance:
|
|
15
|
+
state: any;
|
|
16
|
+
config: any;
|
|
17
|
+
treeInstance: FunctionMap;
|
|
18
|
+
itemInstance: FunctionMap;
|
|
18
19
|
hotkeys: string;
|
|
19
20
|
};
|
|
20
21
|
export type EmptyFeatureDef = {
|
|
@@ -49,19 +50,34 @@ export interface ItemInstance<T> extends ItemInstanceType<T> {
|
|
|
49
50
|
export type HotkeyName<F extends FeatureDef = FeatureDefs<any>> = MergedFeatures<F>["hotkeys"];
|
|
50
51
|
export type HotkeysConfig<T, F extends FeatureDef = FeatureDefs<T>> = Record<HotkeyName<F>, HotkeyConfig<T>>;
|
|
51
52
|
export type CustomHotkeysConfig<T, F extends FeatureDef = FeatureDefs<T>> = Partial<Record<HotkeyName<F> | `custom${string}`, Partial<HotkeyConfig<T>>>>;
|
|
52
|
-
|
|
53
|
+
type MayReturnNull<T extends (...x: any[]) => any> = (...args: Parameters<T>) => ReturnType<T> | null;
|
|
54
|
+
export type ItemInstanceOpts<ItemInstance extends FunctionMap = FunctionMap, TreeInstance extends FunctionMap = FunctionMap, Key extends keyof ItemInstance = any> = {
|
|
55
|
+
item: ItemInstance;
|
|
56
|
+
tree: TreeInstance;
|
|
57
|
+
itemId: string;
|
|
58
|
+
prev?: MayReturnNull<ItemInstance[Key]>;
|
|
59
|
+
};
|
|
60
|
+
export type TreeInstanceOpts<TreeInstance extends FunctionMap = FunctionMap, Key extends keyof TreeInstance = any> = {
|
|
61
|
+
tree: TreeInstance;
|
|
62
|
+
prev?: MayReturnNull<TreeInstance[Key]>;
|
|
63
|
+
};
|
|
64
|
+
export type FeatureImplementation<T = any, SelfFeatureDef extends FeatureDef = any, DepFeaturesDef extends FeatureDef = any> = {
|
|
53
65
|
key?: string;
|
|
54
66
|
deps?: string[];
|
|
55
67
|
overwrites?: string[];
|
|
56
|
-
stateHandlerNames?: Partial<Record<keyof MergedFeatures<
|
|
57
|
-
getInitialState?: (initialState: Partial<MergedFeatures<
|
|
58
|
-
getDefaultConfig?: (defaultConfig: Partial<MergedFeatures<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
stateHandlerNames?: Partial<Record<keyof MergedFeatures<DepFeaturesDef>["state"], keyof MergedFeatures<DepFeaturesDef>["config"]>>;
|
|
69
|
+
getInitialState?: (initialState: Partial<MergedFeatures<DepFeaturesDef>["state"]>, tree: MergedFeatures<DepFeaturesDef>["treeInstance"]) => Partial<SelfFeatureDef["state"] & MergedFeatures<DepFeaturesDef>["state"]>;
|
|
70
|
+
getDefaultConfig?: (defaultConfig: Partial<MergedFeatures<DepFeaturesDef>["config"]>, tree: MergedFeatures<DepFeaturesDef>["treeInstance"]) => Partial<SelfFeatureDef["config"] & MergedFeatures<DepFeaturesDef>["config"]>;
|
|
71
|
+
treeInstance?: {
|
|
72
|
+
[key in keyof (SelfFeatureDef["treeInstance"] & MergedFeatures<DepFeaturesDef>["treeInstance"])]?: (opts: TreeInstanceOpts<SelfFeatureDef["treeInstance"] & MergedFeatures<DepFeaturesDef>["treeInstance"], key>, ...args: Parameters<(SelfFeatureDef["treeInstance"] & MergedFeatures<DepFeaturesDef>["treeInstance"])[key]>) => void;
|
|
73
|
+
};
|
|
74
|
+
itemInstance?: {
|
|
75
|
+
[key in keyof (SelfFeatureDef["itemInstance"] & MergedFeatures<DepFeaturesDef>["itemInstance"])]?: (opts: ItemInstanceOpts<SelfFeatureDef["itemInstance"] & MergedFeatures<DepFeaturesDef>["itemInstance"], SelfFeatureDef["treeInstance"] & MergedFeatures<DepFeaturesDef>["treeInstance"], key>, ...args: Parameters<(SelfFeatureDef["itemInstance"] & MergedFeatures<DepFeaturesDef>["itemInstance"])[key]>) => void;
|
|
76
|
+
};
|
|
77
|
+
onTreeMount?: (instance: MergedFeatures<DepFeaturesDef>["treeInstance"], treeElement: HTMLElement) => void;
|
|
78
|
+
onTreeUnmount?: (instance: MergedFeatures<DepFeaturesDef>["treeInstance"], treeElement: HTMLElement) => void;
|
|
79
|
+
onItemMount?: (instance: MergedFeatures<DepFeaturesDef>["itemInstance"], itemElement: HTMLElement, tree: MergedFeatures<DepFeaturesDef>["treeInstance"]) => void;
|
|
80
|
+
onItemUnmount?: (instance: MergedFeatures<DepFeaturesDef>["itemInstance"], itemElement: HTMLElement, tree: MergedFeatures<DepFeaturesDef>["treeInstance"]) => void;
|
|
81
|
+
hotkeys?: HotkeysConfig<T, SelfFeatureDef>;
|
|
66
82
|
};
|
|
67
83
|
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const throwError: (message: string) => Error;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const throwError = (message) => Error(`Headless Tree: ${message}`);
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
export const insertItemsAtTarget = (itemIds, target, onChangeChildren) => {
|
|
2
2
|
// add moved items to new common parent, if dropped onto parent
|
|
3
3
|
if (target.childIndex === null) {
|
|
4
|
-
|
|
4
|
+
const newChildren = [
|
|
5
5
|
...target.item.getChildren().map((item) => item.getId()),
|
|
6
6
|
...itemIds,
|
|
7
|
-
]
|
|
8
|
-
|
|
7
|
+
];
|
|
8
|
+
onChangeChildren(target.item, newChildren);
|
|
9
|
+
if (target.item && "updateCachedChildrenIds" in target.item) {
|
|
10
|
+
target.item.updateCachedChildrenIds(newChildren);
|
|
11
|
+
}
|
|
12
|
+
target.item.getTree().rebuildTree();
|
|
9
13
|
return;
|
|
10
14
|
}
|
|
11
15
|
// add moved items to new common parent, if dropped between siblings
|
|
@@ -16,5 +20,8 @@ export const insertItemsAtTarget = (itemIds, target, onChangeChildren) => {
|
|
|
16
20
|
...oldChildren.slice(target.insertionIndex).map((item) => item.getId()),
|
|
17
21
|
];
|
|
18
22
|
onChangeChildren(target.item, newChildren);
|
|
23
|
+
if (target.item && "updateCachedChildrenIds" in target.item) {
|
|
24
|
+
target.item.updateCachedChildrenIds(newChildren);
|
|
25
|
+
}
|
|
19
26
|
target.item.getTree().rebuildTree();
|
|
20
27
|
};
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
export const removeItemsFromParents = (movedItems, onChangeChildren) => {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
const movedItemsIds = movedItems.map((item) => item.getId());
|
|
3
|
+
const uniqueParents = [
|
|
4
|
+
...new Set(movedItems.map((item) => item.getParent())),
|
|
5
|
+
];
|
|
6
|
+
for (const parent of uniqueParents) {
|
|
7
|
+
const siblings = parent === null || parent === void 0 ? void 0 : parent.getChildren();
|
|
8
|
+
if (siblings && parent) {
|
|
9
|
+
const newChildren = siblings
|
|
10
|
+
.filter((sibling) => !movedItemsIds.includes(sibling.getId()))
|
|
11
|
+
.map((i) => i.getId());
|
|
12
|
+
onChangeChildren(parent, newChildren);
|
|
13
|
+
if (parent && "updateCachedChildrenIds" in parent) {
|
|
14
|
+
parent === null || parent === void 0 ? void 0 : parent.updateCachedChildrenIds(newChildren);
|
|
15
|
+
}
|
|
10
16
|
}
|
|
11
17
|
}
|
|
12
18
|
movedItems[0].getTree().rebuildTree();
|
package/lib/esm/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { TreeState, Updater } from "./types/core";
|
|
1
|
+
import { SetStateFn, TreeState, Updater } from "./types/core";
|
|
2
2
|
export type NoInfer<T> = [T][T extends any ? 0 : never];
|
|
3
|
-
export declare const memo: <D extends readonly any[], R>(
|
|
3
|
+
export declare const memo: <D extends readonly any[], P extends readonly any[], R>(deps: (...args: [...P]) => [...D], fn: (...args: [...D]) => R) => (...a: P) => R;
|
|
4
4
|
export declare function functionalUpdate<T>(updater: Updater<T>, input: T): T;
|
|
5
|
-
export declare function makeStateUpdater<K extends keyof TreeState<any>>(key: K, instance: unknown):
|
|
5
|
+
export declare function makeStateUpdater<K extends keyof TreeState<any>>(key: K, instance: unknown): SetStateFn<TreeState<any>[K]>;
|
|
6
6
|
export declare const poll: (fn: () => boolean, interval?: number, timeout?: number) => Promise<void>;
|
package/lib/esm/utils.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export const memo = (
|
|
1
|
+
export const memo = (deps, fn) => {
|
|
2
2
|
let value;
|
|
3
3
|
let oldDeps = null;
|
|
4
|
-
return () => {
|
|
5
|
-
const newDeps = deps();
|
|
4
|
+
return (...a) => {
|
|
5
|
+
const newDeps = deps(...a);
|
|
6
6
|
if (!value) {
|
|
7
7
|
value = fn(...newDeps);
|
|
8
8
|
oldDeps = newDeps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@headless-tree/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"main": "lib/cjs/index.js",
|
|
5
6
|
"module": "lib/esm/index.js",
|
|
6
7
|
"types": "lib/esm/index.d.ts",
|
|
@@ -8,7 +9,8 @@
|
|
|
8
9
|
"scripts": {
|
|
9
10
|
"build:cjs": "tsc -m commonjs --outDir lib/cjs",
|
|
10
11
|
"build:esm": "tsc",
|
|
11
|
-
"start": "tsc -w"
|
|
12
|
+
"start": "tsc -w",
|
|
13
|
+
"test": "vitest run"
|
|
12
14
|
},
|
|
13
15
|
"repository": {
|
|
14
16
|
"type": "git",
|
|
@@ -18,6 +20,8 @@
|
|
|
18
20
|
"author": "Lukas Bach <npm@lukasbach.com>",
|
|
19
21
|
"license": "MIT",
|
|
20
22
|
"devDependencies": {
|
|
21
|
-
"
|
|
23
|
+
"jsdom": "^26.0.0",
|
|
24
|
+
"typescript": "^5.7.3",
|
|
25
|
+
"vitest": "^3.0.3"
|
|
22
26
|
}
|
|
23
27
|
}
|