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