wunderbaum 0.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/src/wb_node.ts ADDED
@@ -0,0 +1,1780 @@
1
+ /*!
2
+ * Wunderbaum - wunderbaum_node
3
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
+ */
6
+
7
+ import "./wunderbaum.scss";
8
+ import * as util from "./util";
9
+
10
+ import { Wunderbaum } from "./wunderbaum";
11
+ import {
12
+ NavigationMode,
13
+ ChangeType,
14
+ iconMap,
15
+ ICON_WIDTH,
16
+ KEY_TO_ACTION_DICT,
17
+ makeNodeTitleMatcher,
18
+ MatcherType,
19
+ NodeAnyCallback,
20
+ NodeStatusType,
21
+ NodeVisitCallback,
22
+ NodeVisitResponse,
23
+ ROW_EXTRA_PAD,
24
+ ROW_HEIGHT,
25
+ TEST_IMG,
26
+ ApplyCommandType,
27
+ AddNodeType,
28
+ } from "./common";
29
+ import { Deferred } from "./deferred";
30
+ import { WbNodeData } from "./wb_options";
31
+
32
+ /** Top-level properties that can be passed with `data`. */
33
+ const NODE_PROPS = new Set<string>([
34
+ // TODO: use NODE_ATTRS instead?
35
+ "classes",
36
+ "expanded",
37
+ "icon",
38
+ "key",
39
+ "lazy",
40
+ "refKey",
41
+ "selected",
42
+ "title",
43
+ "tooltip",
44
+ "type",
45
+ ]);
46
+
47
+ const NODE_ATTRS = new Set<string>([
48
+ "checkbox",
49
+ "expanded",
50
+ "extraClasses", // TODO: rename to classes
51
+ "folder",
52
+ "icon",
53
+ "iconTooltip",
54
+ "key",
55
+ "lazy",
56
+ "partsel",
57
+ "radiogroup",
58
+ "refKey",
59
+ "selected",
60
+ "statusNodeType",
61
+ "title",
62
+ "tooltip",
63
+ "type",
64
+ "unselectable",
65
+ "unselectableIgnore",
66
+ "unselectableStatus",
67
+ ]);
68
+
69
+ export class WunderbaumNode {
70
+ static sequence = 0;
71
+
72
+ /** Reference to owning tree. */
73
+ public tree: Wunderbaum;
74
+ /** Parent node (null for the invisible root node `tree.root`). */
75
+ public parent: WunderbaumNode;
76
+ public title: string;
77
+ public readonly key: string;
78
+ public readonly refKey: string | undefined = undefined;
79
+ public children: WunderbaumNode[] | null = null;
80
+ public checkbox?: boolean;
81
+ public colspan?: boolean;
82
+ public icon?: boolean | string;
83
+ public lazy: boolean = false;
84
+ public expanded: boolean = false;
85
+ public selected: boolean = false;
86
+ public type?: string;
87
+ public tooltip?: string;
88
+ /** Additional classes added to `div.wb-row`. */
89
+ public extraClasses = new Set<string>();
90
+ /** Custom data that was passed to the constructor */
91
+ public data: any = {};
92
+ // --- Node Status ---
93
+ public statusNodeType?: string;
94
+ _isLoading = false;
95
+ _requestId = 0;
96
+ _errorInfo: any | null = null;
97
+ _partsel = false;
98
+ _partload = false;
99
+ // --- FILTER ---
100
+ public match?: boolean; // Added and removed by filter code
101
+ public subMatchCount?: number = 0;
102
+ public subMatchBadge?: HTMLElement;
103
+ public titleWithHighlight?: string;
104
+ public _filterAutoExpanded?: boolean;
105
+
106
+ _rowIdx: number | undefined = 0;
107
+ _rowElem: HTMLDivElement | undefined = undefined;
108
+
109
+ constructor(tree: Wunderbaum, parent: WunderbaumNode, data: any) {
110
+ util.assert(!parent || parent.tree === tree);
111
+ util.assert(!data.children);
112
+ this.tree = tree;
113
+ this.parent = parent;
114
+ this.key = "" + (data.key ?? ++WunderbaumNode.sequence);
115
+ this.title = "" + (data.title ?? "<" + this.key + ">");
116
+
117
+ data.refKey != null ? (this.refKey = "" + data.refKey) : 0;
118
+ data.statusNodeType != null
119
+ ? (this.statusNodeType = "" + data.statusNodeType)
120
+ : 0;
121
+ data.type != null ? (this.type = "" + data.type) : 0;
122
+ data.checkbox != null ? (this.checkbox = !!data.checkbox) : 0;
123
+ data.colspan != null ? (this.colspan = !!data.colspan) : 0;
124
+ this.expanded = data.expanded === true;
125
+ data.icon != null ? (this.icon = data.icon) : 0;
126
+ this.lazy = data.lazy === true;
127
+ this.selected = data.selected === true;
128
+ if (data.classes) {
129
+ for (const c of data.classes.split(" ")) {
130
+ this.extraClasses.add(c.trim());
131
+ }
132
+ }
133
+ // Store custom fields as `node.data`
134
+ for (const [key, value] of Object.entries(data)) {
135
+ if (!NODE_PROPS.has(key)) {
136
+ this.data[key] = value;
137
+ }
138
+ }
139
+
140
+ if (parent && !this.statusNodeType) {
141
+ // Don't register root node or status nodes
142
+ tree._registerNode(this);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Return readable string representation for this instance.
148
+ * @internal
149
+ */
150
+ toString() {
151
+ return "WunderbaumNode@" + this.key + "<'" + this.title + "'>";
152
+ }
153
+
154
+ // /** Return an option value. */
155
+ // protected _getOpt(
156
+ // name: string,
157
+ // nodeObject: any = null,
158
+ // treeOptions: any = null,
159
+ // defaultValue: any = null
160
+ // ): any {
161
+ // return evalOption(
162
+ // name,
163
+ // this,
164
+ // nodeObject || this,
165
+ // treeOptions || this.tree.options,
166
+ // defaultValue
167
+ // );
168
+ // }
169
+
170
+ /** Call event handler if defined in tree.options.
171
+ * Example:
172
+ * ```js
173
+ * node._callEvent("edit.beforeEdit", {foo: 42})
174
+ * ```
175
+ */
176
+ _callEvent(name: string, extra?: any): any {
177
+ return this.tree._callEvent(
178
+ name,
179
+ util.extend(
180
+ {
181
+ node: this,
182
+ typeInfo: this.type ? this.tree.types[this.type] : {},
183
+ },
184
+ extra
185
+ )
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Append (or insert) a list of child nodes.
191
+ *
192
+ * Tip: pass `{ before: 0 }` to prepend children
193
+ * @param {NodeData[]} nodeData array of child node definitions (also single child accepted)
194
+ * @param child node (or key or index of such).
195
+ * If omitted, the new children are appended.
196
+ * @returns first child added
197
+ */
198
+ addChildren(nodeData: any, options?: any): WunderbaumNode {
199
+ let insertBefore: WunderbaumNode | string | number = options
200
+ ? options.before
201
+ : null,
202
+ // redraw = options ? options.redraw !== false : true,
203
+ nodeList = [];
204
+
205
+ try {
206
+ this.tree.enableUpdate(false);
207
+
208
+ if (util.isPlainObject(nodeData)) {
209
+ nodeData = [nodeData];
210
+ }
211
+ for (let child of nodeData) {
212
+ let subChildren = child.children;
213
+ delete child.children;
214
+
215
+ let n = new WunderbaumNode(this.tree, this, child);
216
+ nodeList.push(n);
217
+ if (subChildren) {
218
+ n.addChildren(subChildren, { redraw: false });
219
+ }
220
+ }
221
+
222
+ if (!this.children) {
223
+ this.children = nodeList;
224
+ } else if (insertBefore == null || this.children.length === 0) {
225
+ this.children = this.children.concat(nodeList);
226
+ } else {
227
+ // Returns null if insertBefore is not a direct child:
228
+ insertBefore = this.findDirectChild(insertBefore)!;
229
+ let pos = this.children.indexOf(insertBefore);
230
+ util.assert(pos >= 0, "insertBefore must be an existing child");
231
+ // insert nodeList after children[pos]
232
+ this.children.splice(pos, 0, ...nodeList);
233
+ }
234
+ // TODO:
235
+ // if (this.tree.options.selectMode === 3) {
236
+ // this.fixSelection3FromEndNodes();
237
+ // }
238
+ // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
239
+ this.tree.setModified(ChangeType.structure, this);
240
+ return nodeList[0];
241
+ } finally {
242
+ this.tree.enableUpdate(true);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Append or prepend a node, or append a child node.
248
+ *
249
+ * This a convenience function that calls addChildren()
250
+ *
251
+ * @param {NodeData} node node definition
252
+ * @param [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child')
253
+ * @returns new node
254
+ */
255
+ addNode(nodeData: WbNodeData, mode = "child"): WunderbaumNode {
256
+ if (mode === "over") {
257
+ mode = "child"; // compatible with drop region
258
+ }
259
+ switch (mode) {
260
+ case "after":
261
+ return this.parent.addChildren(nodeData, {
262
+ before: this.getNextSibling(),
263
+ });
264
+ case "before":
265
+ return this.parent.addChildren(nodeData, { before: this });
266
+ case "firstChild":
267
+ // Insert before the first child if any
268
+ // let insertBefore = this.children ? this.children[0] : undefined;
269
+ return this.addChildren(nodeData, { before: 0 });
270
+ case "child":
271
+ return this.addChildren(nodeData);
272
+ }
273
+ util.assert(false, "Invalid mode: " + mode);
274
+ return (<unknown>undefined) as WunderbaumNode;
275
+ }
276
+
277
+ /**
278
+ * Apply a modification (or navigation) operation.
279
+ * @see Wunderbaum#applyCommand
280
+ */
281
+ applyCommand(cmd: ApplyCommandType, opts: any): any {
282
+ return this.tree.applyCommand(cmd, this, opts);
283
+ }
284
+
285
+ addClass(className: string | string[] | Set<string>) {
286
+ const cnSet = util.toSet(className);
287
+ cnSet.forEach((cn) => {
288
+ this.extraClasses.add(cn);
289
+ this._rowElem?.classList.add(cn);
290
+ });
291
+ }
292
+
293
+ removeClass(className: string | string[] | Set<string>) {
294
+ const cnSet = util.toSet(className);
295
+ cnSet.forEach((cn) => {
296
+ this.extraClasses.delete(cn);
297
+ this._rowElem?.classList.remove(cn);
298
+ });
299
+ }
300
+
301
+ toggleClass(className: string | string[] | Set<string>, flag: boolean) {
302
+ const cnSet = util.toSet(className);
303
+ cnSet.forEach((cn) => {
304
+ flag ? this.extraClasses.add(cn) : this.extraClasses.delete(cn);
305
+ this._rowElem?.classList.toggle(cn, flag);
306
+ });
307
+ }
308
+
309
+ /** */
310
+ async expandAll(flag: boolean = true) {
311
+ this.visit((node) => {
312
+ node.setExpanded(flag);
313
+ });
314
+ }
315
+
316
+ /**Find all nodes that match condition (excluding self).
317
+ *
318
+ * @param {string | function(node)} match title string to search for, or a
319
+ * callback function that returns `true` if a node is matched.
320
+ */
321
+ findAll(match: string | MatcherType): WunderbaumNode[] {
322
+ const matcher = util.isFunction(match)
323
+ ? <MatcherType>match
324
+ : makeNodeTitleMatcher(<string>match);
325
+ const res: WunderbaumNode[] = [];
326
+ this.visit((n) => {
327
+ if (matcher(n)) {
328
+ res.push(n);
329
+ }
330
+ });
331
+ return res;
332
+ }
333
+
334
+ /** Return the direct child with a given key, index or null. */
335
+ findDirectChild(
336
+ ptr: number | string | WunderbaumNode
337
+ ): WunderbaumNode | null {
338
+ let cl = this.children;
339
+
340
+ if (!cl) return null;
341
+ if (typeof ptr === "string") {
342
+ for (let i = 0, l = cl.length; i < l; i++) {
343
+ if (cl[i].key === ptr) {
344
+ return cl[i];
345
+ }
346
+ }
347
+ } else if (typeof ptr === "number") {
348
+ return cl[ptr];
349
+ } else if (ptr.parent === this) {
350
+ // Return null if `ptr` is not a direct child
351
+ return ptr;
352
+ }
353
+ return null;
354
+ }
355
+
356
+ /**Find first node that matches condition (excluding self).
357
+ *
358
+ * @param match title string to search for, or a
359
+ * callback function that returns `true` if a node is matched.
360
+ */
361
+ findFirst(match: string | MatcherType): WunderbaumNode | null {
362
+ const matcher = util.isFunction(match)
363
+ ? <MatcherType>match
364
+ : makeNodeTitleMatcher(<string>match);
365
+ let res = null;
366
+ this.visit((n) => {
367
+ if (matcher(n)) {
368
+ res = n;
369
+ return false;
370
+ }
371
+ });
372
+ return res;
373
+ }
374
+
375
+ /** Find a node relative to self.
376
+ *
377
+ * @param where The keyCode that would normally trigger this move,
378
+ * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up').
379
+ */
380
+ findRelatedNode(where: string, includeHidden = false) {
381
+ return this.tree.findRelatedNode(this, where, includeHidden);
382
+ }
383
+
384
+ /** Return the `<span class='wb-col'>` element with a given index or id.
385
+ * @returns {WunderbaumNode | null}
386
+ */
387
+ getColElem(colIdx: number | string) {
388
+ if (typeof colIdx === "string") {
389
+ colIdx = this.tree.columns.findIndex((value) => value.id === colIdx);
390
+ }
391
+ const colElems = this._rowElem?.querySelectorAll("span.wb-col");
392
+ return colElems ? (colElems[colIdx] as HTMLSpanElement) : null;
393
+ }
394
+
395
+ /** Return the first child node or null.
396
+ * @returns {WunderbaumNode | null}
397
+ */
398
+ getFirstChild() {
399
+ return this.children ? this.children[0] : null;
400
+ }
401
+
402
+ /** Return the last child node or null.
403
+ * @returns {WunderbaumNode | null}
404
+ */
405
+ getLastChild() {
406
+ return this.children ? this.children[this.children.length - 1] : null;
407
+ }
408
+
409
+ /** Return node depth (starting with 1 for top level nodes). */
410
+ getLevel(): number {
411
+ let i = 0,
412
+ p = this.parent;
413
+
414
+ while (p) {
415
+ i++;
416
+ p = p.parent;
417
+ }
418
+ return i;
419
+ }
420
+
421
+ /** Return the successive node (under the same parent) or null. */
422
+ getNextSibling(): WunderbaumNode | null {
423
+ let ac = this.parent.children!;
424
+ let idx = ac.indexOf(this);
425
+ return ac[idx + 1] || null;
426
+ }
427
+
428
+ /** Return the parent node (null for the system root node). */
429
+ getParent(): WunderbaumNode | null {
430
+ // TODO: return null for top-level nodes?
431
+ return this.parent;
432
+ }
433
+
434
+ /** Return an array of all parent nodes (top-down).
435
+ * @param includeRoot Include the invisible system root node.
436
+ * @param includeSelf Include the node itself.
437
+ */
438
+ getParentList(includeRoot = false, includeSelf = false) {
439
+ let l = [],
440
+ dtn = includeSelf ? this : this.parent;
441
+ while (dtn) {
442
+ if (includeRoot || dtn.parent) {
443
+ l.unshift(dtn);
444
+ }
445
+ dtn = dtn.parent;
446
+ }
447
+ return l;
448
+ }
449
+ /** Return a string representing the hierachical node path, e.g. "a/b/c".
450
+ * @param includeSelf
451
+ * @param node property name or callback
452
+ * @param separator
453
+ */
454
+ getPath(
455
+ includeSelf = true,
456
+ part: keyof WunderbaumNode | NodeAnyCallback = "title",
457
+ separator = "/"
458
+ ) {
459
+ // includeSelf = includeSelf !== false;
460
+ // part = part || "title";
461
+ // separator = separator || "/";
462
+
463
+ let val,
464
+ path: string[] = [],
465
+ isFunc = typeof part === "function";
466
+
467
+ this.visitParents((n) => {
468
+ if (n.parent) {
469
+ val = isFunc
470
+ ? (<NodeAnyCallback>part)(n)
471
+ : n[<keyof WunderbaumNode>part];
472
+ path.unshift(val);
473
+ }
474
+ return undefined; // TODO remove this line
475
+ }, includeSelf);
476
+ return path.join(separator);
477
+ }
478
+
479
+ /** Return the preceeding node (under the same parent) or null. */
480
+ getPrevSibling(): WunderbaumNode | null {
481
+ let ac = this.parent.children!;
482
+ let idx = ac.indexOf(this);
483
+ return ac[idx - 1] || null;
484
+ }
485
+
486
+ /** Return true if node has children.
487
+ * Return undefined if not sure, i.e. the node is lazy and not yet loaded.
488
+ */
489
+ hasChildren() {
490
+ if (this.lazy) {
491
+ if (this.children == null) {
492
+ return undefined; // null or undefined: Not yet loaded
493
+ } else if (this.children.length === 0) {
494
+ return false; // Loaded, but response was empty
495
+ } else if (
496
+ this.children.length === 1 &&
497
+ this.children[0].isStatusNode()
498
+ ) {
499
+ return undefined; // Currently loading or load error
500
+ }
501
+ return true; // One or more child nodes
502
+ }
503
+ return !!(this.children && this.children.length);
504
+ }
505
+
506
+ /** Return true if this node is the currently active tree node. */
507
+ isActive() {
508
+ return this.tree.activeNode === this;
509
+ }
510
+
511
+ /** Return true if this node is a *direct* child of `other`.
512
+ * (See also [[isDescendantOf]].)
513
+ */
514
+ isChildOf(other: WunderbaumNode) {
515
+ return this.parent && this.parent === other;
516
+ }
517
+
518
+ /** Return true if this node is a direct or indirect sub node of `other`.
519
+ * (See also [[isChildOf]].)
520
+ */
521
+ isDescendantOf(other: WunderbaumNode) {
522
+ if (!other || other.tree !== this.tree) {
523
+ return false;
524
+ }
525
+ var p = this.parent;
526
+ while (p) {
527
+ if (p === other) {
528
+ return true;
529
+ }
530
+ if (p === p.parent) {
531
+ util.error("Recursive parent link: " + p);
532
+ }
533
+ p = p.parent;
534
+ }
535
+ return false;
536
+ }
537
+
538
+ /** Return true if this node has children, i.e. the node is generally expandable.
539
+ * If `andCollapsed` is set, we also check if this node is collapsed, i.e.
540
+ * an expand operation is currently possible.
541
+ */
542
+ isExpandable(andCollapsed = false): boolean {
543
+ return !!this.children && (!this.expanded || !andCollapsed);
544
+ }
545
+
546
+ /** Return true if this node is currently in edit-title mode. */
547
+ isEditing(): boolean {
548
+ return this.tree._callMethod("edit.isEditingTitle", this);
549
+ }
550
+
551
+ /** Return true if this node is currently expanded. */
552
+ isExpanded(): boolean {
553
+ return !!this.expanded;
554
+ }
555
+
556
+ /** Return true if this node is the first node of its parent's children. */
557
+ isFirstSibling(): boolean {
558
+ var p = this.parent;
559
+ return !p || p.children![0] === this;
560
+ }
561
+
562
+ /** Return true if this node is the last node of its parent's children. */
563
+ isLastSibling(): boolean {
564
+ var p = this.parent;
565
+ return !p || p.children![p.children!.length - 1] === this;
566
+ }
567
+
568
+ /** Return true if this node is lazy (even if data was already loaded) */
569
+ isLazy(): boolean {
570
+ return !!this.lazy;
571
+ }
572
+
573
+ /** Return true if node is lazy and loaded. For non-lazy nodes always return true. */
574
+ isLoaded(): boolean {
575
+ return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node
576
+ }
577
+
578
+ /** Return true if node is currently loading, i.e. a GET request is pending. */
579
+ isLoading(): boolean {
580
+ return this._isLoading;
581
+ }
582
+
583
+ /** Return true if this node is a temporarily generated status node of type 'paging'. */
584
+ isPagingNode(): boolean {
585
+ return this.statusNodeType === "paging";
586
+ }
587
+
588
+ /** (experimental) Return true if this node is partially loaded. */
589
+ isPartload(): boolean {
590
+ return !!this._partload;
591
+ }
592
+
593
+ /** Return true if this node is partially selected (tri-state). */
594
+ isPartsel(): boolean {
595
+ return !this.selected && !!this._partsel;
596
+ }
597
+
598
+ /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
599
+ isRendered(): boolean {
600
+ return !!this._rowElem;
601
+ }
602
+
603
+ /** Return true if this node is the (invisible) system root node.
604
+ * (See also [[isTopLevel()]].)
605
+ */
606
+ isRootNode(): boolean {
607
+ return this.tree.root === this;
608
+ }
609
+
610
+ /** Return true if this node is selected, i.e. the checkbox is set. */
611
+ isSelected(): boolean {
612
+ return !!this.selected;
613
+ }
614
+
615
+ /** Return true if this node is a temporarily generated system node like
616
+ * 'loading', 'paging', or 'error' (node.statusNodeType contains the type).
617
+ */
618
+ isStatusNode(): boolean {
619
+ return !!this.statusNodeType;
620
+ }
621
+
622
+ /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. */
623
+ isTopLevel(): boolean {
624
+ return this.tree.root === this.parent;
625
+ }
626
+
627
+ /** Return true if node is marked lazy but not yet loaded.
628
+ * For non-lazy nodes always return false.
629
+ */
630
+ isUnloaded(): boolean {
631
+ // Also checks if the only child is a status node:
632
+ return this.hasChildren() === undefined;
633
+ }
634
+
635
+ /** Return true if all parent nodes are expanded. Note: this does not check
636
+ * whether the node is scrolled into the visible part of the screen or viewport.
637
+ */
638
+ isVisible(): boolean {
639
+ let i,
640
+ l,
641
+ n,
642
+ hasFilter = this.tree.filterMode === "hide",
643
+ parents = this.getParentList(false, false);
644
+
645
+ // TODO: check $(n.span).is(":visible")
646
+ // i.e. return false for nodes (but not parents) that are hidden
647
+ // by a filter
648
+ if (hasFilter && !this.match && !this.subMatchCount) {
649
+ // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" );
650
+ return false;
651
+ }
652
+
653
+ for (i = 0, l = parents.length; i < l; i++) {
654
+ n = parents[i];
655
+
656
+ if (!n.expanded) {
657
+ // this.debug("isVisible: HIDDEN (parent collapsed)");
658
+ return false;
659
+ }
660
+ // if (hasFilter && !n.match && !n.subMatchCount) {
661
+ // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")");
662
+ // return false;
663
+ // }
664
+ }
665
+ // this.debug("isVisible: VISIBLE");
666
+ return true;
667
+ }
668
+
669
+ protected _loadSourceObject(source: any) {
670
+ const tree = this.tree;
671
+
672
+ // Let caller modify the parsed JSON response:
673
+ this._callEvent("receive", { response: source });
674
+
675
+ if (util.isArray(source)) {
676
+ source = { children: source };
677
+ }
678
+ util.assert(util.isPlainObject(source));
679
+ util.assert(
680
+ source.children,
681
+ "If `source` is an object, it must have a `children` property"
682
+ );
683
+ if (source.types) {
684
+ // TODO: convert types.classes to Set()
685
+ util.extend(tree.types, source.types);
686
+ }
687
+ this.addChildren(source.children);
688
+
689
+ this._callEvent("load");
690
+ }
691
+
692
+ /** Download data from the cloud, then call `.update()`. */
693
+ async load(source: any) {
694
+ const tree = this.tree;
695
+ // const opts = tree.options;
696
+ const requestId = Date.now();
697
+ const prevParent = this.parent;
698
+ const url = typeof source === "string" ? source : source.url;
699
+
700
+ // Check for overlapping requests
701
+ if (this._requestId) {
702
+ this.logWarn(
703
+ `Recursive load request #${requestId} while #${this._requestId} is pending.`
704
+ );
705
+ // node.debug("Send load request #" + requestId);
706
+ }
707
+ this._requestId = requestId;
708
+
709
+ const timerLabel = tree.logTime(this + ".load()");
710
+
711
+ try {
712
+ if (!url) {
713
+ this._loadSourceObject(source);
714
+ } else {
715
+ this.setStatus(NodeStatusType.loading);
716
+ const response = await fetch(url, { method: "GET" });
717
+ if (!response.ok) {
718
+ util.error(`GET ${url} returned ${response.status}, ${response}`);
719
+ }
720
+ const data = await response.json();
721
+
722
+ if (this._requestId && this._requestId > requestId) {
723
+ this.logWarn(
724
+ `Ignored load response #${requestId} because #${this._requestId} is pending.`
725
+ );
726
+ return;
727
+ } else {
728
+ this.logDebug(`Received response for load request #${requestId}`);
729
+ }
730
+ if (this.parent === null && prevParent !== null) {
731
+ this.logWarn(
732
+ "Lazy parent node was removed while loading: discarding response."
733
+ );
734
+ return;
735
+ }
736
+ this.setStatus(NodeStatusType.ok);
737
+ if (data.columns) {
738
+ tree.logInfo("Re-define columns", data.columns);
739
+ util.assert(!this.parent);
740
+ tree.columns = data.columns;
741
+ delete data.columns;
742
+ tree.renderHeader();
743
+ }
744
+ this._loadSourceObject(data);
745
+ }
746
+ } catch (error) {
747
+ this.logError("Error during load()", source, error);
748
+ this._callEvent("error", { error: error });
749
+ this.setStatus(NodeStatusType.error, "" + error);
750
+ throw error;
751
+ } finally {
752
+ this._requestId = 0;
753
+ tree.logTimeEnd(timerLabel);
754
+ }
755
+ }
756
+
757
+ /**Load content of a lazy node. */
758
+ async loadLazy(forceReload = false) {
759
+ const wasExpanded = this.expanded;
760
+
761
+ util.assert(this.lazy, "load() requires a lazy node");
762
+ // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" );
763
+ if (!forceReload && !this.isUnloaded()) {
764
+ return;
765
+ }
766
+ if (this.isLoaded()) {
767
+ this.resetLazy(); // Also collapses if currently expanded
768
+ }
769
+ // `lazyLoad` may be long-running, so mark node as loading now. `this.load()`
770
+ // will reset the status later.
771
+ this.setStatus(NodeStatusType.loading);
772
+ try {
773
+ const source = await this._callEvent("lazyLoad");
774
+ if (source === false) {
775
+ this.setStatus(NodeStatusType.ok);
776
+ return;
777
+ }
778
+ util.assert(
779
+ util.isArray(source) || (source && source.url),
780
+ "The lazyLoad event must return a node list, `{url: ...}` or false."
781
+ );
782
+
783
+ await this.load(source); // also calls setStatus('ok')
784
+
785
+ if (wasExpanded) {
786
+ this.expanded = true;
787
+ this.tree.updateViewport();
788
+ } else {
789
+ this.render(); // Fix expander icon to 'loaded'
790
+ }
791
+ } catch (e) {
792
+ this.setStatus(NodeStatusType.error, "" + e);
793
+ // } finally {
794
+ }
795
+ return;
796
+ }
797
+
798
+ /** Alias for `logDebug` */
799
+ log(...args: any[]) {
800
+ this.logDebug.apply(this, args);
801
+ }
802
+
803
+ /* Log to console if opts.debugLevel >= 4 */
804
+ logDebug(...args: any[]) {
805
+ if (this.tree.options.debugLevel >= 4) {
806
+ Array.prototype.unshift.call(args, this.toString());
807
+ console.log.apply(console, args);
808
+ }
809
+ }
810
+
811
+ /* Log error to console. */
812
+ logError(...args: any[]) {
813
+ if (this.tree.options.debugLevel >= 1) {
814
+ Array.prototype.unshift.call(args, this.toString());
815
+ console.error.apply(console, args);
816
+ }
817
+ }
818
+
819
+ /* Log to console if opts.debugLevel >= 3 */
820
+ logInfo(...args: any[]) {
821
+ if (this.tree.options.debugLevel >= 3) {
822
+ Array.prototype.unshift.call(args, this.toString());
823
+ console.info.apply(console, args);
824
+ }
825
+ }
826
+
827
+ /* Log warning to console if opts.debugLevel >= 2 */
828
+ logWarn(...args: any[]) {
829
+ if (this.tree.options.debugLevel >= 2) {
830
+ Array.prototype.unshift.call(args, this.toString());
831
+ console.warn.apply(console, args);
832
+ }
833
+ }
834
+
835
+ /** Expand all parents and optionally scroll into visible area as neccessary.
836
+ * Promise is resolved, when lazy loading and animations are done.
837
+ * @param {object} [opts] passed to `setExpanded()`.
838
+ * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
839
+ */
840
+ async makeVisible(opts: any) {
841
+ let i,
842
+ dfd = new Deferred(),
843
+ deferreds = [],
844
+ parents = this.getParentList(false, false),
845
+ len = parents.length,
846
+ effects = !(opts && opts.noAnimation === true),
847
+ scroll = !(opts && opts.scrollIntoView === false);
848
+
849
+ // Expand bottom-up, so only the top node is animated
850
+ for (i = len - 1; i >= 0; i--) {
851
+ // self.debug("pushexpand" + parents[i]);
852
+ deferreds.push(parents[i].setExpanded(true, opts));
853
+ }
854
+ Promise.all(deferreds).then(() => {
855
+ // All expands have finished
856
+ // self.debug("expand DONE", scroll);
857
+ if (scroll) {
858
+ this.scrollIntoView(effects).then(() => {
859
+ // self.debug("scroll DONE");
860
+ dfd.resolve();
861
+ });
862
+ } else {
863
+ dfd.resolve();
864
+ }
865
+ });
866
+ return dfd.promise();
867
+ }
868
+
869
+ /** Move this node to targetNode. */
870
+ moveTo(
871
+ targetNode: WunderbaumNode,
872
+ mode: AddNodeType = "appendChild",
873
+ map?: NodeAnyCallback
874
+ ) {
875
+ if (mode === "prependChild") {
876
+ if (targetNode.children && targetNode.children.length) {
877
+ mode = "before";
878
+ targetNode = targetNode.children[0];
879
+ } else {
880
+ mode = "appendChild";
881
+ }
882
+ }
883
+ let pos,
884
+ tree = this.tree,
885
+ prevParent = this.parent,
886
+ targetParent = mode === "appendChild" ? targetNode : targetNode.parent;
887
+
888
+ if (this === targetNode) {
889
+ return;
890
+ } else if (!this.parent) {
891
+ util.error("Cannot move system root");
892
+ } else if (targetParent.isDescendantOf(this)) {
893
+ util.error("Cannot move a node to its own descendant");
894
+ }
895
+ if (targetParent !== prevParent) {
896
+ prevParent.triggerModifyChild("remove", this);
897
+ }
898
+ // Unlink this node from current parent
899
+ if (this.parent.children!.length === 1) {
900
+ if (this.parent === targetParent) {
901
+ return; // #258
902
+ }
903
+ this.parent.children = this.parent.lazy ? [] : null;
904
+ this.parent.expanded = false;
905
+ } else {
906
+ pos = this.parent.children!.indexOf(this);
907
+ util.assert(pos >= 0, "invalid source parent");
908
+ this.parent.children!.splice(pos, 1);
909
+ }
910
+
911
+ // Insert this node to target parent's child list
912
+ this.parent = targetParent;
913
+ if (targetParent.hasChildren()) {
914
+ switch (mode) {
915
+ case "appendChild":
916
+ // Append to existing target children
917
+ targetParent.children!.push(this);
918
+ break;
919
+ case "before":
920
+ // Insert this node before target node
921
+ pos = targetParent.children!.indexOf(targetNode);
922
+ util.assert(pos >= 0, "invalid target parent");
923
+ targetParent.children!.splice(pos, 0, this);
924
+ break;
925
+ case "after":
926
+ // Insert this node after target node
927
+ pos = targetParent.children!.indexOf(targetNode);
928
+ util.assert(pos >= 0, "invalid target parent");
929
+ targetParent.children!.splice(pos + 1, 0, this);
930
+ break;
931
+ default:
932
+ util.error("Invalid mode " + mode);
933
+ }
934
+ } else {
935
+ targetParent.children = [this];
936
+ }
937
+
938
+ // Let caller modify the nodes
939
+ if (map) {
940
+ targetNode.visit(map, true);
941
+ }
942
+ if (targetParent === prevParent) {
943
+ targetParent.triggerModifyChild("move", this);
944
+ } else {
945
+ // prevParent.triggerModifyChild("remove", this);
946
+ targetParent.triggerModifyChild("add", this);
947
+ }
948
+ // Handle cross-tree moves
949
+ if (tree !== targetNode.tree) {
950
+ // Fix node.tree for all source nodes
951
+ // util.assert(false, "Cross-tree move is not yet implemented.");
952
+ this.logWarn("Cross-tree moveTo is experimental!");
953
+ this.visit(function (n) {
954
+ // TODO: fix selection state and activation, ...
955
+ n.tree = targetNode.tree;
956
+ }, true);
957
+ }
958
+
959
+ tree.updateViewport();
960
+ // TODO: fix selection state
961
+ // TODO: fix active state
962
+ }
963
+
964
+ /** Set focus relative to this node and optionally activate.
965
+ *
966
+ * 'left' collapses the node if it is expanded, or move to the parent
967
+ * otherwise.
968
+ * 'right' expands the node if it is collapsed, or move to the first
969
+ * child otherwise.
970
+ *
971
+ * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
972
+ * (Alternatively the `event.key` that would normally trigger this move,
973
+ * e.g. `ArrowLeft` = 'left'.
974
+ * @param options
975
+ */
976
+ async navigate(where: string, options?: any) {
977
+ // Allow to pass 'ArrowLeft' instead of 'left'
978
+ where = KEY_TO_ACTION_DICT[where] || where;
979
+
980
+ // Otherwise activate or focus the related node
981
+ let node = this.findRelatedNode(where);
982
+ if (node) {
983
+ // setFocus/setActive will scroll later (if autoScroll is specified)
984
+ try {
985
+ node.makeVisible({ scrollIntoView: false });
986
+ } catch (e) {} // #272
987
+ node.setFocus();
988
+ if (options?.activate === false) {
989
+ return Promise.resolve(this);
990
+ }
991
+ return node.setActive(true, { event: options?.event });
992
+ }
993
+ this.logWarn("Could not find related node '" + where + "'.");
994
+ return Promise.resolve(this);
995
+ }
996
+
997
+ /** Delete this node and all descendants. */
998
+ remove() {
999
+ const tree = this.tree;
1000
+ const pos = this.parent.children!.indexOf(this);
1001
+ this.parent.children!.splice(pos, 1);
1002
+ this.visit((n) => {
1003
+ n.removeMarkup();
1004
+ tree._unregisterNode(n);
1005
+ }, true);
1006
+ }
1007
+
1008
+ /** Remove all descendants of this node. */
1009
+ removeChildren() {
1010
+ const tree = this.tree;
1011
+
1012
+ if (!this.children) {
1013
+ return;
1014
+ }
1015
+ if (tree.activeNode && tree.activeNode.isDescendantOf(this)) {
1016
+ tree.activeNode.setActive(false); // TODO: don't fire events
1017
+ }
1018
+ if (tree.focusNode && tree.focusNode.isDescendantOf(this)) {
1019
+ tree.focusNode = null;
1020
+ }
1021
+ // TODO: persist must take care to clear select and expand cookies
1022
+ // Unlink children to support GC
1023
+ // TODO: also delete this.children (not possible using visit())
1024
+ this.triggerModifyChild("remove", null);
1025
+ this.visit((n) => {
1026
+ tree._unregisterNode(n);
1027
+ });
1028
+ if (this.lazy) {
1029
+ // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes
1030
+ this.children = [];
1031
+ } else {
1032
+ this.children = null;
1033
+ }
1034
+ // util.assert(this.parent); // don't call this for root node
1035
+ if (!this.isRootNode()) {
1036
+ this.expanded = false;
1037
+ }
1038
+ this.tree.updateViewport();
1039
+ }
1040
+
1041
+ /** Remove all HTML markup from the DOM. */
1042
+ removeMarkup() {
1043
+ if (this._rowElem) {
1044
+ delete (<any>this._rowElem)._wb_node;
1045
+ this._rowElem.remove();
1046
+ this._rowElem = undefined;
1047
+ }
1048
+ }
1049
+
1050
+ protected _getRenderInfo() {
1051
+ let colInfosById: { [key: string]: any } = {};
1052
+ let idx = 0;
1053
+ let colElems = this._rowElem
1054
+ ? ((<unknown>(
1055
+ this._rowElem.querySelectorAll("span.wb-col")
1056
+ )) as HTMLElement[])
1057
+ : null;
1058
+
1059
+ for (let col of this.tree.columns) {
1060
+ colInfosById[col.id] = {
1061
+ id: col.id,
1062
+ idx: idx,
1063
+ elem: colElems ? colElems[idx] : null,
1064
+ info: col,
1065
+ };
1066
+ idx++;
1067
+ }
1068
+ return colInfosById;
1069
+ }
1070
+
1071
+ protected _createIcon(
1072
+ parentElem: HTMLElement,
1073
+ replaceChild?: HTMLElement
1074
+ ): HTMLElement | null {
1075
+ let iconSpan;
1076
+ let icon = this.getOption("icon");
1077
+ if (this._errorInfo) {
1078
+ icon = iconMap.error;
1079
+ } else if (this._isLoading) {
1080
+ icon = iconMap.loading;
1081
+ }
1082
+ if (icon === false) {
1083
+ return null;
1084
+ }
1085
+ if (typeof icon === "string") {
1086
+ // Callback returned an icon definition
1087
+ // icon = icon.trim()
1088
+ } else if (this.statusNodeType) {
1089
+ icon = (<any>iconMap)[this.statusNodeType];
1090
+ } else if (this.expanded) {
1091
+ icon = iconMap.folderOpen;
1092
+ } else if (this.children) {
1093
+ icon = iconMap.folder;
1094
+ } else {
1095
+ icon = iconMap.doc;
1096
+ }
1097
+
1098
+ // this.log("_createIcon: " + icon);
1099
+ if (icon.indexOf("<") >= 0) {
1100
+ // HTML
1101
+ iconSpan = util.elemFromHtml(icon);
1102
+ } else if (TEST_IMG.test(icon)) {
1103
+ // Image URL
1104
+ iconSpan = util.elemFromHtml(`<img src="${icon}" class="wb-icon">`);
1105
+ } else {
1106
+ // Class name
1107
+ iconSpan = document.createElement("i");
1108
+ iconSpan.className = "wb-icon " + icon;
1109
+ }
1110
+ if (replaceChild) {
1111
+ parentElem.replaceChild(iconSpan, replaceChild);
1112
+ } else {
1113
+ parentElem.appendChild(iconSpan);
1114
+ }
1115
+ // this.log("_createIcon: ", iconSpan);
1116
+ return iconSpan;
1117
+ }
1118
+
1119
+ /** Create HTML markup for this node, i.e. the whole row. */
1120
+ render(opts?: any) {
1121
+ const tree = this.tree;
1122
+ const treeOptions = tree.options;
1123
+ const checkbox = this.getOption("checkbox") !== false;
1124
+ const columns = tree.columns;
1125
+ const typeInfo = this.type ? tree.types[this.type] : null;
1126
+ const level = this.getLevel();
1127
+ let elem: HTMLElement;
1128
+ let nodeElem: HTMLElement;
1129
+ let rowDiv = this._rowElem;
1130
+ let titleSpan: HTMLElement;
1131
+ let checkboxSpan: HTMLElement | null = null;
1132
+ let iconSpan: HTMLElement | null;
1133
+ let expanderSpan: HTMLElement | null = null;
1134
+ const activeColIdx =
1135
+ tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
1136
+ // let colElems: HTMLElement[];
1137
+ const isNew = !rowDiv;
1138
+
1139
+ util.assert(!this.isRootNode());
1140
+ //
1141
+ let rowClasses = ["wb-row"];
1142
+ this.expanded ? rowClasses.push("wb-expanded") : 0;
1143
+ this.lazy ? rowClasses.push("wb-lazy") : 0;
1144
+ this.selected ? rowClasses.push("wb-selected") : 0;
1145
+ this === tree.activeNode ? rowClasses.push("wb-active") : 0;
1146
+ this === tree.focusNode ? rowClasses.push("wb-focus") : 0;
1147
+ this._errorInfo ? rowClasses.push("wb-error") : 0;
1148
+ this._isLoading ? rowClasses.push("wb-loading") : 0;
1149
+ this.statusNodeType
1150
+ ? rowClasses.push("wb-status-" + this.statusNodeType)
1151
+ : 0;
1152
+
1153
+ this.match ? rowClasses.push("wb-match") : 0;
1154
+ this.subMatchCount ? rowClasses.push("wb-submatch") : 0;
1155
+ treeOptions.skeleton ? rowClasses.push("wb-skeleton") : 0;
1156
+ // TODO: no need to hide!
1157
+ // !(this.match || this.subMatchCount) ? rowClasses.push("wb-hide") : 0;
1158
+
1159
+ if (rowDiv) {
1160
+ // Row markup already exists
1161
+ nodeElem = rowDiv.querySelector("span.wb-node") as HTMLElement;
1162
+ titleSpan = nodeElem.querySelector("span.wb-title") as HTMLElement;
1163
+ expanderSpan = nodeElem.querySelector("i.wb-expander") as HTMLElement;
1164
+ checkboxSpan = nodeElem.querySelector("i.wb-checkbox") as HTMLElement;
1165
+ iconSpan = nodeElem.querySelector("i.wb-icon") as HTMLElement;
1166
+ // TODO: we need this, when icons should be replacable
1167
+ // iconSpan = this._createIcon(nodeElem, iconSpan);
1168
+
1169
+ // colElems = (<unknown>(
1170
+ // rowDiv.querySelectorAll("span.wb-col")
1171
+ // )) as HTMLElement[];
1172
+ } else {
1173
+ rowDiv = document.createElement("div");
1174
+ // rowDiv.classList.add("wb-row");
1175
+ // Attach a node reference to the DOM Element:
1176
+ (<any>rowDiv)._wb_node = this;
1177
+
1178
+ nodeElem = document.createElement("span");
1179
+ nodeElem.classList.add("wb-node", "wb-col");
1180
+ rowDiv.appendChild(nodeElem);
1181
+
1182
+ let ofsTitlePx = 0;
1183
+
1184
+ if (checkbox) {
1185
+ checkboxSpan = document.createElement("i");
1186
+ nodeElem.appendChild(checkboxSpan);
1187
+ ofsTitlePx += ICON_WIDTH;
1188
+ }
1189
+
1190
+ for (let i = level - 1; i > 0; i--) {
1191
+ elem = document.createElement("i");
1192
+ elem.classList.add("wb-indent");
1193
+ nodeElem.appendChild(elem);
1194
+ ofsTitlePx += ICON_WIDTH;
1195
+ }
1196
+
1197
+ if (level > treeOptions.minExpandLevel) {
1198
+ expanderSpan = document.createElement("i");
1199
+ nodeElem.appendChild(expanderSpan);
1200
+ ofsTitlePx += ICON_WIDTH;
1201
+ }
1202
+
1203
+ iconSpan = this._createIcon(nodeElem);
1204
+ if (iconSpan) {
1205
+ ofsTitlePx += ICON_WIDTH;
1206
+ }
1207
+
1208
+ titleSpan = document.createElement("span");
1209
+ titleSpan.classList.add("wb-title");
1210
+ nodeElem.appendChild(titleSpan);
1211
+
1212
+ this._callEvent("enhanceTitle", { titleSpan: titleSpan });
1213
+
1214
+ // Store the width of leading icons with the node, so we can calculate
1215
+ // the width of the embedded title span later
1216
+ (<any>nodeElem)._ofsTitlePx = ofsTitlePx;
1217
+ if (tree.options.dnd.dragStart) {
1218
+ nodeElem.draggable = true;
1219
+ }
1220
+
1221
+ // Render columns
1222
+ // colElems = [];
1223
+ if (!this.colspan && columns.length > 1) {
1224
+ let colIdx = 0;
1225
+ for (let col of columns) {
1226
+ colIdx++;
1227
+
1228
+ let colElem;
1229
+ if (col.id === "*") {
1230
+ colElem = nodeElem;
1231
+ } else {
1232
+ colElem = document.createElement("span");
1233
+ colElem.classList.add("wb-col");
1234
+ // colElem.textContent = "" + col.id;
1235
+ rowDiv.appendChild(colElem);
1236
+ }
1237
+ if (colIdx === activeColIdx) {
1238
+ colElem.classList.add("wb-active");
1239
+ }
1240
+ // Add classes from `columns` definition to `<div.wb-col>` cells
1241
+ col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
1242
+
1243
+ colElem.style.left = col._ofsPx + "px";
1244
+ colElem.style.width = col._widthPx + "px";
1245
+ // colElems.push(colElem);
1246
+ if (isNew && col.html) {
1247
+ if (typeof col.html === "string") {
1248
+ colElem.innerHTML = col.html;
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ // --- From here common code starts (either new or existing markup):
1256
+
1257
+ rowDiv.className = rowClasses.join(" "); // Reset prev. classes
1258
+
1259
+ // Add classes from `node.extraClasses`
1260
+ rowDiv.classList.add(...this.extraClasses);
1261
+ // Add classes from `tree.types[node.type]`
1262
+ if (typeInfo && typeInfo.classes) {
1263
+ rowDiv.classList.add(...typeInfo.classes);
1264
+ }
1265
+ // rowDiv.style.top = (this._rowIdx! * 1.1) + "em";
1266
+ rowDiv.style.top = this._rowIdx! * ROW_HEIGHT + "px";
1267
+
1268
+ if (expanderSpan) {
1269
+ if (this.isExpandable(false)) {
1270
+ if (this.expanded) {
1271
+ expanderSpan.className = "wb-expander " + iconMap.expanderExpanded;
1272
+ } else {
1273
+ expanderSpan.className = "wb-expander " + iconMap.expanderCollapsed;
1274
+ }
1275
+ } else if (this._isLoading) {
1276
+ expanderSpan.className = "wb-expander " + iconMap.loading;
1277
+ } else if (this.lazy && this.children == null) {
1278
+ expanderSpan.className = "wb-expander " + iconMap.expanderLazy;
1279
+ } else {
1280
+ expanderSpan.classList.add("wb-indent");
1281
+ }
1282
+ }
1283
+ if (checkboxSpan) {
1284
+ if (this.selected) {
1285
+ checkboxSpan.className = "wb-checkbox " + iconMap.checkChecked;
1286
+ } else {
1287
+ checkboxSpan.className = "wb-checkbox " + iconMap.checkUnchecked;
1288
+ }
1289
+ }
1290
+
1291
+ if (this.titleWithHighlight) {
1292
+ titleSpan.innerHTML = this.titleWithHighlight;
1293
+ } else if (tree.options.escapeTitles) {
1294
+ titleSpan.textContent = this.title;
1295
+ } else {
1296
+ titleSpan.innerHTML = this.title;
1297
+ }
1298
+ // Set the width of the title span, so overflow ellipsis work
1299
+ if (!treeOptions.skeleton) {
1300
+ if (this.colspan) {
1301
+ let vpWidth = tree.element.clientWidth;
1302
+ titleSpan.style.width =
1303
+ vpWidth - (<any>nodeElem)._ofsTitlePx - ROW_EXTRA_PAD + "px";
1304
+ } else {
1305
+ titleSpan.style.width =
1306
+ columns[0]._widthPx -
1307
+ (<any>nodeElem)._ofsTitlePx -
1308
+ ROW_EXTRA_PAD +
1309
+ "px";
1310
+ }
1311
+ }
1312
+
1313
+ this._rowElem = rowDiv;
1314
+
1315
+ if (this.statusNodeType) {
1316
+ this._callEvent("renderStatusNode", {
1317
+ isNew: isNew,
1318
+ nodeElem: nodeElem,
1319
+ });
1320
+ } else if (this.parent) {
1321
+ // Skip root node
1322
+ this._callEvent("render", {
1323
+ isNew: isNew,
1324
+ nodeElem: nodeElem,
1325
+ typeInfo: typeInfo,
1326
+ colInfosById: this._getRenderInfo(),
1327
+ });
1328
+ }
1329
+
1330
+ // Attach to DOM as late as possible
1331
+ // if (!this._rowElem) {
1332
+ tree.nodeListElement.appendChild(rowDiv);
1333
+ // }
1334
+ }
1335
+
1336
+ /**
1337
+ * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
1338
+ * event is triggered on next expand.
1339
+ */
1340
+ resetLazy() {
1341
+ this.removeChildren();
1342
+ this.expanded = false;
1343
+ this.lazy = true;
1344
+ this.children = null;
1345
+ this.tree.updateViewport();
1346
+ }
1347
+
1348
+ /** Convert node (or whole branch) into a plain object.
1349
+ *
1350
+ * The result is compatible with node.addChildren().
1351
+ *
1352
+ * @param include child nodes
1353
+ * @param callback(dict, node) is called for every node, in order to allow
1354
+ * modifications.
1355
+ * Return `false` to ignore this node or `"skip"` to include this node
1356
+ * without its children.
1357
+ * @returns {NodeData}
1358
+ */
1359
+ toDict(recursive = false, callback?: any): any {
1360
+ const dict: any = {};
1361
+
1362
+ NODE_ATTRS.forEach((propName: string) => {
1363
+ const val = (<any>this)[propName];
1364
+
1365
+ if (val instanceof Set) {
1366
+ // Convert Set to string (or skip if set is empty)
1367
+ val.size
1368
+ ? (dict[propName] = Array.prototype.join.call(val.keys(), " "))
1369
+ : 0;
1370
+ } else if (val || val === false || val === 0) {
1371
+ dict[propName] = val;
1372
+ }
1373
+ });
1374
+ if (!util.isEmptyObject(this.data)) {
1375
+ dict.data = util.extend({}, this.data);
1376
+ if (util.isEmptyObject(dict.data)) {
1377
+ delete dict.data;
1378
+ }
1379
+ }
1380
+ if (callback) {
1381
+ const res = callback(dict, this);
1382
+ if (res === false) {
1383
+ return false; // Don't include this node nor its children
1384
+ }
1385
+ if (res === "skip") {
1386
+ recursive = false; // Include this node, but not the children
1387
+ }
1388
+ }
1389
+ if (recursive) {
1390
+ if (util.isArray(this.children)) {
1391
+ dict.children = [];
1392
+ for (let i = 0, l = this.children!.length; i < l; i++) {
1393
+ const node = this.children![i];
1394
+ if (!node.isStatusNode()) {
1395
+ const res = node.toDict(true, callback);
1396
+ if (res !== false) {
1397
+ dict.children.push(res);
1398
+ }
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1403
+ return dict;
1404
+ }
1405
+
1406
+ /** Return an option value that has a default, but may be overridden by a
1407
+ * callback or a node instance attribute.
1408
+ *
1409
+ * Evaluation sequence:
1410
+ *
1411
+ * If `tree.options.<name>` is a callback that returns something, use that.
1412
+ * Else if `node.<name>` is defined, use that.
1413
+ * Else if `tree.types[<node.type>]` is a value, use that.
1414
+ * Else if `tree.options.<name>` is a value, use that.
1415
+ * Else use `defaultValue`.
1416
+ *
1417
+ * @param name name of the option property (on node and tree)
1418
+ * @param defaultValue return this if nothing else matched
1419
+ */
1420
+ getOption(name: string, defaultValue?: any) {
1421
+ let tree = this.tree;
1422
+ let opts: any = tree.options;
1423
+
1424
+ // Lookup `name` in options dict
1425
+ if (name.indexOf(".") >= 0) {
1426
+ [opts, name] = name.split(".");
1427
+ }
1428
+ let value = opts[name]; // ?? defaultValue;
1429
+
1430
+ // A callback resolver always takes precedence
1431
+ if (typeof value === "function") {
1432
+ let res = value.call(tree, {
1433
+ type: "resolve",
1434
+ tree: tree,
1435
+ node: this,
1436
+ // typeInfo: this.type ? tree.types[this.type] : {},
1437
+ });
1438
+ if (res !== undefined) {
1439
+ return res;
1440
+ }
1441
+ }
1442
+ // If this node has an explicit local setting, use it:
1443
+ if ((<any>this)[name] !== undefined) {
1444
+ return (<any>this)[name];
1445
+ }
1446
+ // Use value from type definition if defined
1447
+ let typeInfo = this.type ? tree.types[this.type] : undefined;
1448
+ let res = typeInfo ? typeInfo[name] : undefined;
1449
+ if (res !== undefined) {
1450
+ return res;
1451
+ }
1452
+ // Use value from value options dict, fallback do default
1453
+ return value ?? defaultValue;
1454
+ }
1455
+
1456
+ async scrollIntoView(options?: any) {
1457
+ return this.tree.scrollTo(this);
1458
+ }
1459
+
1460
+ async setActive(flag: boolean = true, options?: any) {
1461
+ const tree = this.tree;
1462
+ const prev = tree.activeNode;
1463
+ const retrigger = options?.retrigger;
1464
+ const noEvent = options?.noEvent;
1465
+
1466
+ if (!noEvent) {
1467
+ let orgEvent = options?.event;
1468
+ if (flag) {
1469
+ if (prev !== this || retrigger) {
1470
+ if (
1471
+ prev?._callEvent("deactivate", {
1472
+ nextNode: this,
1473
+ orgEvent: orgEvent,
1474
+ }) === false
1475
+ ) {
1476
+ return;
1477
+ }
1478
+ if (
1479
+ this._callEvent("activate", {
1480
+ prevNode: prev,
1481
+ orgEvent: orgEvent,
1482
+ }) === false
1483
+ ) {
1484
+ tree.activeNode = null;
1485
+ prev?.setDirty(ChangeType.status);
1486
+ return;
1487
+ }
1488
+ }
1489
+ } else if (prev === this || retrigger) {
1490
+ this._callEvent("deactivate", { nextNode: null, orgEvent: orgEvent });
1491
+ }
1492
+ }
1493
+
1494
+ if (prev !== this) {
1495
+ tree.activeNode = this;
1496
+ prev?.setDirty(ChangeType.status);
1497
+ this.setDirty(ChangeType.status);
1498
+ }
1499
+ if (
1500
+ options &&
1501
+ options.colIdx != null &&
1502
+ options.colIdx !== tree.activeColIdx &&
1503
+ tree.navMode !== NavigationMode.row
1504
+ ) {
1505
+ tree.setColumn(options.colIdx);
1506
+ }
1507
+ // requestAnimationFrame(() => {
1508
+ // this.scrollIntoView();
1509
+ // })
1510
+ this.scrollIntoView();
1511
+ }
1512
+
1513
+ setDirty(type: ChangeType) {
1514
+ if (this.tree._disableUpdate) {
1515
+ return;
1516
+ }
1517
+ if (type === ChangeType.structure) {
1518
+ this.tree.updateViewport();
1519
+ } else if (this._rowElem) {
1520
+ // otherwise not in viewport, so no need to render
1521
+ this.render();
1522
+ }
1523
+ }
1524
+
1525
+ async setExpanded(flag: boolean = true, options?: any) {
1526
+ // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
1527
+ if (
1528
+ !flag &&
1529
+ this.isExpanded() &&
1530
+ this.getLevel() < this.getOption("minExpandLevel") &&
1531
+ !util.getOption(options, "force")
1532
+ ) {
1533
+ this.logDebug("Ignored collapse request.");
1534
+ return;
1535
+ }
1536
+ if (flag && this.lazy && this.children == null) {
1537
+ await this.loadLazy();
1538
+ }
1539
+ this.expanded = flag;
1540
+ this.setDirty(ChangeType.structure);
1541
+ }
1542
+
1543
+ setIcon() {
1544
+ throw new Error("Not yet implemented");
1545
+ // this.setDirty(ChangeType.status);
1546
+ }
1547
+
1548
+ setFocus(flag: boolean = true, options?: any) {
1549
+ const prev = this.tree.focusNode;
1550
+ this.tree.focusNode = this;
1551
+ prev?.setDirty(ChangeType.status);
1552
+ this.setDirty(ChangeType.status);
1553
+ }
1554
+
1555
+ setSelected(flag: boolean = true, options?: any) {
1556
+ const prev = this.selected;
1557
+ if (!!flag !== prev) {
1558
+ this._callEvent("select", { flag: flag });
1559
+ }
1560
+ this.selected = !!flag;
1561
+ this.setDirty(ChangeType.status);
1562
+ }
1563
+
1564
+ /** Show node status (ok, loading, error, noData) using styles and a dummy child node.
1565
+ */
1566
+ setStatus(
1567
+ status: NodeStatusType,
1568
+ message?: string,
1569
+ details?: string
1570
+ ): WunderbaumNode | null {
1571
+ let tree = this.tree;
1572
+ let statusNode: WunderbaumNode | null = null;
1573
+
1574
+ const _clearStatusNode = () => {
1575
+ // Remove dedicated dummy node, if any
1576
+ let children = this.children;
1577
+
1578
+ if (children && children.length && children[0].isStatusNode()) {
1579
+ children[0].remove();
1580
+ }
1581
+ };
1582
+
1583
+ const _setStatusNode = (data: any) => {
1584
+ // Create/modify the dedicated dummy node for 'loading...' or
1585
+ // 'error!' status. (only called for direct child of the invisible
1586
+ // system root)
1587
+ let children = this.children;
1588
+ let firstChild = children ? children[0] : null;
1589
+
1590
+ util.assert(data.statusNodeType);
1591
+ util.assert(!firstChild || !firstChild.isStatusNode());
1592
+
1593
+ statusNode = this.addNode(data, "firstChild");
1594
+ statusNode.match = true;
1595
+ tree.setModified(ChangeType.structure);
1596
+
1597
+ return statusNode;
1598
+ };
1599
+
1600
+ _clearStatusNode();
1601
+
1602
+ switch (status) {
1603
+ case "ok":
1604
+ this._isLoading = false;
1605
+ this._errorInfo = null;
1606
+ break;
1607
+ case "loading":
1608
+ // If this is the invisible root, add a visible top-level node
1609
+ if (!this.parent) {
1610
+ _setStatusNode({
1611
+ statusNodeType: status,
1612
+ title:
1613
+ tree.options.strings.loading +
1614
+ (message ? " (" + message + ")" : ""),
1615
+ checkbox: false,
1616
+ colspan: true,
1617
+ tooltip: details,
1618
+ });
1619
+ }
1620
+ this._isLoading = true;
1621
+ this._errorInfo = null;
1622
+ // this.render();
1623
+ break;
1624
+ case "error":
1625
+ _setStatusNode({
1626
+ statusNodeType: status,
1627
+ title:
1628
+ tree.options.strings.loadError +
1629
+ (message ? " (" + message + ")" : ""),
1630
+ checkbox: false,
1631
+ colspan: true,
1632
+ // classes: "wb-center",
1633
+ tooltip: details,
1634
+ });
1635
+ this._isLoading = false;
1636
+ this._errorInfo = { message: message, details: details };
1637
+ break;
1638
+ case "noData":
1639
+ _setStatusNode({
1640
+ statusNodeType: status,
1641
+ title: message || tree.options.strings.noData,
1642
+ checkbox: false,
1643
+ colspan: true,
1644
+ tooltip: details,
1645
+ });
1646
+ this._isLoading = false;
1647
+ this._errorInfo = null;
1648
+ break;
1649
+ default:
1650
+ util.error("invalid node status " + status);
1651
+ }
1652
+ tree.updateViewport();
1653
+ return statusNode;
1654
+ }
1655
+
1656
+ setTitle(title: string): void {
1657
+ this.title = title;
1658
+ this.setDirty(ChangeType.status);
1659
+ // this.triggerModify("rename"); // TODO
1660
+ }
1661
+
1662
+ /**
1663
+ * Trigger `modifyChild` event on a parent to signal that a child was modified.
1664
+ * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
1665
+ */
1666
+ triggerModifyChild(
1667
+ operation: string,
1668
+ child: WunderbaumNode | null,
1669
+ extra?: any
1670
+ ) {
1671
+ if (!this.tree.options.modifyChild) return;
1672
+
1673
+ if (child && child.parent !== this) {
1674
+ util.error("child " + child + " is not a child of " + this);
1675
+ }
1676
+ this._callEvent(
1677
+ "modifyChild",
1678
+ util.extend({ operation: operation, child: child }, extra)
1679
+ );
1680
+ }
1681
+
1682
+ /**
1683
+ * Trigger `modifyChild` event on node.parent(!).
1684
+ * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
1685
+ * @param {object} [extra]
1686
+ */
1687
+ triggerModify(operation: string, extra: any) {
1688
+ this.parent.triggerModifyChild(operation, this, extra);
1689
+ }
1690
+
1691
+ /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br>
1692
+ * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
1693
+ * Return false if iteration was stopped.
1694
+ *
1695
+ * @param {function} callback the callback function.
1696
+ * Return false to stop iteration, return "skip" to skip this node and
1697
+ * its children only.
1698
+ */
1699
+ visit(
1700
+ callback: NodeVisitCallback,
1701
+ includeSelf: boolean = false
1702
+ ): NodeVisitResponse {
1703
+ let i,
1704
+ l,
1705
+ res: any = true,
1706
+ children = this.children;
1707
+
1708
+ if (includeSelf === true) {
1709
+ res = callback(this);
1710
+ if (res === false || res === "skip") {
1711
+ return res;
1712
+ }
1713
+ }
1714
+ if (children) {
1715
+ for (i = 0, l = children.length; i < l; i++) {
1716
+ res = children[i].visit(callback, true);
1717
+ if (res === false) {
1718
+ break;
1719
+ }
1720
+ }
1721
+ }
1722
+ return res;
1723
+ }
1724
+
1725
+ /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.<br>
1726
+ * Stop iteration, if callback() returns false.<br>
1727
+ * Return false if iteration was stopped.
1728
+ *
1729
+ * @param callback the callback function. Return false to stop iteration
1730
+ */
1731
+ visitParents(
1732
+ callback: (node: WunderbaumNode) => boolean | void,
1733
+ includeSelf: boolean = false
1734
+ ): boolean {
1735
+ if (includeSelf && callback(this) === false) {
1736
+ return false;
1737
+ }
1738
+ let p = this.parent;
1739
+ while (p) {
1740
+ if (callback(p) === false) {
1741
+ return false;
1742
+ }
1743
+ p = p.parent;
1744
+ }
1745
+ return true;
1746
+ }
1747
+
1748
+ /** Call fn(node) for all sibling nodes.<br>
1749
+ * Stop iteration, if fn() returns false.<br>
1750
+ * Return false if iteration was stopped.
1751
+ *
1752
+ * @param {function} fn the callback function.
1753
+ * Return false to stop iteration.
1754
+ */
1755
+ visitSiblings(
1756
+ callback: (node: WunderbaumNode) => boolean | void,
1757
+ includeSelf: boolean = false
1758
+ ): boolean {
1759
+ let i,
1760
+ l,
1761
+ n,
1762
+ ac = this.parent.children!;
1763
+
1764
+ for (i = 0, l = ac.length; i < l; i++) {
1765
+ n = ac[i];
1766
+ if (includeSelf || n !== this) {
1767
+ if (callback(n) === false) {
1768
+ return false;
1769
+ }
1770
+ }
1771
+ }
1772
+ return true;
1773
+ }
1774
+ /**
1775
+ * [ext-filter] Return true if this node is matched by current filter (or no filter is active).
1776
+ */
1777
+ isMatched() {
1778
+ return !(this.tree.filterMode && !this.match);
1779
+ }
1780
+ }