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.
@@ -0,0 +1,1819 @@
1
+ /*!
2
+ * wunderbaum.ts
3
+ *
4
+ * A tree control.
5
+ *
6
+ * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
7
+ * Released under the MIT license.
8
+ *
9
+ * @version @VERSION
10
+ * @date @DATE
11
+ */
12
+
13
+ import "./wunderbaum.scss";
14
+ import * as util from "./util";
15
+ import { FilterExtension } from "./wb_ext_filter";
16
+ import { KeynavExtension } from "./wb_ext_keynav";
17
+ import { LoggerExtension } from "./wb_ext_logger";
18
+ import { DndExtension } from "./wb_ext_dnd";
19
+ import { ExtensionsDict, WunderbaumExtension } from "./wb_extension_base";
20
+
21
+ import {
22
+ NavigationMode,
23
+ ChangeType,
24
+ DEFAULT_DEBUGLEVEL,
25
+ FilterModeType,
26
+ makeNodeTitleStartMatcher,
27
+ MatcherType,
28
+ NavigationModeOption,
29
+ NodeStatusType,
30
+ RENDER_MAX_PREFETCH,
31
+ ROW_HEIGHT,
32
+ TargetType as NodeRegion,
33
+ ApplyCommandType,
34
+ } from "./common";
35
+ import { WunderbaumNode } from "./wb_node";
36
+ import { Deferred } from "./deferred";
37
+ import { DebouncedFunction, throttle } from "./debounce";
38
+ import { EditExtension } from "./wb_ext_edit";
39
+ import { WunderbaumOptions } from "./wb_options";
40
+
41
+ // const class_prefix = "wb-";
42
+ // const node_props: string[] = ["title", "key", "refKey"];
43
+ const MAX_CHANGED_NODES = 10;
44
+
45
+ /**
46
+ * A persistent plain object or array.
47
+ *
48
+ * See also [[WunderbaumOptions]].
49
+ */
50
+ export class Wunderbaum {
51
+ static version: string = "@VERSION"; // Set to semver by 'grunt release'
52
+ static sequence = 0;
53
+
54
+ /** The invisible root node, that holds all visible top level nodes. */
55
+ readonly root: WunderbaumNode;
56
+ readonly id: string;
57
+
58
+ readonly element: HTMLDivElement;
59
+ // readonly treeElement: HTMLDivElement;
60
+ readonly headerElement: HTMLDivElement | null;
61
+ readonly scrollContainer: HTMLDivElement;
62
+ readonly nodeListElement: HTMLDivElement;
63
+ readonly _updateViewportThrottled: DebouncedFunction<(...args: any) => void>;
64
+
65
+ protected extensionList: WunderbaumExtension[] = [];
66
+ protected extensions: ExtensionsDict = {};
67
+ // protected extensionMap = new Map<string, WunderbaumExtension>();
68
+
69
+ /** Merged options from constructor args and tree- and extension defaults. */
70
+ public options: WunderbaumOptions;
71
+
72
+ protected keyMap = new Map<string, WunderbaumNode>();
73
+ protected refKeyMap = new Map<string, Set<WunderbaumNode>>();
74
+ protected viewNodes = new Set<WunderbaumNode>();
75
+ // protected rows: WunderbaumNode[] = [];
76
+ // protected _rowCount = 0;
77
+
78
+ // protected eventHandlers : Array<function> = [];
79
+
80
+ public activeNode: WunderbaumNode | null = null;
81
+ public focusNode: WunderbaumNode | null = null;
82
+ _disableUpdate = 0;
83
+ _disableUpdateCount = 0;
84
+
85
+ /** Shared properties, referenced by `node.type`. */
86
+ public types: { [key: string]: any } = {};
87
+ /** List of column definitions. */
88
+ public columns: any[] = [];
89
+ public _columnsById: { [key: string]: any } = {};
90
+
91
+ protected resizeObserver;
92
+
93
+ // Modification Status
94
+ protected changedSince = 0;
95
+ protected changes = new Set<ChangeType>();
96
+ protected changedNodes = new Set<WunderbaumNode>();
97
+
98
+ // --- FILTER ---
99
+ public filterMode: FilterModeType = null;
100
+
101
+ // --- KEYNAV ---
102
+ public activeColIdx = 0;
103
+ public navMode = NavigationMode.row;
104
+ public lastQuicksearchTime = 0;
105
+ public lastQuicksearchTerm = "";
106
+
107
+ // --- EDIT ---
108
+ protected lastClickTime = 0;
109
+
110
+ readonly ready: Promise<any>;
111
+
112
+ public static util = util;
113
+ // TODO: make accessible in compiled JS like this?
114
+ public _util = util;
115
+
116
+ constructor(options: WunderbaumOptions) {
117
+ let opts = (this.options = util.extend(
118
+ {
119
+ id: null,
120
+ source: null, // URL for GET/PUT, ajax options, or callback
121
+ element: null, // <div class="wunderbaum">
122
+ debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
123
+ header: null, // Show/hide header (pass bool or string)
124
+ headerHeightPx: ROW_HEIGHT,
125
+ rowHeightPx: ROW_HEIGHT,
126
+ columns: null,
127
+ types: null,
128
+ escapeTitles: true,
129
+ showSpinner: false,
130
+ checkbox: true,
131
+ minExpandLevel: 0,
132
+ updateThrottleWait: 200,
133
+ skeleton: false,
134
+ // --- KeyNav ---
135
+ navigationMode: NavigationModeOption.startRow,
136
+ quicksearch: true,
137
+ // --- Events ---
138
+ change: util.noop,
139
+ enhanceTitle: util.noop,
140
+ error: util.noop,
141
+ receive: util.noop,
142
+ // --- Strings ---
143
+ strings: {
144
+ loadError: "Error",
145
+ loading: "Loading...",
146
+ // loading: "Loading&hellip;",
147
+ noData: "No data",
148
+ },
149
+ },
150
+ options
151
+ ));
152
+
153
+ const readyDeferred = new Deferred();
154
+ this.ready = readyDeferred.promise();
155
+ let readyOk = false;
156
+ this.ready
157
+ .then(() => {
158
+ readyOk = true;
159
+ try {
160
+ this._callEvent("init");
161
+ } catch (error) {
162
+ // We re-raise in the reject handler, but Chrome resets the stack
163
+ // frame then, so we log it here:
164
+ console.error("Exception inside `init(e)` event:", error);
165
+ }
166
+ })
167
+ .catch((err) => {
168
+ if (readyOk) {
169
+ // Error occurred in `init` handler. We can re-raise, but Chrome
170
+ // resets the stack frame.
171
+ throw err;
172
+ } else {
173
+ // Error in load process
174
+ this._callEvent("init", { error: err });
175
+ }
176
+ });
177
+
178
+ this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
179
+ this.root = new WunderbaumNode(this, <WunderbaumNode>(<unknown>null), {
180
+ key: "__root__",
181
+ // title: "__root__",
182
+ });
183
+
184
+ this._registerExtension(new KeynavExtension(this));
185
+ this._registerExtension(new EditExtension(this));
186
+ this._registerExtension(new FilterExtension(this));
187
+ this._registerExtension(new DndExtension(this));
188
+ this._registerExtension(new LoggerExtension(this));
189
+
190
+ // --- Evaluate options
191
+ this.columns = opts.columns;
192
+ delete opts.columns;
193
+ if (!this.columns) {
194
+ let defaultName = typeof opts.header === "string" ? opts.header : this.id;
195
+ this.columns = [{ id: "*", title: defaultName, width: "*" }];
196
+ }
197
+
198
+ this.types = opts.types || {};
199
+ delete opts.types;
200
+ // Convert `TYPE.classes` to a Set
201
+ for (let t of Object.values(this.types) as any) {
202
+ if (t.classes) {
203
+ t.classes = util.toSet(t.classes);
204
+ }
205
+ }
206
+
207
+ if (this.columns.length === 1) {
208
+ opts.navigationMode = NavigationModeOption.row;
209
+ }
210
+
211
+ if (
212
+ opts.navigationMode === NavigationModeOption.cell ||
213
+ opts.navigationMode === NavigationModeOption.startCell
214
+ ) {
215
+ this.navMode = NavigationMode.cellNav;
216
+ }
217
+
218
+ this._updateViewportThrottled = throttle(
219
+ () => {
220
+ this._updateViewport();
221
+ },
222
+ opts.updateThrottleWait,
223
+ { leading: true, trailing: true }
224
+ );
225
+
226
+ // --- Create Markup
227
+ this.element = util.elemFromSelector(opts.element) as HTMLDivElement;
228
+ util.assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
229
+
230
+ this.element.classList.add("wunderbaum");
231
+ if (!this.element.getAttribute("tabindex")) {
232
+ this.element.tabIndex = 0;
233
+ }
234
+
235
+ // Attach tree instance to <div>
236
+ (<any>this.element)._wb_tree = this;
237
+
238
+ // Create header markup, or take it from the existing html
239
+
240
+ this.headerElement = this.element.querySelector(
241
+ "div.wb-header"
242
+ ) as HTMLDivElement;
243
+
244
+ const wantHeader =
245
+ opts.header == null ? this.columns.length > 1 : !!opts.header;
246
+
247
+ if (this.headerElement) {
248
+ // User existing header markup to define `this.columns`
249
+ util.assert(
250
+ !this.columns,
251
+ "`opts.columns` must not be set if markup already contains a header"
252
+ );
253
+ this.columns = [];
254
+ const rowElement = this.headerElement.querySelector(
255
+ "div.wb-row"
256
+ ) as HTMLDivElement;
257
+ for (const colDiv of rowElement.querySelectorAll("div")) {
258
+ this.columns.push({
259
+ id: colDiv.dataset.id || null,
260
+ text: "" + colDiv.textContent,
261
+ });
262
+ }
263
+ } else if (wantHeader) {
264
+ // We need a row div, the rest will be computed from `this.columns`
265
+ const coldivs = "<span class='wb-col'></span>".repeat(
266
+ this.columns.length
267
+ );
268
+ this.element.innerHTML = `
269
+ <div class='wb-header'>
270
+ <div class='wb-row'>
271
+ ${coldivs}
272
+ </div>
273
+ </div>`;
274
+ // this.updateColumns({ render: false });
275
+ } else {
276
+ this.element.innerHTML = "";
277
+ }
278
+
279
+ //
280
+ this.element.innerHTML += `
281
+ <div class="wb-scroll-container">
282
+ <div class="wb-node-list"></div>
283
+ </div>`;
284
+ this.scrollContainer = this.element.querySelector(
285
+ "div.wb-scroll-container"
286
+ ) as HTMLDivElement;
287
+ this.nodeListElement = this.scrollContainer.querySelector(
288
+ "div.wb-node-list"
289
+ ) as HTMLDivElement;
290
+ this.headerElement = this.element.querySelector(
291
+ "div.wb-header"
292
+ ) as HTMLDivElement;
293
+
294
+ if (this.columns.length > 1) {
295
+ this.element.classList.add("wb-grid");
296
+ }
297
+
298
+ this._initExtensions();
299
+
300
+ // --- Load initial data
301
+ if (opts.source) {
302
+ if (opts.showSpinner) {
303
+ this.nodeListElement.innerHTML =
304
+ "<progress class='spinner'>loading...</progress>";
305
+ }
306
+ this.load(opts.source)
307
+ .then(() => {
308
+ readyDeferred.resolve();
309
+ })
310
+ .catch((error) => {
311
+ readyDeferred.reject(error);
312
+ })
313
+ .finally(() => {
314
+ this.element.querySelector("progress.spinner")?.remove();
315
+ this.element.classList.remove("wb-initializing");
316
+ // this.updateViewport();
317
+ });
318
+ } else {
319
+ // this.updateViewport();
320
+ readyDeferred.resolve();
321
+ }
322
+
323
+ // TODO: This is sometimes required, because this.element.clientWidth
324
+ // has a wrong value at start???
325
+ setTimeout(() => {
326
+ this.updateViewport();
327
+ }, 50);
328
+
329
+ // --- Bind listeners
330
+ this.scrollContainer.addEventListener("scroll", (e: Event) => {
331
+ this.updateViewport();
332
+ });
333
+
334
+ // window.addEventListener("resize", (e: Event) => {
335
+ // this.updateViewport();
336
+ // });
337
+ this.resizeObserver = new ResizeObserver((entries) => {
338
+ this.updateViewport();
339
+ console.log("ResizeObserver: Size changed", entries);
340
+ });
341
+ this.resizeObserver.observe(this.element);
342
+
343
+ util.onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
344
+ const info = Wunderbaum.getEventInfo(e);
345
+ const node = info.node;
346
+
347
+ if (
348
+ this._callEvent("click", { event: e, node: node, info: info }) === false
349
+ ) {
350
+ this.lastClickTime = Date.now();
351
+ return false;
352
+ }
353
+ if (node) {
354
+ // Edit title if 'clickActive' is triggered:
355
+ const trigger = this.getOption("edit.trigger");
356
+ const slowClickDelay = this.getOption("edit.slowClickDelay");
357
+ if (
358
+ trigger.indexOf("clickActive") >= 0 &&
359
+ info.region === "title" &&
360
+ node.isActive() &&
361
+ (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)
362
+ ) {
363
+ this._callMethod("edit.startEditTitle", node);
364
+ }
365
+
366
+ if (info.colIdx >= 0) {
367
+ node.setActive(true, { colIdx: info.colIdx, event: e });
368
+ } else {
369
+ node.setActive(true, { event: e });
370
+ }
371
+
372
+ if (info.region === NodeRegion.expander) {
373
+ node.setExpanded(!node.isExpanded());
374
+ } else if (info.region === NodeRegion.checkbox) {
375
+ node.setSelected(!node.isSelected());
376
+ }
377
+ }
378
+ // if(e.target.classList.)
379
+ // this.log("click", info);
380
+ this.lastClickTime = Date.now();
381
+ });
382
+
383
+ util.onEvent(this.element, "keydown", (e) => {
384
+ const info = Wunderbaum.getEventInfo(e);
385
+ const eventName = util.eventToString(e);
386
+ this._callHook("onKeyEvent", {
387
+ event: e,
388
+ node: info.node,
389
+ info: info,
390
+ eventName: eventName,
391
+ });
392
+ });
393
+
394
+ util.onEvent(this.element, "focusin focusout", (e) => {
395
+ const flag = e.type === "focusin";
396
+
397
+ this._callEvent("focus", { flag: flag, event: e });
398
+ if (!flag) {
399
+ this._callMethod("edit._stopEditTitle", true, {
400
+ event: e,
401
+ forceClose: true,
402
+ });
403
+ }
404
+ // if (flag && !this.activeNode ) {
405
+ // setTimeout(() => {
406
+ // if (!this.activeNode) {
407
+ // const firstNode = this.getFirstChild();
408
+ // if (firstNode && !firstNode?.isStatusNode()) {
409
+ // firstNode.logInfo("Activate on focus", e);
410
+ // firstNode.setActive(true, { event: e });
411
+ // }
412
+ // }
413
+ // }, 10);
414
+ // }
415
+ });
416
+ }
417
+
418
+ /** */
419
+ // _renderHeader(){
420
+ // const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
421
+ // this.element.innerHTML = `
422
+ // <div class='wb-header'>
423
+ // <div class='wb-row'>
424
+ // ${coldivs}
425
+ // </div>
426
+ // </div>`;
427
+
428
+ // }
429
+
430
+ /** Return a Wunderbaum instance, from element, index, or event.
431
+ *
432
+ * @example
433
+ * getTree(); // Get first Wunderbaum instance on page
434
+ * getTree(1); // Get second Wunderbaum instance on page
435
+ * getTree(event); // Get tree for this mouse- or keyboard event
436
+ * getTree("foo"); // Get tree for this `tree.options.id`
437
+ * getTree("#tree"); // Get tree for this matching element
438
+ */
439
+ public static getTree(
440
+ el?: Element | Event | number | string | WunderbaumNode
441
+ ): Wunderbaum | null {
442
+ if (el instanceof Wunderbaum) {
443
+ return el;
444
+ } else if (el instanceof WunderbaumNode) {
445
+ return el.tree;
446
+ }
447
+ if (el === undefined) {
448
+ el = 0; // get first tree
449
+ }
450
+ if (typeof el === "number") {
451
+ el = document.querySelectorAll(".wunderbaum")[el]; // el was an integer: return nth element
452
+ } else if (typeof el === "string") {
453
+ // Search all trees for matching ID
454
+ for (let treeElem of document.querySelectorAll(".wunderbaum")) {
455
+ const tree = (<any>treeElem)._wb_tree;
456
+ if (tree && tree.id === el) {
457
+ return tree;
458
+ }
459
+ }
460
+ // Search by selector
461
+ el = document.querySelector(el)!;
462
+ if (!el) {
463
+ return null;
464
+ }
465
+ } else if ((<Event>el).target) {
466
+ el = (<Event>el).target as Element;
467
+ }
468
+ util.assert(el instanceof Element);
469
+ if (!(<HTMLElement>el).matches(".wunderbaum")) {
470
+ el = (<HTMLElement>el).closest(".wunderbaum")!;
471
+ }
472
+
473
+ if (el && (<any>el)._wb_tree) {
474
+ return (<any>el)._wb_tree;
475
+ }
476
+ return null;
477
+ }
478
+
479
+ /** Return a WunderbaumNode instance from element, event.
480
+ *
481
+ * @param el
482
+ */
483
+ public static getNode(el: Element | Event): WunderbaumNode | null {
484
+ if (!el) {
485
+ return null;
486
+ } else if (el instanceof WunderbaumNode) {
487
+ return el;
488
+ } else if ((<Event>el).target !== undefined) {
489
+ el = (<Event>el).target! as Element; // el was an Event
490
+ }
491
+ // `el` is a DOM element
492
+ // let nodeElem = obj.closest("div.wb-row");
493
+ while (el) {
494
+ if ((<any>el)._wb_node) {
495
+ return (<any>el)._wb_node as WunderbaumNode;
496
+ }
497
+ el = (<Element>el).parentElement!; //.parentNode;
498
+ }
499
+ return null;
500
+ }
501
+
502
+ /** */
503
+ protected _registerExtension(extension: WunderbaumExtension): void {
504
+ this.extensionList.push(extension);
505
+ this.extensions[extension.id] = extension;
506
+ // this.extensionMap.set(extension.id, extension);
507
+ }
508
+
509
+ /** Called on tree (re)init after markup is created, before loading. */
510
+ protected _initExtensions(): void {
511
+ for (let ext of this.extensionList) {
512
+ ext.init();
513
+ }
514
+ }
515
+
516
+ /** Add node to tree's bookkeeping data structures. */
517
+ _registerNode(node: WunderbaumNode): void {
518
+ let key = node.key;
519
+ util.assert(key != null && !this.keyMap.has(key));
520
+ this.keyMap.set(key, node);
521
+ let rk = node.refKey;
522
+ if (rk) {
523
+ let rks = this.refKeyMap.get(rk); // Set of nodes with this refKey
524
+ if (rks) {
525
+ rks.add(node);
526
+ } else {
527
+ this.refKeyMap.set(rk, new Set());
528
+ }
529
+ }
530
+ }
531
+
532
+ /** Remove node from tree's bookkeeping data structures. */
533
+ _unregisterNode(node: WunderbaumNode): void {
534
+ const rk = node.refKey;
535
+ if (rk) {
536
+ const rks = this.refKeyMap.get(rk);
537
+ if (rks && rks.delete(node) && !rks.size) {
538
+ // We just removed the last element
539
+ this.refKeyMap.delete(rk);
540
+ }
541
+ }
542
+ // mark as disposed
543
+ (node.tree as any) = null;
544
+ (node.parent as any) = null;
545
+ // node.title = "DISPOSED: " + node.title
546
+ this.viewNodes.delete(node);
547
+ node.removeMarkup();
548
+ }
549
+
550
+ /** Call all hook methods of all registered extensions.*/
551
+ protected _callHook(hook: keyof WunderbaumExtension, data: any = {}): any {
552
+ let res;
553
+ let d = util.extend(
554
+ {},
555
+ { tree: this, options: this.options, result: undefined },
556
+ data
557
+ );
558
+
559
+ for (let ext of this.extensionList) {
560
+ res = (<any>ext[hook]).call(ext, d);
561
+ if (res === false) {
562
+ break;
563
+ }
564
+ if (d.result !== undefined) {
565
+ res = d.result;
566
+ }
567
+ }
568
+ return res;
569
+ }
570
+
571
+ /** Call tree method or extension method if defined.
572
+ * Example:
573
+ * ```js
574
+ * tree._callMethod("edit.startEdit", "arg1", "arg2")
575
+ * ```
576
+ */
577
+ _callMethod(name: string, ...args: any[]): any {
578
+ const [p, n] = name.split(".");
579
+ const obj = n ? this.extensions[p] : this;
580
+ const func = (<any>obj)[n];
581
+ if (func) {
582
+ return func.apply(obj, args);
583
+ } else {
584
+ this.logError(`Calling undefined method '${name}()'.`);
585
+ }
586
+ }
587
+
588
+ /** Call event handler if defined in tree.options.
589
+ * Example:
590
+ * ```js
591
+ * tree._callEvent("edit.beforeEdit", {foo: 42})
592
+ * ```
593
+ */
594
+ _callEvent(name: string, extra?: any): any {
595
+ const [p, n] = name.split(".");
596
+ const opts = this.options as any;
597
+ const func = n ? opts[p][n] : opts[p];
598
+ if (func) {
599
+ return func.call(
600
+ this,
601
+ util.extend({ name: name, tree: this, util: this._util }, extra)
602
+ );
603
+ // } else {
604
+ // this.logError(`Triggering undefined event '${name}'.`)
605
+ }
606
+ }
607
+
608
+ /** Return the topmost visible node in the viewport */
609
+ protected _firstNodeInView(complete = true) {
610
+ let topIdx: number, node: WunderbaumNode;
611
+ if (complete) {
612
+ topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
613
+ } else {
614
+ topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
615
+ }
616
+ // TODO: start searching from active node (reverse)
617
+ this.visitRows((n) => {
618
+ if (n._rowIdx === topIdx) {
619
+ node = n;
620
+ return false;
621
+ }
622
+ });
623
+ return <WunderbaumNode>node!;
624
+ }
625
+
626
+ /** Return the lowest visible node in the viewport */
627
+ protected _lastNodeInView(complete = true) {
628
+ let bottomIdx: number, node: WunderbaumNode;
629
+ if (complete) {
630
+ bottomIdx =
631
+ Math.floor(
632
+ (this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
633
+ ROW_HEIGHT
634
+ ) - 1;
635
+ } else {
636
+ bottomIdx =
637
+ Math.ceil(
638
+ (this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
639
+ ROW_HEIGHT
640
+ ) - 1;
641
+ }
642
+ // TODO: start searching from active node
643
+ this.visitRows((n) => {
644
+ if (n._rowIdx === bottomIdx) {
645
+ node = n;
646
+ return false;
647
+ }
648
+ });
649
+ return <WunderbaumNode>node!;
650
+ }
651
+
652
+ /** Return preceeding visible node in the viewport */
653
+ protected _getPrevNodeInView(node?: WunderbaumNode, ofs = 1) {
654
+ this.visitRows(
655
+ (n) => {
656
+ node = n;
657
+ if (ofs-- <= 0) {
658
+ return false;
659
+ }
660
+ },
661
+ { reverse: true, start: node || this.getActiveNode() }
662
+ );
663
+ return node;
664
+ }
665
+
666
+ /** Return following visible node in the viewport */
667
+ protected _getNextNodeInView(node?: WunderbaumNode, ofs = 1) {
668
+ this.visitRows(
669
+ (n) => {
670
+ node = n;
671
+ if (ofs-- <= 0) {
672
+ return false;
673
+ }
674
+ },
675
+ { reverse: false, start: node || this.getActiveNode() }
676
+ );
677
+ return node;
678
+ }
679
+
680
+ addChildren(nodeData: any, options?: any): WunderbaumNode {
681
+ return this.root.addChildren(nodeData, options);
682
+ }
683
+
684
+ /**
685
+ * Apply a modification (or navigation) operation on the tree or active node.
686
+ * @returns
687
+ */
688
+ applyCommand(cmd: ApplyCommandType, opts?: any): any;
689
+
690
+ /**
691
+ * Apply a modification (or navigation) operation on a node.
692
+ * @returns
693
+ */
694
+ applyCommand(cmd: ApplyCommandType, node: WunderbaumNode, opts?: any): any;
695
+
696
+ /*
697
+ * Apply a modification or navigation operation.
698
+ *
699
+ * Most of these commands simply map to a node or tree method.
700
+ * This method is especially useful when implementing keyboard mapping,
701
+ * context menus, or external buttons.
702
+ *
703
+ * Valid commands:
704
+ * - 'moveUp', 'moveDown'
705
+ * - 'indent', 'outdent'
706
+ * - 'remove'
707
+ * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension)
708
+ * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard')
709
+ * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate
710
+ *
711
+ */
712
+ applyCommand(
713
+ cmd: ApplyCommandType,
714
+ nodeOrOpts?: WunderbaumNode | any,
715
+ opts?: any
716
+ ): any {
717
+ let // clipboard,
718
+ node,
719
+ refNode;
720
+ // opts = $.extend(
721
+ // { setActive: true, clipboard: CLIPBOARD },
722
+ // opts_
723
+ // );
724
+ if (nodeOrOpts instanceof WunderbaumNode) {
725
+ node = nodeOrOpts;
726
+ } else {
727
+ node = this.getActiveNode()!;
728
+ util.assert(opts === undefined);
729
+ opts = nodeOrOpts;
730
+ }
731
+ // clipboard = opts.clipboard;
732
+
733
+ switch (cmd) {
734
+ // Sorting and indentation:
735
+ case "moveUp":
736
+ refNode = node.getPrevSibling();
737
+ if (refNode) {
738
+ node.moveTo(refNode, "before");
739
+ node.setActive();
740
+ }
741
+ break;
742
+ case "moveDown":
743
+ refNode = node.getNextSibling();
744
+ if (refNode) {
745
+ node.moveTo(refNode, "after");
746
+ node.setActive();
747
+ }
748
+ break;
749
+ case "indent":
750
+ refNode = node.getPrevSibling();
751
+ if (refNode) {
752
+ node.moveTo(refNode, "appendChild");
753
+ refNode.setExpanded();
754
+ node.setActive();
755
+ }
756
+ break;
757
+ case "outdent":
758
+ if (!node.isTopLevel()) {
759
+ node.moveTo(node.getParent()!, "after");
760
+ node.setActive();
761
+ }
762
+ break;
763
+ // Remove:
764
+ case "remove":
765
+ refNode = node.getPrevSibling() || node.getParent();
766
+ node.remove();
767
+ if (refNode) {
768
+ refNode.setActive();
769
+ }
770
+ break;
771
+ // Add, edit (requires ext-edit):
772
+ case "addChild":
773
+ this._callMethod("edit.createNode", "prependChild");
774
+ break;
775
+ case "addSibling":
776
+ this._callMethod("edit.createNode", "after");
777
+ break;
778
+ case "rename":
779
+ this._callMethod("edit.startEditTitle");
780
+ break;
781
+ // Simple clipboard simulation:
782
+ // case "cut":
783
+ // clipboard = { mode: cmd, data: node };
784
+ // break;
785
+ // case "copy":
786
+ // clipboard = {
787
+ // mode: cmd,
788
+ // data: node.toDict(function(d, n) {
789
+ // delete d.key;
790
+ // }),
791
+ // };
792
+ // break;
793
+ // case "clear":
794
+ // clipboard = null;
795
+ // break;
796
+ // case "paste":
797
+ // if (clipboard.mode === "cut") {
798
+ // // refNode = node.getPrevSibling();
799
+ // clipboard.data.moveTo(node, "child");
800
+ // clipboard.data.setActive();
801
+ // } else if (clipboard.mode === "copy") {
802
+ // node.addChildren(clipboard.data).setActive();
803
+ // }
804
+ // break;
805
+ // Navigation commands:
806
+ case "down":
807
+ case "first":
808
+ case "last":
809
+ case "left":
810
+ case "pageDown":
811
+ case "pageUp":
812
+ case "parent":
813
+ case "right":
814
+ case "up":
815
+ return node.navigate(cmd);
816
+ default:
817
+ util.error(`Unhandled command: '${cmd}'`);
818
+ }
819
+ }
820
+
821
+ /** Delete all nodes. */
822
+ clear() {
823
+ this.root.removeChildren();
824
+ this.root.children = null;
825
+ this.keyMap.clear();
826
+ this.refKeyMap.clear();
827
+ this.viewNodes.clear();
828
+ this.activeNode = null;
829
+ this.focusNode = null;
830
+
831
+ // this.types = {};
832
+ // this. columns =[];
833
+ // this._columnsById = {};
834
+
835
+ // Modification Status
836
+ this.changedSince = 0;
837
+ this.changes.clear();
838
+ this.changedNodes.clear();
839
+
840
+ // // --- FILTER ---
841
+ // public filterMode: FilterModeType = null;
842
+
843
+ // // --- KEYNAV ---
844
+ // public activeColIdx = 0;
845
+ // public cellNavMode = false;
846
+ // public lastQuicksearchTime = 0;
847
+ // public lastQuicksearchTerm = "";
848
+ this.updateViewport();
849
+ }
850
+
851
+ /**
852
+ * Clear nodes and markup and detach events and observers.
853
+ *
854
+ * This method may be useful to free up resources before re-creating a tree
855
+ * on an existing div, for example in unittest suites.
856
+ * Note that this Wunderbaum instance becomes unusable afterwards.
857
+ */
858
+ public destroy() {
859
+ this.logInfo("destroy()...");
860
+ this.clear();
861
+ this.resizeObserver.disconnect();
862
+ this.element.innerHTML = "";
863
+ // Remove all event handlers
864
+ this.element.outerHTML = this.element.outerHTML;
865
+ }
866
+
867
+ /**
868
+ * Return `tree.option.NAME` (also resolving if this is a callback).
869
+ *
870
+ * See also [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
871
+ * `tree.types[node.type].NAME`.
872
+ *
873
+ * @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
874
+ */
875
+ getOption(name: string, defaultValue?: any): any {
876
+ let ext;
877
+ let opts = this.options as any;
878
+
879
+ // Lookup `name` in options dict
880
+ if (name.indexOf(".") >= 0) {
881
+ [ext, name] = name.split(".");
882
+ opts = opts[ext];
883
+ }
884
+ let value = opts[name];
885
+
886
+ // A callback resolver always takes precedence
887
+ if (typeof value === "function") {
888
+ value = value({ type: "resolve", tree: this });
889
+ }
890
+ // Use value from value options dict, fallback do default
891
+ return value ?? defaultValue;
892
+ }
893
+
894
+ /**
895
+ *
896
+ * @param name
897
+ * @param value
898
+ */
899
+ setOption(name: string, value: any): void {
900
+ if (name.indexOf(".") === -1) {
901
+ (this.options as any)[name] = value;
902
+ // switch (name) {
903
+ // case value:
904
+ // break;
905
+ // default:
906
+ // break;
907
+ // }
908
+ return;
909
+ }
910
+ const parts = name.split(".");
911
+ const ext = this.extensions[parts[0]];
912
+ ext!.setPluginOption(parts[1], value);
913
+ }
914
+
915
+ /**Return true if the tree (or one of its nodes) has the input focus. */
916
+ hasFocus() {
917
+ return this.element.contains(document.activeElement);
918
+ }
919
+
920
+ /** Run code, but defer `updateViewport()` until done. */
921
+ runWithoutUpdate(func: () => any, hint = null): void {
922
+ // const prev = this._disableUpdate;
923
+ // const start = Date.now();
924
+ // this._disableUpdate = Date.now();
925
+ try {
926
+ this.enableUpdate(false);
927
+ return func();
928
+ } finally {
929
+ this.enableUpdate(true);
930
+ // if (!prev && this._disableUpdate === start) {
931
+ // this._disableUpdate = 0;
932
+ // }
933
+ }
934
+ }
935
+
936
+ /** Recursively expand all expandable nodes (triggers lazy load id needed). */
937
+ async expandAll(flag: boolean = true) {
938
+ const tag = this.logTime("expandAll(" + flag + ")");
939
+ try {
940
+ this.enableUpdate(false);
941
+ await this.root.expandAll(flag);
942
+ } finally {
943
+ this.enableUpdate(true);
944
+ this.logTimeEnd(tag);
945
+ }
946
+ }
947
+
948
+ /** Return the number of nodes in the data model.*/
949
+ count(visible = false) {
950
+ if (visible) {
951
+ return this.viewNodes.size;
952
+ }
953
+ return this.keyMap.size;
954
+ }
955
+
956
+ /* Internal sanity check. */
957
+ _check() {
958
+ let i = 0;
959
+ this.visit((n) => {
960
+ i++;
961
+ });
962
+ if (this.keyMap.size !== i) {
963
+ this.logWarn(`_check failed: ${this.keyMap.size} !== ${i}`);
964
+ }
965
+ // util.assert(this.keyMap.size === i);
966
+ }
967
+
968
+ /**Find all nodes that matches condition.
969
+ *
970
+ * @param match title string to search for, or a
971
+ * callback function that returns `true` if a node is matched.
972
+ * @see [[WunderbaumNode.findAll]]
973
+ */
974
+ findAll(match: string | MatcherType) {
975
+ return this.root.findAll(match);
976
+ }
977
+
978
+ /**Find first node that matches condition.
979
+ *
980
+ * @param match title string to search for, or a
981
+ * callback function that returns `true` if a node is matched.
982
+ * @see [[WunderbaumNode.findFirst]]
983
+ */
984
+ findFirst(match: string | MatcherType) {
985
+ return this.root.findFirst(match);
986
+ }
987
+
988
+ /** Find the next visible node that starts with `match`, starting at `startNode`
989
+ * and wrap-around at the end.
990
+ */
991
+ findNextNode(
992
+ match: string | MatcherType,
993
+ startNode?: WunderbaumNode | null
994
+ ): WunderbaumNode | null {
995
+ //, visibleOnly) {
996
+ let res: WunderbaumNode | null = null,
997
+ firstNode = this.getFirstChild()!;
998
+
999
+ let matcher =
1000
+ typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
1001
+ startNode = startNode || firstNode;
1002
+
1003
+ function _checkNode(n: WunderbaumNode) {
1004
+ // console.log("_check " + n)
1005
+ if (matcher(n)) {
1006
+ res = n;
1007
+ }
1008
+ if (res || n === startNode) {
1009
+ return false;
1010
+ }
1011
+ }
1012
+ this.visitRows(_checkNode, {
1013
+ start: startNode,
1014
+ includeSelf: false,
1015
+ });
1016
+ // Wrap around search
1017
+ if (!res && startNode !== firstNode) {
1018
+ this.visitRows(_checkNode, {
1019
+ start: firstNode,
1020
+ includeSelf: true,
1021
+ });
1022
+ }
1023
+ return res;
1024
+ }
1025
+
1026
+ /** Find a node relative to another node.
1027
+ *
1028
+ * @param node
1029
+ * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
1030
+ * (Alternatively the keyCode that would normally trigger this move,
1031
+ * e.g. `$.ui.keyCode.LEFT` = 'left'.
1032
+ * @param includeHidden Not yet implemented
1033
+ */
1034
+ findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) {
1035
+ let res = null;
1036
+ let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
1037
+
1038
+ switch (where) {
1039
+ case "parent":
1040
+ if (node.parent && node.parent.parent) {
1041
+ res = node.parent;
1042
+ }
1043
+ break;
1044
+ case "first":
1045
+ // First visible node
1046
+ this.visit(function (n) {
1047
+ if (n.isVisible()) {
1048
+ res = n;
1049
+ return false;
1050
+ }
1051
+ });
1052
+ break;
1053
+ case "last":
1054
+ this.visit(function (n) {
1055
+ // last visible node
1056
+ if (n.isVisible()) {
1057
+ res = n;
1058
+ }
1059
+ });
1060
+ break;
1061
+ case "left":
1062
+ if (node.parent && node.parent.parent) {
1063
+ res = node.parent;
1064
+ }
1065
+ // if (node.expanded) {
1066
+ // node.setExpanded(false);
1067
+ // } else if (node.parent && node.parent.parent) {
1068
+ // res = node.parent;
1069
+ // }
1070
+ break;
1071
+ case "right":
1072
+ if (node.children && node.children.length) {
1073
+ res = node.children[0];
1074
+ }
1075
+ // if (this.cellNavMode) {
1076
+ // throw new Error("Not implemented");
1077
+ // } else {
1078
+ // if (!node.expanded && (node.children || node.lazy)) {
1079
+ // node.setExpanded();
1080
+ // res = node;
1081
+ // } else if (node.children && node.children.length) {
1082
+ // res = node.children[0];
1083
+ // }
1084
+ // }
1085
+ break;
1086
+ case "up":
1087
+ res = this._getPrevNodeInView(node);
1088
+ break;
1089
+ case "down":
1090
+ res = this._getNextNodeInView(node);
1091
+ break;
1092
+ case "pageDown":
1093
+ let bottomNode = this._lastNodeInView();
1094
+ // this.logDebug(where, this.focusNode, bottomNode);
1095
+
1096
+ if (this.focusNode !== bottomNode) {
1097
+ res = bottomNode;
1098
+ } else {
1099
+ res = this._getNextNodeInView(node, pageSize);
1100
+ }
1101
+ break;
1102
+ case "pageUp":
1103
+ if (this.focusNode && this.focusNode._rowIdx === 0) {
1104
+ res = this.focusNode;
1105
+ } else {
1106
+ let topNode = this._firstNodeInView();
1107
+ if (this.focusNode !== topNode) {
1108
+ res = topNode;
1109
+ } else {
1110
+ res = this._getPrevNodeInView(node, pageSize);
1111
+ }
1112
+ }
1113
+ break;
1114
+ default:
1115
+ this.logWarn("Unknown relation '" + where + "'.");
1116
+ }
1117
+ return res;
1118
+ }
1119
+
1120
+ /**
1121
+ * Return the active cell of the currently active node or null.
1122
+ */
1123
+ getActiveColElem() {
1124
+ if (this.activeNode && this.activeColIdx >= 0) {
1125
+ return this.activeNode.getColElem(this.activeColIdx);
1126
+ }
1127
+ return null;
1128
+ }
1129
+
1130
+ /**
1131
+ * Return the currently active node or null.
1132
+ */
1133
+ getActiveNode() {
1134
+ return this.activeNode;
1135
+ }
1136
+
1137
+ /** Return the first top level node if any (not the invisible root node).
1138
+ * @returns {FancytreeNode | null}
1139
+ */
1140
+ getFirstChild() {
1141
+ return this.root.getFirstChild();
1142
+ }
1143
+
1144
+ /**
1145
+ * Return the currently active node or null.
1146
+ */
1147
+ getFocusNode() {
1148
+ return this.focusNode;
1149
+ }
1150
+
1151
+ /** Return a {node: FancytreeNode, region: TYPE} object for a mouse event.
1152
+ *
1153
+ * @param {Event} event Mouse event, e.g. click, ...
1154
+ * @returns {object} Return a {node: FancytreeNode, region: TYPE} object
1155
+ * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
1156
+ */
1157
+ static getEventInfo(event: Event) {
1158
+ let target = <Element>event.target,
1159
+ cl = target.classList,
1160
+ parentCol = target.closest(".wb-col"),
1161
+ node = Wunderbaum.getNode(target),
1162
+ res = {
1163
+ node: node,
1164
+ region: NodeRegion.unknown,
1165
+ colDef: undefined,
1166
+ colIdx: -1,
1167
+ colId: undefined,
1168
+ colElem: parentCol,
1169
+ };
1170
+
1171
+ if (cl.contains("wb-title")) {
1172
+ res.region = NodeRegion.title;
1173
+ } else if (cl.contains("wb-expander")) {
1174
+ res.region =
1175
+ node!.hasChildren() === false ? NodeRegion.prefix : NodeRegion.expander;
1176
+ } else if (cl.contains("wb-checkbox")) {
1177
+ res.region = NodeRegion.checkbox;
1178
+ } else if (cl.contains("wb-icon")) {
1179
+ //|| cl.contains("wb-custom-icon")) {
1180
+ res.region = NodeRegion.icon;
1181
+ } else if (cl.contains("wb-node")) {
1182
+ res.region = NodeRegion.title;
1183
+ } else if (parentCol) {
1184
+ res.region = NodeRegion.column;
1185
+ const idx = Array.prototype.indexOf.call(
1186
+ parentCol.parentNode!.children,
1187
+ parentCol
1188
+ );
1189
+ res.colIdx = idx;
1190
+ } else {
1191
+ // Somewhere near the title
1192
+ console.warn("getEventInfo(): not found", event, res);
1193
+ return res;
1194
+ }
1195
+ if (res.colIdx === -1) {
1196
+ res.colIdx = 0;
1197
+ }
1198
+ res.colDef = node!.tree.columns[res.colIdx];
1199
+ res.colDef != null ? (res.colId = (<any>res.colDef).id) : 0;
1200
+ // this.log("Event", event, res);
1201
+ return res;
1202
+ }
1203
+
1204
+ // /** Return a string describing the affected node region for a mouse event.
1205
+ // *
1206
+ // * @param {Event} event Mouse event, e.g. click, mousemove, ...
1207
+ // * @returns {string} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
1208
+ // */
1209
+ // getEventNodeRegion(event: Event) {
1210
+ // return this.getEventInfo(event).region;
1211
+ // }
1212
+
1213
+ /**
1214
+ * Return readable string representation for this instance.
1215
+ * @internal
1216
+ */
1217
+ toString() {
1218
+ return "Wunderbaum<'" + this.id + "'>";
1219
+ }
1220
+
1221
+ /** Return true if any node is currently in edit-title mode. */
1222
+ isEditing(): boolean {
1223
+ return this._callMethod("edit.isEditingTitle");
1224
+ }
1225
+
1226
+ /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
1227
+ */
1228
+ isLoading(): boolean {
1229
+ var res = false;
1230
+
1231
+ this.root.visit((n) => {
1232
+ // also visit rootNode
1233
+ if (n._isLoading || n._requestId) {
1234
+ res = true;
1235
+ return false;
1236
+ }
1237
+ }, true);
1238
+ return res;
1239
+ }
1240
+
1241
+ /** Alias for `logDebug` */
1242
+ log = this.logDebug; // Alias
1243
+
1244
+ /** Log to console if opts.debugLevel >= 4 */
1245
+ logDebug(...args: any[]) {
1246
+ if (this.options.debugLevel >= 4) {
1247
+ Array.prototype.unshift.call(args, this.toString());
1248
+ console.log.apply(console, args);
1249
+ }
1250
+ }
1251
+
1252
+ /** Log error to console. */
1253
+ logError(...args: any[]) {
1254
+ if (this.options.debugLevel >= 1) {
1255
+ Array.prototype.unshift.call(args, this.toString());
1256
+ console.error.apply(console, args);
1257
+ }
1258
+ }
1259
+
1260
+ /* Log to console if opts.debugLevel >= 3 */
1261
+ logInfo(...args: any[]) {
1262
+ if (this.options.debugLevel >= 3) {
1263
+ Array.prototype.unshift.call(args, this.toString());
1264
+ console.info.apply(console, args);
1265
+ }
1266
+ }
1267
+
1268
+ /** @internal */
1269
+ logTime(label: string): string {
1270
+ if (this.options.debugLevel >= 4) {
1271
+ console.time(this + ": " + label);
1272
+ }
1273
+ return label;
1274
+ }
1275
+
1276
+ /** @internal */
1277
+ logTimeEnd(label: string): void {
1278
+ if (this.options.debugLevel >= 4) {
1279
+ console.timeEnd(this + ": " + label);
1280
+ }
1281
+ }
1282
+
1283
+ /** Log to console if opts.debugLevel >= 2 */
1284
+ logWarn(...args: any[]) {
1285
+ if (this.options.debugLevel >= 2) {
1286
+ Array.prototype.unshift.call(args, this.toString());
1287
+ console.warn.apply(console, args);
1288
+ }
1289
+ }
1290
+
1291
+ /** */
1292
+ protected render(opts?: any): boolean {
1293
+ const label = this.logTime("render");
1294
+ let idx = 0;
1295
+ let top = 0;
1296
+ const height = ROW_HEIGHT;
1297
+ let modified = false;
1298
+ let start = opts?.startIdx;
1299
+ let end = opts?.endIdx;
1300
+ const obsoleteViewNodes = this.viewNodes;
1301
+
1302
+ this.viewNodes = new Set();
1303
+ let viewNodes = this.viewNodes;
1304
+ // this.debug("render", opts);
1305
+ util.assert(start != null && end != null);
1306
+
1307
+ // Make sure start is always even, so the alternating row colors don't
1308
+ // change when scrolling:
1309
+ if (start % 2) {
1310
+ start--;
1311
+ }
1312
+
1313
+ this.visitRows(function (node) {
1314
+ const prevIdx = node._rowIdx;
1315
+
1316
+ viewNodes.add(node);
1317
+ obsoleteViewNodes.delete(node);
1318
+ if (prevIdx !== idx) {
1319
+ node._rowIdx = idx;
1320
+ modified = true;
1321
+ }
1322
+ if (idx < start || idx > end) {
1323
+ node._callEvent("discard");
1324
+ node.removeMarkup();
1325
+ } else {
1326
+ // if (!node._rowElem || prevIdx != idx) {
1327
+ node.render({ top: top });
1328
+ }
1329
+ idx++;
1330
+ top += height;
1331
+ });
1332
+ for (const prevNode of obsoleteViewNodes) {
1333
+ prevNode._callEvent("discard");
1334
+ prevNode.removeMarkup();
1335
+ }
1336
+ // Resize tree container
1337
+ this.nodeListElement.style.height = "" + top + "px";
1338
+ // this.log("render()", this.nodeListElement.style.height);
1339
+ this.logTimeEnd(label);
1340
+ return modified;
1341
+ }
1342
+
1343
+ /**Recalc and apply header columns from `this.columns`. */
1344
+ renderHeader() {
1345
+ if (!this.headerElement) {
1346
+ return;
1347
+ }
1348
+ const headerRow = this.headerElement.querySelector(".wb-row")!;
1349
+ util.assert(headerRow);
1350
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(
1351
+ this.columns.length
1352
+ );
1353
+
1354
+ for (let i = 0; i < this.columns.length; i++) {
1355
+ let col = this.columns[i];
1356
+ let colElem = <HTMLElement>headerRow.children[i];
1357
+ colElem.style.left = col._ofsPx + "px";
1358
+ colElem.style.width = col._widthPx + "px";
1359
+ colElem.textContent = col.title || col.id;
1360
+ }
1361
+ }
1362
+
1363
+ /**
1364
+ *
1365
+ * @param {boolean | PlainObject} [effects=false] animation options.
1366
+ * @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
1367
+ * this node will remain visible in
1368
+ * any case, even if `this` is outside the scroll pane.
1369
+ * Make sure that a node is scrolled into the viewport.
1370
+ */
1371
+ scrollTo(opts: any) {
1372
+ const MARGIN = 1;
1373
+ const node = opts.node || this.getActiveNode();
1374
+ util.assert(node._rowIdx != null);
1375
+ const curTop = this.scrollContainer.scrollTop;
1376
+ const height = this.scrollContainer.clientHeight;
1377
+ const nodeOfs = node._rowIdx * ROW_HEIGHT;
1378
+ let newTop;
1379
+
1380
+ if (nodeOfs > curTop) {
1381
+ if (nodeOfs + ROW_HEIGHT < curTop + height) {
1382
+ // Already in view
1383
+ } else {
1384
+ // Node is below viewport
1385
+ newTop = nodeOfs - height + ROW_HEIGHT - MARGIN;
1386
+ }
1387
+ } else if (nodeOfs < curTop) {
1388
+ // Node is above viewport
1389
+ newTop = nodeOfs + MARGIN;
1390
+ }
1391
+ this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
1392
+ if (newTop != null) {
1393
+ this.scrollContainer.scrollTop = newTop;
1394
+ this.updateViewport();
1395
+ }
1396
+ }
1397
+
1398
+ /** */
1399
+ setCellMode(mode: NavigationMode) {
1400
+ // util.assert(this.cellNavMode);
1401
+ // util.assert(0 <= colIdx && colIdx < this.columns.length);
1402
+ if (mode === this.navMode) {
1403
+ return;
1404
+ }
1405
+ const prevMode = this.navMode;
1406
+ const cellMode = mode !== NavigationMode.row;
1407
+
1408
+ this.navMode = mode;
1409
+ if (cellMode && prevMode === NavigationMode.row) {
1410
+ this.setColumn(0);
1411
+ }
1412
+ this.element.classList.toggle("wb-cell-mode", cellMode);
1413
+ this.element.classList.toggle(
1414
+ "wb-cell-edit-mode",
1415
+ mode === NavigationMode.cellEdit
1416
+ );
1417
+ this.setModified(ChangeType.row, this.activeNode);
1418
+ }
1419
+
1420
+ /** */
1421
+ setColumn(colIdx: number) {
1422
+ util.assert(this.navMode !== NavigationMode.row);
1423
+ util.assert(0 <= colIdx && colIdx < this.columns.length);
1424
+ this.activeColIdx = colIdx;
1425
+ // node.setActive(true, { column: tree.activeColIdx + 1 });
1426
+ this.setModified(ChangeType.row, this.activeNode);
1427
+ // Update `wb-active` class for all headers
1428
+ if (this.headerElement) {
1429
+ for (let rowDiv of this.headerElement.children) {
1430
+ // for (let rowDiv of document.querySelector("div.wb-header").children) {
1431
+ let i = 0;
1432
+ for (let colDiv of rowDiv.children) {
1433
+ (colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
1434
+ }
1435
+ }
1436
+ }
1437
+ // Update `wb-active` class for all cell divs
1438
+ for (let rowDiv of this.nodeListElement.children) {
1439
+ let i = 0;
1440
+ for (let colDiv of rowDiv.children) {
1441
+ (colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
1442
+ }
1443
+ }
1444
+ }
1445
+
1446
+ /** */
1447
+ setFocus(flag = true) {
1448
+ if (flag) {
1449
+ this.element.focus();
1450
+ } else {
1451
+ this.element.blur();
1452
+ }
1453
+ }
1454
+
1455
+ /** */
1456
+ setModified(change: ChangeType, options?: any): void;
1457
+
1458
+ /** */
1459
+ setModified(
1460
+ change: ChangeType,
1461
+ node?: WunderbaumNode | any,
1462
+ options?: any
1463
+ ): void {
1464
+ if (!(node instanceof WunderbaumNode)) {
1465
+ options = node;
1466
+ }
1467
+ if (!this.changedSince) {
1468
+ this.changedSince = Date.now();
1469
+ }
1470
+ this.changes.add(change);
1471
+ if (change === ChangeType.structure) {
1472
+ this.changedNodes.clear();
1473
+ } else if (node && !this.changes.has(ChangeType.structure)) {
1474
+ if (this.changedNodes.size < MAX_CHANGED_NODES) {
1475
+ this.changedNodes.add(node);
1476
+ } else {
1477
+ this.changes.add(ChangeType.structure);
1478
+ this.changedNodes.clear();
1479
+ }
1480
+ }
1481
+ // this.log("setModified(" + change + ")", node);
1482
+ }
1483
+
1484
+ setStatus(
1485
+ status: NodeStatusType,
1486
+ message?: string,
1487
+ details?: string
1488
+ ): WunderbaumNode | null {
1489
+ return this.root.setStatus(status, message, details);
1490
+ }
1491
+
1492
+ /** Update column headers and width. */
1493
+ updateColumns(opts: any) {
1494
+ let modified = false;
1495
+ let minWidth = 4;
1496
+ let vpWidth = this.element.clientWidth;
1497
+ let totalWeight = 0;
1498
+ let fixedWidth = 0;
1499
+
1500
+ // Gather width requests
1501
+ this._columnsById = {};
1502
+ for (let col of this.columns) {
1503
+ this._columnsById[<string>col.id] = col;
1504
+ let cw = col.width;
1505
+
1506
+ if (!cw || cw === "*") {
1507
+ col._weight = 1.0;
1508
+ totalWeight += 1.0;
1509
+ } else if (typeof cw === "number") {
1510
+ col._weight = cw;
1511
+ totalWeight += cw;
1512
+ } else if (typeof cw === "string" && cw.endsWith("px")) {
1513
+ col._weight = 0;
1514
+ let px = parseFloat(cw.slice(0, -2));
1515
+ if (col._widthPx != px) {
1516
+ modified = true;
1517
+ col._widthPx = px;
1518
+ }
1519
+ fixedWidth += px;
1520
+ } else {
1521
+ util.error("Invalid column width: " + cw);
1522
+ }
1523
+ }
1524
+ // Share remaining space between non-fixed columns
1525
+ let restPx = Math.max(0, vpWidth - fixedWidth);
1526
+ let ofsPx = 0;
1527
+
1528
+ for (let col of this.columns) {
1529
+ if (col._weight) {
1530
+ let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
1531
+ if (col._widthPx != px) {
1532
+ modified = true;
1533
+ col._widthPx = px;
1534
+ }
1535
+ }
1536
+ col._ofsPx = ofsPx;
1537
+ ofsPx += col._widthPx;
1538
+ }
1539
+ // Every column has now a calculated `_ofsPx` and `_widthPx`
1540
+ // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
1541
+ // console.trace();
1542
+ // util.error("BREAK");
1543
+ if (modified) {
1544
+ this.renderHeader();
1545
+ if (opts.render !== false) {
1546
+ this.render();
1547
+ }
1548
+ }
1549
+ }
1550
+
1551
+ /** Render all rows that are visible in the viewport. */
1552
+ updateViewport(immediate = false) {
1553
+ // Call the `throttle` wrapper for `this._updateViewport()` which will
1554
+ // execute immediately on the leading edge of a sequence:
1555
+ this._updateViewportThrottled();
1556
+ if (immediate) {
1557
+ this._updateViewportThrottled.flush();
1558
+ }
1559
+ }
1560
+
1561
+ protected _updateViewport() {
1562
+ if (this._disableUpdate) {
1563
+ return;
1564
+ }
1565
+ let height = this.scrollContainer.clientHeight;
1566
+ // We cannot get the height for abolut positioned parent, so look at first col
1567
+ // let headerHeight = this.headerElement.clientHeight
1568
+ // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
1569
+ const headerHeight = this.options.headerHeightPx;
1570
+ let wantHeight = this.element.clientHeight - headerHeight;
1571
+ let ofs = this.scrollContainer.scrollTop;
1572
+
1573
+ if (Math.abs(height - wantHeight) > 1.0) {
1574
+ // this.log("resize", height, wantHeight);
1575
+ this.scrollContainer.style.height = wantHeight + "px";
1576
+ height = wantHeight;
1577
+ }
1578
+
1579
+ this.updateColumns({ render: false });
1580
+ this.render({
1581
+ startIdx: Math.max(0, ofs / ROW_HEIGHT - RENDER_MAX_PREFETCH),
1582
+ endIdx: Math.max(0, (ofs + height) / ROW_HEIGHT + RENDER_MAX_PREFETCH),
1583
+ });
1584
+ this._callEvent("update");
1585
+ }
1586
+
1587
+ /** Call callback(node) for all nodes in hierarchical order (depth-first).
1588
+ *
1589
+ * @param {function} callback the callback function.
1590
+ * Return false to stop iteration, return "skip" to skip this node and children only.
1591
+ * @returns {boolean} false, if the iterator was stopped.
1592
+ */
1593
+ visit(callback: (node: WunderbaumNode) => any) {
1594
+ return this.root.visit(callback, false);
1595
+ }
1596
+
1597
+ /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
1598
+ * Stop iteration, if fn() returns false.<br>
1599
+ * Return false if iteration was stopped.
1600
+ *
1601
+ * @param callback the callback function.
1602
+ * Return false to stop iteration, return "skip" to skip this node and children only.
1603
+ * @param [options]
1604
+ * Defaults:
1605
+ * {start: First tree node, reverse: false, includeSelf: true, includeHidden: false, wrap: false}
1606
+ * @returns {boolean} false if iteration was canceled
1607
+ */
1608
+ visitRows(callback: (node: WunderbaumNode) => any, opts?: any): boolean {
1609
+ if (!this.root.hasChildren()) {
1610
+ return false;
1611
+ }
1612
+ if (opts && opts.reverse) {
1613
+ delete opts.reverse;
1614
+ return this._visitRowsUp(callback, opts);
1615
+ }
1616
+ opts = opts || {};
1617
+ let i,
1618
+ nextIdx,
1619
+ parent,
1620
+ res,
1621
+ siblings,
1622
+ stopNode: WunderbaumNode,
1623
+ siblingOfs = 0,
1624
+ skipFirstNode = opts.includeSelf === false,
1625
+ includeHidden = !!opts.includeHidden,
1626
+ checkFilter = !includeHidden && this.filterMode === "hide",
1627
+ node: WunderbaumNode = opts.start || this.root.children![0];
1628
+
1629
+ parent = node.parent;
1630
+ while (parent) {
1631
+ // visit siblings
1632
+ siblings = parent.children!;
1633
+ nextIdx = siblings.indexOf(node) + siblingOfs;
1634
+ util.assert(
1635
+ nextIdx >= 0,
1636
+ "Could not find " + node + " in parent's children: " + parent
1637
+ );
1638
+
1639
+ for (i = nextIdx; i < siblings.length; i++) {
1640
+ node = siblings[i];
1641
+ if (node === stopNode!) {
1642
+ return false;
1643
+ }
1644
+ if (
1645
+ checkFilter &&
1646
+ !node.statusNodeType &&
1647
+ !node.match &&
1648
+ !node.subMatchCount
1649
+ ) {
1650
+ continue;
1651
+ }
1652
+ if (!skipFirstNode && callback(node) === false) {
1653
+ return false;
1654
+ }
1655
+ skipFirstNode = false;
1656
+ // Dive into node's child nodes
1657
+ if (
1658
+ node.children &&
1659
+ node.children.length &&
1660
+ (includeHidden || node.expanded)
1661
+ ) {
1662
+ res = node.visit(function (n: WunderbaumNode) {
1663
+ if (n === stopNode) {
1664
+ return false;
1665
+ }
1666
+ if (checkFilter && !n.match && !n.subMatchCount) {
1667
+ return "skip";
1668
+ }
1669
+ if (callback(n) === false) {
1670
+ return false;
1671
+ }
1672
+ if (!includeHidden && n.children && !n.expanded) {
1673
+ return "skip";
1674
+ }
1675
+ }, false);
1676
+ if (res === false) {
1677
+ return false;
1678
+ }
1679
+ }
1680
+ }
1681
+ // Visit parent nodes (bottom up)
1682
+ node = parent;
1683
+ parent = parent.parent;
1684
+ siblingOfs = 1; //
1685
+
1686
+ if (!parent && opts.wrap) {
1687
+ this.logDebug("visitRows(): wrap around");
1688
+ util.assert(opts.start, "`wrap` option requires `start`");
1689
+ stopNode = opts.start;
1690
+ opts.wrap = false;
1691
+ parent = this.root;
1692
+ siblingOfs = 0;
1693
+ }
1694
+ }
1695
+ return true;
1696
+ }
1697
+
1698
+ /** Call fn(node) for all nodes in vertical order, bottom up.
1699
+ * @internal
1700
+ */
1701
+ protected _visitRowsUp(
1702
+ callback: (node: WunderbaumNode) => any,
1703
+ opts: any
1704
+ ): boolean {
1705
+ let children,
1706
+ idx,
1707
+ parent,
1708
+ includeHidden = !!opts.includeHidden,
1709
+ node = opts.start || this.root.children![0];
1710
+
1711
+ if (opts.includeSelf !== false) {
1712
+ if (callback(node) === false) {
1713
+ return false;
1714
+ }
1715
+ }
1716
+ while (true) {
1717
+ parent = node.parent;
1718
+ children = parent.children;
1719
+
1720
+ if (children[0] === node) {
1721
+ // If this is already the first sibling, goto parent
1722
+ node = parent;
1723
+ if (!node.parent) {
1724
+ break; // first node of the tree
1725
+ }
1726
+ children = parent.children;
1727
+ } else {
1728
+ // Otherwise, goto prev. sibling
1729
+ idx = children.indexOf(node);
1730
+ node = children[idx - 1];
1731
+ // If the prev. sibling has children, follow down to last descendant
1732
+ while (
1733
+ (includeHidden || node.expanded) &&
1734
+ node.children &&
1735
+ node.children.length
1736
+ ) {
1737
+ children = node.children;
1738
+ parent = node;
1739
+ node = children[children.length - 1];
1740
+ }
1741
+ }
1742
+ // Skip invisible
1743
+ if (!includeHidden && !node.isVisible()) {
1744
+ continue;
1745
+ }
1746
+ if (callback(node) === false) {
1747
+ return false;
1748
+ }
1749
+ }
1750
+ return true;
1751
+ }
1752
+
1753
+ /** . */
1754
+ load(source: any, options: any = {}) {
1755
+ this.clear();
1756
+ const columns = options.columns || source.columns;
1757
+ if (columns) {
1758
+ this.columns = options.columns;
1759
+ this.renderHeader();
1760
+ // this.updateColumns({ render: false });
1761
+ }
1762
+ return this.root.load(source);
1763
+ }
1764
+
1765
+ /**
1766
+ *
1767
+ */
1768
+ public enableUpdate(flag: boolean): void {
1769
+ /*
1770
+ 5 7 9 20 25 30
1771
+ 1 >-------------------------------------<
1772
+ 2 >--------------------<
1773
+ 3 >--------------------------<
1774
+
1775
+ 5
1776
+
1777
+ */
1778
+ // this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
1779
+ if (flag) {
1780
+ util.assert(this._disableUpdateCount > 0);
1781
+ this._disableUpdateCount--;
1782
+ if (this._disableUpdateCount === 0) {
1783
+ this.updateViewport();
1784
+ }
1785
+ } else {
1786
+ this._disableUpdateCount++;
1787
+ // this._disableUpdate = Date.now();
1788
+ }
1789
+ // return !flag; // return previous value
1790
+ }
1791
+
1792
+ /* ---------------------------------------------------------------------------
1793
+ * FILTER
1794
+ * -------------------------------------------------------------------------*/
1795
+ /**
1796
+ * [ext-filter] Reset the filter.
1797
+ *
1798
+ * @requires [[FilterExtension]]
1799
+ */
1800
+ clearFilter() {
1801
+ return (this.extensions.filter as FilterExtension).clearFilter();
1802
+ }
1803
+ /**
1804
+ * [ext-filter] Return true if a filter is currently applied.
1805
+ *
1806
+ * @requires [[FilterExtension]]
1807
+ */
1808
+ isFilterActive() {
1809
+ return !!this.filterMode;
1810
+ }
1811
+ /**
1812
+ * [ext-filter] Re-apply current filter.
1813
+ *
1814
+ * @requires [[FilterExtension]]
1815
+ */
1816
+ updateFilter() {
1817
+ return (this.extensions.filter as FilterExtension).updateFilter();
1818
+ }
1819
+ }