tree-element 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +221 -0
- package/lib/animation.d.ts +5 -0
- package/lib/animation.d.ts.map +1 -0
- package/lib/animation.js +39 -0
- package/lib/animation.js.map +1 -0
- package/lib/classNames.d.ts +34 -0
- package/lib/classNames.d.ts.map +1 -0
- package/lib/classNames.js +39 -0
- package/lib/classNames.js.map +1 -0
- package/lib/dataLoader.d.ts +25 -0
- package/lib/dataLoader.d.ts.map +1 -0
- package/lib/dataLoader.js +69 -0
- package/lib/dataLoader.js.map +1 -0
- package/lib/dragAndDropHandler/binarySearch.d.ts +3 -0
- package/lib/dragAndDropHandler/binarySearch.d.ts.map +1 -0
- package/lib/dragAndDropHandler/binarySearch.js +23 -0
- package/lib/dragAndDropHandler/binarySearch.js.map +1 -0
- package/lib/dragAndDropHandler/dragElement.d.ts +20 -0
- package/lib/dragAndDropHandler/dragElement.d.ts.map +1 -0
- package/lib/dragAndDropHandler/dragElement.js +39 -0
- package/lib/dragAndDropHandler/dragElement.js.map +1 -0
- package/lib/dragAndDropHandler/generateHitAreas.d.ts +13 -0
- package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -0
- package/lib/dragAndDropHandler/generateHitAreas.js +130 -0
- package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -0
- package/lib/dragAndDropHandler/index.d.ts +76 -0
- package/lib/dragAndDropHandler/index.d.ts.map +1 -0
- package/lib/dragAndDropHandler/index.js +311 -0
- package/lib/dragAndDropHandler/index.js.map +1 -0
- package/lib/dragAndDropHandler/iterateVisibleNodes.d.ts +11 -0
- package/lib/dragAndDropHandler/iterateVisibleNodes.d.ts.map +1 -0
- package/lib/dragAndDropHandler/iterateVisibleNodes.js +54 -0
- package/lib/dragAndDropHandler/iterateVisibleNodes.js.map +1 -0
- package/lib/dragAndDropHandler/types.d.ts +11 -0
- package/lib/dragAndDropHandler/types.d.ts.map +1 -0
- package/lib/elementsRenderer.d.ts +53 -0
- package/lib/elementsRenderer.d.ts.map +1 -0
- package/lib/elementsRenderer.js +251 -0
- package/lib/elementsRenderer.js.map +1 -0
- package/lib/events.d.ts +224 -0
- package/lib/events.d.ts.map +1 -0
- package/lib/index.d.ts +367 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1098 -0
- package/lib/index.js.map +1 -0
- package/lib/keyHandler.d.ts +29 -0
- package/lib/keyHandler.d.ts.map +1 -0
- package/lib/keyHandler.js +107 -0
- package/lib/keyHandler.js.map +1 -0
- package/lib/methodTypes.d.ts +21 -0
- package/lib/methodTypes.d.ts.map +1 -0
- package/lib/mouseHandler.d.ts +60 -0
- package/lib/mouseHandler.d.ts.map +1 -0
- package/lib/mouseHandler.js +283 -0
- package/lib/mouseHandler.js.map +1 -0
- package/lib/mouseUtils.d.ts +9 -0
- package/lib/mouseUtils.d.ts.map +1 -0
- package/lib/mouseUtils.js +15 -0
- package/lib/mouseUtils.js.map +1 -0
- package/lib/node.d.ts +320 -0
- package/lib/node.d.ts.map +1 -0
- package/lib/node.js +737 -0
- package/lib/node.js.map +1 -0
- package/lib/nodeElement/borderDropHint.d.ts +9 -0
- package/lib/nodeElement/borderDropHint.d.ts.map +1 -0
- package/lib/nodeElement/borderDropHint.js +24 -0
- package/lib/nodeElement/borderDropHint.js.map +1 -0
- package/lib/nodeElement/folderElement.d.ts +22 -0
- package/lib/nodeElement/folderElement.d.ts.map +1 -0
- package/lib/nodeElement/folderElement.js +100 -0
- package/lib/nodeElement/folderElement.js.map +1 -0
- package/lib/nodeElement/ghostDropHint.d.ts +18 -0
- package/lib/nodeElement/ghostDropHint.d.ts.map +1 -0
- package/lib/nodeElement/ghostDropHint.js +67 -0
- package/lib/nodeElement/ghostDropHint.js.map +1 -0
- package/lib/nodeElement/index.d.ts +28 -0
- package/lib/nodeElement/index.d.ts.map +1 -0
- package/lib/nodeElement/index.js +66 -0
- package/lib/nodeElement/index.js.map +1 -0
- package/lib/nodeUtils.d.ts +7 -0
- package/lib/nodeUtils.d.ts.map +1 -0
- package/lib/nodeUtils.js +4 -0
- package/lib/nodeUtils.js.map +1 -0
- package/lib/options.d.ts +310 -0
- package/lib/options.d.ts.map +1 -0
- package/lib/package.json +3 -0
- package/lib/positionUtils.d.ts +8 -0
- package/lib/positionUtils.d.ts.map +1 -0
- package/lib/positionUtils.js +14 -0
- package/lib/positionUtils.js.map +1 -0
- package/lib/requestUrl.d.ts +9 -0
- package/lib/requestUrl.d.ts.map +1 -0
- package/lib/requestUrl.js +37 -0
- package/lib/requestUrl.js.map +1 -0
- package/lib/saveStateHandler.d.ts +46 -0
- package/lib/saveStateHandler.d.ts.map +1 -0
- package/lib/saveStateHandler.js +204 -0
- package/lib/saveStateHandler.js.map +1 -0
- package/lib/scrollHandler/containerScrollParent.d.ts +12 -0
- package/lib/scrollHandler/containerScrollParent.d.ts.map +1 -0
- package/lib/scrollHandler/containerScrollParent.js +49 -0
- package/lib/scrollHandler/containerScrollParent.js.map +1 -0
- package/lib/scrollHandler/createScrollParent.d.ts +4 -0
- package/lib/scrollHandler/createScrollParent.d.ts.map +1 -0
- package/lib/scrollHandler/createScrollParent.js +38 -0
- package/lib/scrollHandler/createScrollParent.js.map +1 -0
- package/lib/scrollHandler/documentScrollParent.d.ts +22 -0
- package/lib/scrollHandler/documentScrollParent.d.ts.map +1 -0
- package/lib/scrollHandler/documentScrollParent.js +71 -0
- package/lib/scrollHandler/documentScrollParent.js.map +1 -0
- package/lib/scrollHandler/scrollParent.d.ts +26 -0
- package/lib/scrollHandler/scrollParent.d.ts.map +1 -0
- package/lib/scrollHandler/scrollParent.js +79 -0
- package/lib/scrollHandler/scrollParent.js.map +1 -0
- package/lib/scrollHandler.d.ts +20 -0
- package/lib/scrollHandler.d.ts.map +1 -0
- package/lib/scrollHandler.js +41 -0
- package/lib/scrollHandler.js.map +1 -0
- package/lib/selectNodeHandler.d.ts +38 -0
- package/lib/selectNodeHandler.d.ts.map +1 -0
- package/lib/selectNodeHandler.js +166 -0
- package/lib/selectNodeHandler.js.map +1 -0
- package/lib/setDefaultOptions.d.ts +4 -0
- package/lib/setDefaultOptions.d.ts.map +1 -0
- package/lib/setDefaultOptions.js +86 -0
- package/lib/setDefaultOptions.js.map +1 -0
- package/lib/triggerCustomEvent.d.ts +3 -0
- package/lib/triggerCustomEvent.d.ts.map +1 -0
- package/lib/triggerCustomEvent.js +13 -0
- package/lib/triggerCustomEvent.js.map +1 -0
- package/lib/util.d.ts +3 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +5 -0
- package/lib/util.js.map +1 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +5 -0
- package/lib/version.js.map +1 -0
- package/package.json +110 -0
- package/src/animation.ts +48 -0
- package/src/classNames.ts +70 -0
- package/src/dataLoader.ts +92 -0
- package/src/dragAndDropHandler/binarySearch.ts +27 -0
- package/src/dragAndDropHandler/dragElement.ts +62 -0
- package/src/dragAndDropHandler/generateHitAreas.ts +195 -0
- package/src/dragAndDropHandler/index.ts +442 -0
- package/src/dragAndDropHandler/iterateVisibleNodes.ts +91 -0
- package/src/dragAndDropHandler/types.ts +12 -0
- package/src/elementsRenderer.ts +367 -0
- package/src/events.ts +237 -0
- package/src/header.txt +16 -0
- package/src/index.ts +1290 -0
- package/src/keyHandler.ts +139 -0
- package/src/methodTypes.ts +44 -0
- package/src/mouseHandler.ts +391 -0
- package/src/mouseUtils.ts +23 -0
- package/src/node.ts +831 -0
- package/src/nodeElement/borderDropHint.ts +37 -0
- package/src/nodeElement/folderElement.ts +140 -0
- package/src/nodeElement/ghostDropHint.ts +84 -0
- package/src/nodeElement/index.ts +103 -0
- package/src/nodeUtils.ts +12 -0
- package/src/options.ts +325 -0
- package/src/positionUtils.ts +18 -0
- package/src/requestUrl.ts +41 -0
- package/src/saveStateHandler.ts +279 -0
- package/src/scrollHandler/containerScrollParent.ts +71 -0
- package/src/scrollHandler/createScrollParent.ts +51 -0
- package/src/scrollHandler/documentScrollParent.ts +104 -0
- package/src/scrollHandler/scrollParent.ts +117 -0
- package/src/scrollHandler.ts +55 -0
- package/src/selectNodeHandler.ts +192 -0
- package/src/setDefaultOptions.ts +79 -0
- package/src/triggerCustomEvent.ts +14 -0
- package/src/util.ts +5 -0
- package/src/version.ts +4 -0
- package/tree_element.css +195 -0
- package/tree_element.debug.js +4153 -0
- package/tree_element.debug.js.map +1 -0
- package/tree_element.js +21 -0
- package/tree_element.js.map +1 -0
package/src/node.ts
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A node reads and writes the private members of other nodes (node.setParent),
|
|
3
|
+
so the `_` prefix that the build adds cannot be limited to `this.`:
|
|
4
|
+
|
|
5
|
+
prefix-private-members: all
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isNodeRecordWithChildren } from "./nodeUtils";
|
|
9
|
+
|
|
10
|
+
export type NodeData = NodeRecord | string;
|
|
11
|
+
|
|
12
|
+
export type NodeId = number | string;
|
|
13
|
+
|
|
14
|
+
export interface NodeRecord {
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
children?: NodeData[];
|
|
17
|
+
id?: NodeId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type Position = "after" | "before" | "inside";
|
|
21
|
+
|
|
22
|
+
type IterateCallback = (node: Node, level: number) => boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @groupDescription Properties
|
|
26
|
+
* Any other key in the node data is copied onto the node, so
|
|
27
|
+
* `{ name: "node1", color: "green" }` gives you `node.color`.
|
|
28
|
+
*
|
|
29
|
+
* @groupDescription Searching
|
|
30
|
+
* These work on any node, and search that node's subtree. Called on the root
|
|
31
|
+
* node, they search the whole tree — which is what the tree's own methods of
|
|
32
|
+
* the same name do.
|
|
33
|
+
*
|
|
34
|
+
* @groupDescription Changing the tree
|
|
35
|
+
* ::: warning
|
|
36
|
+
* These methods change the data without re-rendering. The tree's
|
|
37
|
+
* [methods](/reference/methods) — `appendNode`, `removeNode`, `moveNode` and
|
|
38
|
+
* friends — do the same and refresh the display, so prefer those. If you do
|
|
39
|
+
* use these, call `tree.refresh()` afterwards.
|
|
40
|
+
* :::
|
|
41
|
+
*/
|
|
42
|
+
export class Node {
|
|
43
|
+
/** @hidden */
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
|
|
46
|
+
/** The child nodes. */
|
|
47
|
+
public children: Node[];
|
|
48
|
+
/** The `li` element, once the node is rendered. */
|
|
49
|
+
public element?: HTMLElement;
|
|
50
|
+
/** The id from the node data. */
|
|
51
|
+
public id?: NodeId;
|
|
52
|
+
/** @hidden */
|
|
53
|
+
public idMapping?: Map<NodeId, Node>;
|
|
54
|
+
/** Whether the node's children are being fetched. */
|
|
55
|
+
public is_loading?: boolean;
|
|
56
|
+
/** Whether the folder is open. */
|
|
57
|
+
public is_open?: boolean;
|
|
58
|
+
/** Whether the node data had an empty `children` array. */
|
|
59
|
+
public isEmptyFolder: boolean;
|
|
60
|
+
/** Whether the children still have to be fetched. */
|
|
61
|
+
public load_on_demand: boolean;
|
|
62
|
+
/** The label. Also settable from the `label` key in node data. */
|
|
63
|
+
public name: string;
|
|
64
|
+
/** @hidden */
|
|
65
|
+
public nodeClass?: typeof Node;
|
|
66
|
+
/** The parent; `null` for the root node. */
|
|
67
|
+
public parent: Node | null;
|
|
68
|
+
/** The root node. */
|
|
69
|
+
public tree?: Node;
|
|
70
|
+
|
|
71
|
+
/** @hidden */
|
|
72
|
+
constructor(
|
|
73
|
+
nodeData: NodeData | null = null,
|
|
74
|
+
isRoot = false,
|
|
75
|
+
nodeClass = Node,
|
|
76
|
+
) {
|
|
77
|
+
this.name = "";
|
|
78
|
+
this.load_on_demand = false;
|
|
79
|
+
|
|
80
|
+
this.isEmptyFolder =
|
|
81
|
+
nodeData != null &&
|
|
82
|
+
isNodeRecordWithChildren(nodeData) &&
|
|
83
|
+
nodeData.children.length === 0;
|
|
84
|
+
|
|
85
|
+
this.setData(nodeData);
|
|
86
|
+
|
|
87
|
+
this.children = [];
|
|
88
|
+
this.parent = null;
|
|
89
|
+
this.is_loading = undefined;
|
|
90
|
+
this.is_open ??= undefined;
|
|
91
|
+
|
|
92
|
+
if (isRoot) {
|
|
93
|
+
this.idMapping = new Map<NodeId, Node>();
|
|
94
|
+
this.tree = this;
|
|
95
|
+
this.nodeClass = nodeClass;
|
|
96
|
+
} else {
|
|
97
|
+
this.idMapping = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Adds a sibling after this node.
|
|
103
|
+
*
|
|
104
|
+
* @returns The new node, or `null` when the node has no parent.
|
|
105
|
+
* @group Changing the tree
|
|
106
|
+
*/
|
|
107
|
+
public addAfter(nodeInfo: NodeData): Node | null {
|
|
108
|
+
if (!this.parent) {
|
|
109
|
+
return null;
|
|
110
|
+
} else {
|
|
111
|
+
const node = this.createNode(nodeInfo);
|
|
112
|
+
|
|
113
|
+
const childIndex = this.parent.getChildIndex(this);
|
|
114
|
+
this.parent.addChildAtPosition(node, childIndex + 1);
|
|
115
|
+
|
|
116
|
+
node.loadChildrenFromData(nodeInfo);
|
|
117
|
+
return node;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Adds a sibling before this node.
|
|
123
|
+
*
|
|
124
|
+
* @returns The new node, or `null` when the node has no parent.
|
|
125
|
+
* @group Changing the tree
|
|
126
|
+
*/
|
|
127
|
+
public addBefore(nodeInfo: NodeData): Node | null {
|
|
128
|
+
if (!this.parent) {
|
|
129
|
+
return null;
|
|
130
|
+
} else {
|
|
131
|
+
const node = this.createNode(nodeInfo);
|
|
132
|
+
|
|
133
|
+
const childIndex = this.parent.getChildIndex(this);
|
|
134
|
+
this.parent.addChildAtPosition(node, childIndex);
|
|
135
|
+
|
|
136
|
+
node.loadChildrenFromData(nodeInfo);
|
|
137
|
+
return node;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Adds an existing `Node` object as the last child.
|
|
143
|
+
*
|
|
144
|
+
* @group Changing the tree
|
|
145
|
+
*/
|
|
146
|
+
public addChild(node: Node): void {
|
|
147
|
+
this.children.push(node);
|
|
148
|
+
node.setParent(this);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Adds an existing `Node` object as a child, at this position in
|
|
153
|
+
* `children`. The index starts at `0`.
|
|
154
|
+
*
|
|
155
|
+
* @group Changing the tree
|
|
156
|
+
*/
|
|
157
|
+
public addChildAtPosition(node: Node, index: number): void {
|
|
158
|
+
this.children.splice(index, 0, node);
|
|
159
|
+
node.setParent(this);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @hidden */
|
|
163
|
+
public addNodeToIndex(node: Node): void {
|
|
164
|
+
if (node.id != null) {
|
|
165
|
+
this.idMapping?.set(node.id, node);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Inserts a new parent between this node and its current parent.
|
|
171
|
+
*
|
|
172
|
+
* @returns The new node, or `null` when the node has no parent.
|
|
173
|
+
* @group Changing the tree
|
|
174
|
+
*/
|
|
175
|
+
public addParent(nodeInfo: NodeData): Node | null {
|
|
176
|
+
if (!this.parent) {
|
|
177
|
+
return null;
|
|
178
|
+
} else {
|
|
179
|
+
const newParent = this.createNode(nodeInfo);
|
|
180
|
+
|
|
181
|
+
if (this.tree) {
|
|
182
|
+
newParent.setParent(this.tree);
|
|
183
|
+
}
|
|
184
|
+
const originalParent = this.parent;
|
|
185
|
+
|
|
186
|
+
for (const child of originalParent.children) {
|
|
187
|
+
newParent.addChild(child);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
originalParent.children = [];
|
|
191
|
+
originalParent.addChild(newParent);
|
|
192
|
+
return newParent;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Adds a child at the end.
|
|
198
|
+
*
|
|
199
|
+
* @returns The new node.
|
|
200
|
+
* @group Changing the tree
|
|
201
|
+
*/
|
|
202
|
+
public append(nodeInfo: NodeData): Node {
|
|
203
|
+
const node = this.createNode(nodeInfo);
|
|
204
|
+
this.addChild(node);
|
|
205
|
+
|
|
206
|
+
node.loadChildrenFromData(nodeInfo);
|
|
207
|
+
return node;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Returns all nodes in the subtree for which the callback returns
|
|
212
|
+
* `true`.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```js
|
|
216
|
+
* const folders = tree.getTree().filter((node) => node.isFolder());
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* @group Searching
|
|
220
|
+
*/
|
|
221
|
+
public filter(f: (node: Node) => boolean): Node[] {
|
|
222
|
+
const result: Node[] = [];
|
|
223
|
+
|
|
224
|
+
this.iterate((node: Node) => {
|
|
225
|
+
if (f(node)) {
|
|
226
|
+
result.push(node);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns the position of a child in `children`, or `-1`.
|
|
237
|
+
*
|
|
238
|
+
* @group Inspecting a node
|
|
239
|
+
*/
|
|
240
|
+
public getChildIndex(node: Node): number {
|
|
241
|
+
return this.children.indexOf(node);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Returns the subtree as plain data, ready for `JSON.stringify`.
|
|
246
|
+
* Internal properties (`parent`, `children`, `element`, `tree`,
|
|
247
|
+
* `idMapping`, `nodeClass`, `load_on_demand`, `isEmptyFolder`) are left
|
|
248
|
+
* out; your own properties are kept.
|
|
249
|
+
*
|
|
250
|
+
* ```js
|
|
251
|
+
* tree.getTree().getData();
|
|
252
|
+
* // [{ name: "node1", id: 1, children: [{ name: "child1", id: 2 }] }]
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* @param includeParent - Make the result the node itself rather than its
|
|
256
|
+
* children. Default `false`.
|
|
257
|
+
* @group Reading data back
|
|
258
|
+
*/
|
|
259
|
+
public getData(includeParent = false): NodeRecord[] {
|
|
260
|
+
const getDataFromNodes = (nodes: Node[]): Record<string, unknown>[] => {
|
|
261
|
+
return nodes.map((node) => {
|
|
262
|
+
const tmpNode: Record<string, unknown> = {};
|
|
263
|
+
|
|
264
|
+
for (const k in node) {
|
|
265
|
+
if (
|
|
266
|
+
[
|
|
267
|
+
"parent",
|
|
268
|
+
"children",
|
|
269
|
+
"element",
|
|
270
|
+
"idMapping",
|
|
271
|
+
"load_on_demand",
|
|
272
|
+
"nodeClass",
|
|
273
|
+
"tree",
|
|
274
|
+
"isEmptyFolder",
|
|
275
|
+
].indexOf(k) === -1 &&
|
|
276
|
+
Object.prototype.hasOwnProperty.call(node, k)
|
|
277
|
+
) {
|
|
278
|
+
const v = node[k];
|
|
279
|
+
tmpNode[k] = v;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (node.hasChildren()) {
|
|
284
|
+
tmpNode.children = getDataFromNodes(node.children);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return tmpNode;
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (includeParent) {
|
|
292
|
+
return getDataFromNodes([this]);
|
|
293
|
+
} else {
|
|
294
|
+
return getDataFromNodes(this.children);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Returns the last child, or `null`. When that child is an open folder,
|
|
300
|
+
* its own last child, and so on.
|
|
301
|
+
*
|
|
302
|
+
* @group Moving around the tree
|
|
303
|
+
*/
|
|
304
|
+
public getLastChild(): Node | null {
|
|
305
|
+
if (!this.hasChildren()) {
|
|
306
|
+
return null;
|
|
307
|
+
} else {
|
|
308
|
+
const lastChild = this.children[this.children.length - 1] as Node;
|
|
309
|
+
|
|
310
|
+
if (!(lastChild.hasChildren() && lastChild.is_open)) {
|
|
311
|
+
return lastChild;
|
|
312
|
+
} else {
|
|
313
|
+
return lastChild.getLastChild();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Returns the depth of the node, counting the top level as `1`.
|
|
320
|
+
*
|
|
321
|
+
* @group Inspecting a node
|
|
322
|
+
*/
|
|
323
|
+
public getLevel(): number {
|
|
324
|
+
let level = 0;
|
|
325
|
+
let node: Node = this; // eslint-disable-line @typescript-eslint/no-this-alias
|
|
326
|
+
|
|
327
|
+
while (node.parent) {
|
|
328
|
+
level += 1;
|
|
329
|
+
node = node.parent;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return level;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Returns the next node in the tree, regardless of whether it is
|
|
337
|
+
* visible. `getNextNode(false)` skips the node's own children.
|
|
338
|
+
*
|
|
339
|
+
* @group Moving around the tree
|
|
340
|
+
*/
|
|
341
|
+
public getNextNode(includeChildren = true): Node | null {
|
|
342
|
+
if (includeChildren && this.hasChildren()) {
|
|
343
|
+
return this.children[0] ?? null;
|
|
344
|
+
} else if (!this.parent) {
|
|
345
|
+
return null;
|
|
346
|
+
} else {
|
|
347
|
+
const nextSibling = this.getNextSibling();
|
|
348
|
+
|
|
349
|
+
if (nextSibling) {
|
|
350
|
+
return nextSibling;
|
|
351
|
+
} else {
|
|
352
|
+
return this.parent.getNextNode(false);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Returns the next sibling, or `null`.
|
|
359
|
+
*
|
|
360
|
+
* @group Moving around the tree
|
|
361
|
+
*/
|
|
362
|
+
public getNextSibling(): Node | null {
|
|
363
|
+
if (!this.parent) {
|
|
364
|
+
return null;
|
|
365
|
+
} else {
|
|
366
|
+
const nextIndex = this.parent.getChildIndex(this) + 1;
|
|
367
|
+
if (nextIndex < this.parent.children.length) {
|
|
368
|
+
return this.parent.children[nextIndex] ?? null;
|
|
369
|
+
} else {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Like `getNextNode`, but skipping nodes inside closed folders — this is
|
|
377
|
+
* what the arrow keys use.
|
|
378
|
+
*
|
|
379
|
+
* @group Moving around the tree
|
|
380
|
+
*/
|
|
381
|
+
public getNextVisibleNode(): Node | null {
|
|
382
|
+
if (this.hasChildren() && this.is_open) {
|
|
383
|
+
// First child
|
|
384
|
+
return this.children[0] ?? null;
|
|
385
|
+
} else {
|
|
386
|
+
if (!this.parent) {
|
|
387
|
+
return null;
|
|
388
|
+
} else {
|
|
389
|
+
const nextSibling = this.getNextSibling();
|
|
390
|
+
if (nextSibling) {
|
|
391
|
+
// Next sibling
|
|
392
|
+
return nextSibling;
|
|
393
|
+
} else {
|
|
394
|
+
// Next node of parent
|
|
395
|
+
return this.parent.getNextNode(false);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Returns the first node for which the callback returns `true`.
|
|
403
|
+
*
|
|
404
|
+
* @group Searching
|
|
405
|
+
*/
|
|
406
|
+
public getNodeByCallback(callback: (node: Node) => boolean): Node | null {
|
|
407
|
+
let result: Node | null = null;
|
|
408
|
+
|
|
409
|
+
this.iterate((node: Node) => {
|
|
410
|
+
if (result) {
|
|
411
|
+
return false;
|
|
412
|
+
} else if (callback(node)) {
|
|
413
|
+
result = node;
|
|
414
|
+
return false;
|
|
415
|
+
} else {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Returns the node with this id. Only available on the root node, which
|
|
425
|
+
* keeps the id index.
|
|
426
|
+
*
|
|
427
|
+
* @group Searching
|
|
428
|
+
*/
|
|
429
|
+
public getNodeById(nodeId: NodeId): Node | null {
|
|
430
|
+
return this.idMapping?.get(nodeId) ?? null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Returns the first node with this name.
|
|
435
|
+
*
|
|
436
|
+
* @group Searching
|
|
437
|
+
*/
|
|
438
|
+
public getNodeByName(name: string): Node | null {
|
|
439
|
+
return this.getNodeByCallback((node: Node) => node.name === name);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Like `getNodeByName`, but throws when there is no such node.
|
|
444
|
+
*
|
|
445
|
+
* @group Searching
|
|
446
|
+
*/
|
|
447
|
+
public getNodeByNameMustExist(name: string): Node {
|
|
448
|
+
const node = this.getNodeByCallback((n: Node) => n.name === name);
|
|
449
|
+
|
|
450
|
+
if (!node) {
|
|
451
|
+
throw new Error(`Node with name ${name} not found`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return node;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Returns all nodes with this property value.
|
|
459
|
+
*
|
|
460
|
+
* @group Searching
|
|
461
|
+
*/
|
|
462
|
+
public getNodesByProperty(key: string, value: unknown): Node[] {
|
|
463
|
+
return this.filter((node: Node) => node[key] === value);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Returns the parent: `null` for a top-level node — the root node is not
|
|
468
|
+
* returned.
|
|
469
|
+
*
|
|
470
|
+
* @group Moving around the tree
|
|
471
|
+
*/
|
|
472
|
+
public getParent(): Node | null {
|
|
473
|
+
// Return parent except if it is the root node
|
|
474
|
+
if (!this.parent) {
|
|
475
|
+
return null;
|
|
476
|
+
} else if (!this.parent.parent) {
|
|
477
|
+
// Root node -> null
|
|
478
|
+
return null;
|
|
479
|
+
} else {
|
|
480
|
+
return this.parent;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Returns the previous node in the tree, regardless of whether it is
|
|
486
|
+
* visible.
|
|
487
|
+
*
|
|
488
|
+
* @group Moving around the tree
|
|
489
|
+
*/
|
|
490
|
+
public getPreviousNode(): Node | null {
|
|
491
|
+
if (!this.parent) {
|
|
492
|
+
return null;
|
|
493
|
+
} else {
|
|
494
|
+
const previousSibling = this.getPreviousSibling();
|
|
495
|
+
|
|
496
|
+
if (!previousSibling) {
|
|
497
|
+
return this.getParent();
|
|
498
|
+
} else if (previousSibling.hasChildren()) {
|
|
499
|
+
return previousSibling.getLastChild();
|
|
500
|
+
} else {
|
|
501
|
+
return previousSibling;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Returns the previous sibling, or `null`.
|
|
508
|
+
*
|
|
509
|
+
* @group Moving around the tree
|
|
510
|
+
*/
|
|
511
|
+
public getPreviousSibling(): Node | null {
|
|
512
|
+
if (!this.parent) {
|
|
513
|
+
return null;
|
|
514
|
+
} else {
|
|
515
|
+
const previousIndex = this.parent.getChildIndex(this) - 1;
|
|
516
|
+
if (previousIndex >= 0) {
|
|
517
|
+
return this.parent.children[previousIndex] ?? null;
|
|
518
|
+
} else {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Like `getPreviousNode`, but skipping nodes inside closed folders —
|
|
526
|
+
* this is what the arrow keys use.
|
|
527
|
+
*
|
|
528
|
+
* @group Moving around the tree
|
|
529
|
+
*/
|
|
530
|
+
public getPreviousVisibleNode(): Node | null {
|
|
531
|
+
if (!this.parent) {
|
|
532
|
+
return null;
|
|
533
|
+
} else {
|
|
534
|
+
const previousSibling = this.getPreviousSibling();
|
|
535
|
+
|
|
536
|
+
if (!previousSibling) {
|
|
537
|
+
return this.getParent();
|
|
538
|
+
} else if (
|
|
539
|
+
!previousSibling.hasChildren() ||
|
|
540
|
+
!previousSibling.is_open
|
|
541
|
+
) {
|
|
542
|
+
// Previous sibling
|
|
543
|
+
return previousSibling;
|
|
544
|
+
} else {
|
|
545
|
+
// Last child of previous sibling
|
|
546
|
+
return previousSibling.getLastChild();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Whether the node has children.
|
|
553
|
+
*
|
|
554
|
+
* @group Inspecting a node
|
|
555
|
+
*/
|
|
556
|
+
public hasChildren(): boolean {
|
|
557
|
+
return this.children.length !== 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Init Node from data without making it the root of the tree.
|
|
562
|
+
*
|
|
563
|
+
* @hidden
|
|
564
|
+
*/
|
|
565
|
+
public initFromData(data: NodeData): void {
|
|
566
|
+
const addNode = (nodeData: NodeData): void => {
|
|
567
|
+
this.setData(nodeData);
|
|
568
|
+
|
|
569
|
+
if (
|
|
570
|
+
isNodeRecordWithChildren(nodeData) &&
|
|
571
|
+
nodeData.children.length
|
|
572
|
+
) {
|
|
573
|
+
addChildren(nodeData.children);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const addChildren = (childrenData: NodeData[]): void => {
|
|
578
|
+
for (const child of childrenData) {
|
|
579
|
+
const node = this.createNode();
|
|
580
|
+
node.initFromData(child);
|
|
581
|
+
this.addChild(node);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
addNode(data);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* `true` when the node has children, or is marked `load_on_demand`.
|
|
590
|
+
*
|
|
591
|
+
* @group Inspecting a node
|
|
592
|
+
*/
|
|
593
|
+
public isFolder(): boolean {
|
|
594
|
+
return this.hasChildren() || this.load_on_demand;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Whether this node is an ancestor of the other node.
|
|
599
|
+
*
|
|
600
|
+
* @group Inspecting a node
|
|
601
|
+
*/
|
|
602
|
+
public isParentOf(node: Node): boolean {
|
|
603
|
+
let parent = node.parent;
|
|
604
|
+
|
|
605
|
+
while (parent) {
|
|
606
|
+
if (parent === this) {
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
parent = parent.parent;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Walks the subtree, calling the callback with `(node, level)`. Return
|
|
618
|
+
* `false` from the callback to stop descending into that node:
|
|
619
|
+
*
|
|
620
|
+
* ```js
|
|
621
|
+
* tree.getTree().iterate((node, level) => {
|
|
622
|
+
* console.log(" ".repeat(level) + node.name);
|
|
623
|
+
* return level <= 2; // don't go deeper than level 2
|
|
624
|
+
* });
|
|
625
|
+
* ```
|
|
626
|
+
*
|
|
627
|
+
* @group Searching
|
|
628
|
+
*/
|
|
629
|
+
public iterate(callback: IterateCallback): void {
|
|
630
|
+
const _iterate = (node: Node, level: number): void => {
|
|
631
|
+
for (const child of node.children) {
|
|
632
|
+
const result = callback(child, level);
|
|
633
|
+
|
|
634
|
+
if (result && child.hasChildren()) {
|
|
635
|
+
_iterate(child, level + 1);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
_iterate(this, 0);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Replaces the children with new node data.
|
|
645
|
+
*
|
|
646
|
+
* @group Changing the tree
|
|
647
|
+
*/
|
|
648
|
+
public loadFromData(data: NodeData[]): this {
|
|
649
|
+
this.removeChildren();
|
|
650
|
+
|
|
651
|
+
for (const childData of data) {
|
|
652
|
+
const node = this.createNode(childData);
|
|
653
|
+
this.addChild(node);
|
|
654
|
+
|
|
655
|
+
if (isNodeRecordWithChildren(childData)) {
|
|
656
|
+
node.loadFromData(childData.children);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Moves a node relative to another node. Called on the root node.
|
|
665
|
+
*
|
|
666
|
+
* @param position - `"before"`, `"after"` or `"inside"`.
|
|
667
|
+
* @returns `false` when the move is impossible, for instance moving a
|
|
668
|
+
* node into its own subtree.
|
|
669
|
+
* @group Changing the tree
|
|
670
|
+
*/
|
|
671
|
+
public moveNode(
|
|
672
|
+
movedNode: Node,
|
|
673
|
+
targetNode: Node,
|
|
674
|
+
position: Position,
|
|
675
|
+
): boolean {
|
|
676
|
+
if (!movedNode.parent || movedNode.isParentOf(targetNode)) {
|
|
677
|
+
// - Node is parent of target node
|
|
678
|
+
// - Or, parent is empty
|
|
679
|
+
return false;
|
|
680
|
+
} else {
|
|
681
|
+
movedNode.parent.doRemoveChild(movedNode);
|
|
682
|
+
|
|
683
|
+
switch (position) {
|
|
684
|
+
case "after": {
|
|
685
|
+
if (targetNode.parent) {
|
|
686
|
+
targetNode.parent.addChildAtPosition(
|
|
687
|
+
movedNode,
|
|
688
|
+
targetNode.parent.getChildIndex(targetNode) + 1,
|
|
689
|
+
);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
case "before": {
|
|
696
|
+
if (targetNode.parent) {
|
|
697
|
+
targetNode.parent.addChildAtPosition(
|
|
698
|
+
movedNode,
|
|
699
|
+
targetNode.parent.getChildIndex(targetNode),
|
|
700
|
+
);
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
case "inside": {
|
|
707
|
+
// move inside as first child
|
|
708
|
+
targetNode.addChildAtPosition(movedNode, 0);
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Adds a child at the start.
|
|
717
|
+
*
|
|
718
|
+
* @returns The new node.
|
|
719
|
+
* @group Changing the tree
|
|
720
|
+
*/
|
|
721
|
+
public prepend(nodeInfo: NodeData): Node {
|
|
722
|
+
const node = this.createNode(nodeInfo);
|
|
723
|
+
this.addChildAtPosition(node, 0);
|
|
724
|
+
|
|
725
|
+
node.loadChildrenFromData(nodeInfo);
|
|
726
|
+
return node;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Removes this node from its parent.
|
|
731
|
+
*
|
|
732
|
+
* @group Changing the tree
|
|
733
|
+
*/
|
|
734
|
+
public remove(): void {
|
|
735
|
+
if (this.parent) {
|
|
736
|
+
this.parent.removeChild(this);
|
|
737
|
+
this.parent = null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Removes a child and its children.
|
|
743
|
+
*
|
|
744
|
+
* @group Changing the tree
|
|
745
|
+
*/
|
|
746
|
+
public removeChild(node: Node): void {
|
|
747
|
+
// remove children from the index
|
|
748
|
+
node.removeChildren();
|
|
749
|
+
|
|
750
|
+
this.doRemoveChild(node);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Removes all children.
|
|
755
|
+
*
|
|
756
|
+
* @group Changing the tree
|
|
757
|
+
*/
|
|
758
|
+
public removeChildren(): void {
|
|
759
|
+
this.iterate((child: Node) => {
|
|
760
|
+
this.tree?.removeNodeFromIndex(child);
|
|
761
|
+
return true;
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
this.children = [];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** @hidden */
|
|
768
|
+
public removeNodeFromIndex(node: Node): void {
|
|
769
|
+
if (node.id != null) {
|
|
770
|
+
this.idMapping?.delete(node.id);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Updates the node's properties from node data. `children` and `parent`
|
|
776
|
+
* are ignored. A string sets the name. Existing node values are not
|
|
777
|
+
* removed.
|
|
778
|
+
*
|
|
779
|
+
* @group Changing the tree
|
|
780
|
+
*/
|
|
781
|
+
public setData(o: NodeData | null): void {
|
|
782
|
+
if (!o) {
|
|
783
|
+
return;
|
|
784
|
+
} else if (typeof o === "string") {
|
|
785
|
+
this.name = o;
|
|
786
|
+
} else if (typeof o === "object") {
|
|
787
|
+
for (const key in o) {
|
|
788
|
+
if (Object.prototype.hasOwnProperty.call(o, key)) {
|
|
789
|
+
const value = o[key];
|
|
790
|
+
|
|
791
|
+
if (key === "label" || key === "name") {
|
|
792
|
+
// You can use the 'label' key instead of 'name'; this is a legacy feature
|
|
793
|
+
if (typeof value === "string") {
|
|
794
|
+
this.name = value;
|
|
795
|
+
}
|
|
796
|
+
} else if (key !== "children" && key !== "parent") {
|
|
797
|
+
// You can't update the children or the parent using this function
|
|
798
|
+
this[key] = value;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private createNode(nodeData?: NodeData): Node {
|
|
806
|
+
const nodeClass = this.getNodeClass();
|
|
807
|
+
return new nodeClass(nodeData);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private doRemoveChild(node: Node): void {
|
|
811
|
+
this.children.splice(this.getChildIndex(node), 1);
|
|
812
|
+
this.tree?.removeNodeFromIndex(node);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private getNodeClass(): typeof Node {
|
|
816
|
+
return this.nodeClass ?? this.tree?.nodeClass ?? Node;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Load children data from nodeInfo if it has children
|
|
820
|
+
private loadChildrenFromData(nodeInfo: NodeData) {
|
|
821
|
+
if (isNodeRecordWithChildren(nodeInfo) && nodeInfo.children.length) {
|
|
822
|
+
this.loadFromData(nodeInfo.children);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private setParent(parent: Node): void {
|
|
827
|
+
this.parent = parent;
|
|
828
|
+
this.tree = parent.tree;
|
|
829
|
+
this.tree?.addNodeToIndex(this);
|
|
830
|
+
}
|
|
831
|
+
}
|