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/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/wunderbaum.css +5 -0
- package/dist/wunderbaum.d.ts +1348 -0
- package/dist/wunderbaum.esm.js +5266 -0
- package/dist/wunderbaum.esm.min.js +69 -0
- package/dist/wunderbaum.esm.min.js.map +1 -0
- package/dist/wunderbaum.umd.js +5276 -0
- package/dist/wunderbaum.umd.min.js +71 -0
- package/dist/wunderbaum.umd.min.js.map +1 -0
- package/package.json +123 -0
- package/src/common.ts +198 -0
- package/src/debounce.ts +365 -0
- package/src/deferred.ts +50 -0
- package/src/util.ts +656 -0
- package/src/wb_ext_dnd.ts +336 -0
- package/src/wb_ext_edit.ts +340 -0
- package/src/wb_ext_filter.ts +370 -0
- package/src/wb_ext_keynav.ts +210 -0
- package/src/wb_ext_logger.ts +54 -0
- package/src/wb_extension_base.ts +76 -0
- package/src/wb_node.ts +1780 -0
- package/src/wb_options.ts +117 -0
- package/src/wunderbaum.scss +509 -0
- package/src/wunderbaum.ts +1819 -0
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
|
+
}
|