wunderbaum 0.0.1 → 0.0.4
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/README.md +4 -3
- package/dist/wunderbaum.css +1 -1
- package/dist/wunderbaum.d.ts +738 -174
- package/dist/wunderbaum.esm.js +1123 -647
- package/dist/wunderbaum.esm.min.js +30 -20
- package/dist/wunderbaum.esm.min.js.map +1 -1
- package/dist/wunderbaum.umd.js +1125 -649
- package/dist/wunderbaum.umd.min.js +34 -25
- package/dist/wunderbaum.umd.min.js.map +1 -1
- package/package.json +29 -30
- package/src/common.ts +46 -6
- package/src/deferred.ts +12 -2
- package/src/drag_observer.ts +169 -0
- package/src/util.ts +129 -16
- package/src/wb_ext_dnd.ts +144 -3
- package/src/wb_ext_edit.ts +10 -1
- package/src/wb_ext_filter.ts +35 -35
- package/src/wb_ext_grid.ts +45 -0
- package/src/wb_ext_keynav.ts +8 -4
- package/src/wb_extension_base.ts +3 -3
- package/src/wb_node.ts +359 -214
- package/src/wb_options.ts +173 -26
- package/src/wunderbaum.scss +114 -28
- package/src/wunderbaum.ts +504 -338
package/src/wunderbaum.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { FilterExtension } from "./wb_ext_filter";
|
|
|
16
16
|
import { KeynavExtension } from "./wb_ext_keynav";
|
|
17
17
|
import { LoggerExtension } from "./wb_ext_logger";
|
|
18
18
|
import { DndExtension } from "./wb_ext_dnd";
|
|
19
|
+
import { GridExtension } from "./wb_ext_grid";
|
|
19
20
|
import { ExtensionsDict, WunderbaumExtension } from "./wb_extension_base";
|
|
20
21
|
|
|
21
22
|
import {
|
|
@@ -34,13 +35,13 @@ import {
|
|
|
34
35
|
} from "./common";
|
|
35
36
|
import { WunderbaumNode } from "./wb_node";
|
|
36
37
|
import { Deferred } from "./deferred";
|
|
37
|
-
import { DebouncedFunction, throttle } from "./debounce";
|
|
38
|
+
// import { DebouncedFunction, throttle } from "./debounce";
|
|
38
39
|
import { EditExtension } from "./wb_ext_edit";
|
|
39
40
|
import { WunderbaumOptions } from "./wb_options";
|
|
40
41
|
|
|
41
42
|
// const class_prefix = "wb-";
|
|
42
43
|
// const node_props: string[] = ["title", "key", "refKey"];
|
|
43
|
-
const MAX_CHANGED_NODES = 10;
|
|
44
|
+
// const MAX_CHANGED_NODES = 10;
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* A persistent plain object or array.
|
|
@@ -48,71 +49,81 @@ const MAX_CHANGED_NODES = 10;
|
|
|
48
49
|
* See also [[WunderbaumOptions]].
|
|
49
50
|
*/
|
|
50
51
|
export class Wunderbaum {
|
|
51
|
-
static
|
|
52
|
-
static sequence = 0;
|
|
52
|
+
protected static sequence = 0;
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
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>;
|
|
54
|
+
/** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
|
|
55
|
+
public static version: string = "@VERSION"; // Set to semver by 'grunt release'
|
|
64
56
|
|
|
57
|
+
/** The invisible root node, that holds all visible top level nodes. */
|
|
58
|
+
public readonly root: WunderbaumNode;
|
|
59
|
+
/** Unique tree ID as passed to constructor. Defaults to `"wb_SEQUENCE"`. */
|
|
60
|
+
public readonly id: string;
|
|
61
|
+
/** The `div` container element that was passed to the constructor. */
|
|
62
|
+
public readonly element: HTMLDivElement;
|
|
63
|
+
/** The `div.wb-header` element if any. */
|
|
64
|
+
public readonly headerElement: HTMLDivElement | null;
|
|
65
|
+
/** The `div.wb-scroll-container` element that contains the `nodeListElement`. */
|
|
66
|
+
public readonly scrollContainer: HTMLDivElement;
|
|
67
|
+
/** The `div.wb-node-list` element that contains all visible div.wb-row child elements. */
|
|
68
|
+
public readonly nodeListElement: HTMLDivElement;
|
|
69
|
+
|
|
70
|
+
protected readonly _updateViewportThrottled: (...args: any) => void;
|
|
65
71
|
protected extensionList: WunderbaumExtension[] = [];
|
|
66
72
|
protected extensions: ExtensionsDict = {};
|
|
67
|
-
// protected extensionMap = new Map<string, WunderbaumExtension>();
|
|
68
73
|
|
|
69
74
|
/** Merged options from constructor args and tree- and extension defaults. */
|
|
70
75
|
public options: WunderbaumOptions;
|
|
71
76
|
|
|
72
77
|
protected keyMap = new Map<string, WunderbaumNode>();
|
|
73
78
|
protected refKeyMap = new Map<string, Set<WunderbaumNode>>();
|
|
74
|
-
protected viewNodes = new Set<WunderbaumNode>();
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
// protected viewNodes = new Set<WunderbaumNode>();
|
|
80
|
+
protected treeRowCount = 0;
|
|
81
|
+
protected _disableUpdateCount = 0;
|
|
77
82
|
|
|
78
83
|
// protected eventHandlers : Array<function> = [];
|
|
79
84
|
|
|
85
|
+
/** Currently active node if any. */
|
|
80
86
|
public activeNode: WunderbaumNode | null = null;
|
|
87
|
+
/** Current node hat has keyboard focus if any. */
|
|
81
88
|
public focusNode: WunderbaumNode | null = null;
|
|
82
|
-
_disableUpdate = 0;
|
|
83
|
-
_disableUpdateCount = 0;
|
|
84
89
|
|
|
85
90
|
/** Shared properties, referenced by `node.type`. */
|
|
86
91
|
public types: { [key: string]: any } = {};
|
|
87
92
|
/** List of column definitions. */
|
|
88
93
|
public columns: any[] = [];
|
|
89
|
-
public _columnsById: { [key: string]: any } = {};
|
|
90
94
|
|
|
91
|
-
protected
|
|
95
|
+
protected _columnsById: { [key: string]: any } = {};
|
|
96
|
+
protected resizeObserver: ResizeObserver;
|
|
92
97
|
|
|
93
98
|
// Modification Status
|
|
94
|
-
protected changedSince = 0;
|
|
95
|
-
protected changes = new Set<ChangeType>();
|
|
96
|
-
protected changedNodes = new Set<WunderbaumNode>();
|
|
99
|
+
// protected changedSince = 0;
|
|
100
|
+
// protected changes = new Set<ChangeType>();
|
|
101
|
+
// protected changedNodes = new Set<WunderbaumNode>();
|
|
102
|
+
protected changeRedrawRequestPending = false;
|
|
103
|
+
|
|
104
|
+
/** A Promise that is resolved when the tree was initialized (similar to `init(e)` event). */
|
|
105
|
+
public readonly ready: Promise<any>;
|
|
106
|
+
/** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
|
|
107
|
+
public static util = util;
|
|
108
|
+
/** Expose some useful methods of the util.ts module as `tree._util`. */
|
|
109
|
+
public _util = util;
|
|
97
110
|
|
|
98
111
|
// --- FILTER ---
|
|
99
112
|
public filterMode: FilterModeType = null;
|
|
100
113
|
|
|
101
114
|
// --- KEYNAV ---
|
|
115
|
+
/** @internal Use `setColumn()`/`getActiveColElem()`*/
|
|
102
116
|
public activeColIdx = 0;
|
|
117
|
+
/** @internal */
|
|
103
118
|
public navMode = NavigationMode.row;
|
|
119
|
+
/** @internal */
|
|
104
120
|
public lastQuicksearchTime = 0;
|
|
121
|
+
/** @internal */
|
|
105
122
|
public lastQuicksearchTerm = "";
|
|
106
123
|
|
|
107
124
|
// --- EDIT ---
|
|
108
125
|
protected lastClickTime = 0;
|
|
109
126
|
|
|
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
127
|
constructor(options: WunderbaumOptions) {
|
|
117
128
|
let opts = (this.options = util.extend(
|
|
118
129
|
{
|
|
@@ -125,7 +136,7 @@ export class Wunderbaum {
|
|
|
125
136
|
rowHeightPx: ROW_HEIGHT,
|
|
126
137
|
columns: null,
|
|
127
138
|
types: null,
|
|
128
|
-
escapeTitles: true,
|
|
139
|
+
// escapeTitles: true,
|
|
129
140
|
showSpinner: false,
|
|
130
141
|
checkbox: true,
|
|
131
142
|
minExpandLevel: 0,
|
|
@@ -185,6 +196,7 @@ export class Wunderbaum {
|
|
|
185
196
|
this._registerExtension(new EditExtension(this));
|
|
186
197
|
this._registerExtension(new FilterExtension(this));
|
|
187
198
|
this._registerExtension(new DndExtension(this));
|
|
199
|
+
this._registerExtension(new GridExtension(this));
|
|
188
200
|
this._registerExtension(new LoggerExtension(this));
|
|
189
201
|
|
|
190
202
|
// --- Evaluate options
|
|
@@ -215,12 +227,9 @@ export class Wunderbaum {
|
|
|
215
227
|
this.navMode = NavigationMode.cellNav;
|
|
216
228
|
}
|
|
217
229
|
|
|
218
|
-
this._updateViewportThrottled =
|
|
219
|
-
()
|
|
220
|
-
|
|
221
|
-
},
|
|
222
|
-
opts.updateThrottleWait,
|
|
223
|
-
{ leading: true, trailing: true }
|
|
230
|
+
this._updateViewportThrottled = util.addaptiveThrottle(
|
|
231
|
+
this._updateViewport.bind(this),
|
|
232
|
+
{}
|
|
224
233
|
);
|
|
225
234
|
|
|
226
235
|
// --- Create Markup
|
|
@@ -313,10 +322,8 @@ export class Wunderbaum {
|
|
|
313
322
|
.finally(() => {
|
|
314
323
|
this.element.querySelector("progress.spinner")?.remove();
|
|
315
324
|
this.element.classList.remove("wb-initializing");
|
|
316
|
-
// this.updateViewport();
|
|
317
325
|
});
|
|
318
326
|
} else {
|
|
319
|
-
// this.updateViewport();
|
|
320
327
|
readyDeferred.resolve();
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -328,21 +335,19 @@ export class Wunderbaum {
|
|
|
328
335
|
|
|
329
336
|
// --- Bind listeners
|
|
330
337
|
this.scrollContainer.addEventListener("scroll", (e: Event) => {
|
|
331
|
-
this.
|
|
338
|
+
this.setModified(ChangeType.vscroll);
|
|
332
339
|
});
|
|
333
340
|
|
|
334
|
-
// window.addEventListener("resize", (e: Event) => {
|
|
335
|
-
// this.updateViewport();
|
|
336
|
-
// });
|
|
337
341
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
338
|
-
this.
|
|
339
|
-
|
|
342
|
+
this.setModified(ChangeType.vscroll);
|
|
343
|
+
// this.log("ResizeObserver: Size changed", entries);
|
|
340
344
|
});
|
|
341
345
|
this.resizeObserver.observe(this.element);
|
|
342
346
|
|
|
343
347
|
util.onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
|
|
344
348
|
const info = Wunderbaum.getEventInfo(e);
|
|
345
349
|
const node = info.node;
|
|
350
|
+
// this.log("click", info, e);
|
|
346
351
|
|
|
347
352
|
if (
|
|
348
353
|
this._callEvent("click", { event: e, node: node, info: info }) === false
|
|
@@ -375,8 +380,6 @@ export class Wunderbaum {
|
|
|
375
380
|
node.setSelected(!node.isSelected());
|
|
376
381
|
}
|
|
377
382
|
}
|
|
378
|
-
// if(e.target.classList.)
|
|
379
|
-
// this.log("click", info);
|
|
380
383
|
this.lastClickTime = Date.now();
|
|
381
384
|
});
|
|
382
385
|
|
|
@@ -401,40 +404,19 @@ export class Wunderbaum {
|
|
|
401
404
|
forceClose: true,
|
|
402
405
|
});
|
|
403
406
|
}
|
|
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
407
|
});
|
|
416
408
|
}
|
|
417
409
|
|
|
418
|
-
/**
|
|
419
|
-
|
|
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.
|
|
410
|
+
/**
|
|
411
|
+
* Return a Wunderbaum instance, from element, id, index, or event.
|
|
431
412
|
*
|
|
432
|
-
*
|
|
433
|
-
* getTree();
|
|
434
|
-
* getTree(1);
|
|
435
|
-
* getTree(event);
|
|
436
|
-
* getTree("foo");
|
|
413
|
+
* ```js
|
|
414
|
+
* getTree(); // Get first Wunderbaum instance on page
|
|
415
|
+
* getTree(1); // Get second Wunderbaum instance on page
|
|
416
|
+
* getTree(event); // Get tree for this mouse- or keyboard event
|
|
417
|
+
* getTree("foo"); // Get tree for this `tree.options.id`
|
|
437
418
|
* getTree("#tree"); // Get tree for this matching element
|
|
419
|
+
* ```
|
|
438
420
|
*/
|
|
439
421
|
public static getTree(
|
|
440
422
|
el?: Element | Event | number | string | WunderbaumNode
|
|
@@ -476,9 +458,8 @@ export class Wunderbaum {
|
|
|
476
458
|
return null;
|
|
477
459
|
}
|
|
478
460
|
|
|
479
|
-
/**
|
|
480
|
-
*
|
|
481
|
-
* @param el
|
|
461
|
+
/**
|
|
462
|
+
* Return a WunderbaumNode instance from element or event.
|
|
482
463
|
*/
|
|
483
464
|
public static getNode(el: Element | Event): WunderbaumNode | null {
|
|
484
465
|
if (!el) {
|
|
@@ -499,7 +480,7 @@ export class Wunderbaum {
|
|
|
499
480
|
return null;
|
|
500
481
|
}
|
|
501
482
|
|
|
502
|
-
/** */
|
|
483
|
+
/** @internal */
|
|
503
484
|
protected _registerExtension(extension: WunderbaumExtension): void {
|
|
504
485
|
this.extensionList.push(extension);
|
|
505
486
|
this.extensions[extension.id] = extension;
|
|
@@ -543,7 +524,7 @@ export class Wunderbaum {
|
|
|
543
524
|
(node.tree as any) = null;
|
|
544
525
|
(node.parent as any) = null;
|
|
545
526
|
// node.title = "DISPOSED: " + node.title
|
|
546
|
-
this.viewNodes.delete(node);
|
|
527
|
+
// this.viewNodes.delete(node);
|
|
547
528
|
node.removeMarkup();
|
|
548
529
|
}
|
|
549
530
|
|
|
@@ -568,7 +549,9 @@ export class Wunderbaum {
|
|
|
568
549
|
return res;
|
|
569
550
|
}
|
|
570
551
|
|
|
571
|
-
/**
|
|
552
|
+
/**
|
|
553
|
+
* Call tree method or extension method if defined.
|
|
554
|
+
*
|
|
572
555
|
* Example:
|
|
573
556
|
* ```js
|
|
574
557
|
* tree._callMethod("edit.startEdit", "arg1", "arg2")
|
|
@@ -585,37 +568,34 @@ export class Wunderbaum {
|
|
|
585
568
|
}
|
|
586
569
|
}
|
|
587
570
|
|
|
588
|
-
/**
|
|
571
|
+
/**
|
|
572
|
+
* Call event handler if defined in tree or tree.EXTENSION options.
|
|
573
|
+
*
|
|
589
574
|
* Example:
|
|
590
575
|
* ```js
|
|
591
576
|
* tree._callEvent("edit.beforeEdit", {foo: 42})
|
|
592
577
|
* ```
|
|
593
578
|
*/
|
|
594
|
-
_callEvent(
|
|
595
|
-
const [p, n] =
|
|
579
|
+
_callEvent(type: string, extra?: any): any {
|
|
580
|
+
const [p, n] = type.split(".");
|
|
596
581
|
const opts = this.options as any;
|
|
597
582
|
const func = n ? opts[p][n] : opts[p];
|
|
598
583
|
if (func) {
|
|
599
584
|
return func.call(
|
|
600
585
|
this,
|
|
601
|
-
util.extend({
|
|
586
|
+
util.extend({ type: type, tree: this, util: this._util }, extra)
|
|
602
587
|
);
|
|
603
588
|
// } else {
|
|
604
|
-
// this.logError(`Triggering undefined event '${
|
|
589
|
+
// this.logError(`Triggering undefined event '${type}'.`)
|
|
605
590
|
}
|
|
606
591
|
}
|
|
607
592
|
|
|
608
|
-
/** Return the
|
|
609
|
-
protected
|
|
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
|
-
}
|
|
593
|
+
/** Return the node for given row index. */
|
|
594
|
+
protected _getNodeByRowIdx(idx: number): WunderbaumNode | null {
|
|
616
595
|
// TODO: start searching from active node (reverse)
|
|
596
|
+
let node: WunderbaumNode | null = null;
|
|
617
597
|
this.visitRows((n) => {
|
|
618
|
-
if (n._rowIdx ===
|
|
598
|
+
if (n._rowIdx === idx) {
|
|
619
599
|
node = n;
|
|
620
600
|
return false;
|
|
621
601
|
}
|
|
@@ -623,9 +603,24 @@ export class Wunderbaum {
|
|
|
623
603
|
return <WunderbaumNode>node!;
|
|
624
604
|
}
|
|
625
605
|
|
|
626
|
-
/** Return the
|
|
627
|
-
|
|
628
|
-
let
|
|
606
|
+
/** Return the topmost visible node in the viewport. */
|
|
607
|
+
getTopmostVpNode(complete = true) {
|
|
608
|
+
let topIdx: number;
|
|
609
|
+
const gracePy = 1; // ignore subpixel scrolling
|
|
610
|
+
|
|
611
|
+
if (complete) {
|
|
612
|
+
topIdx = Math.ceil(
|
|
613
|
+
(this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT
|
|
614
|
+
);
|
|
615
|
+
} else {
|
|
616
|
+
topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
|
|
617
|
+
}
|
|
618
|
+
return this._getNodeByRowIdx(topIdx)!;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Return the lowest visible node in the viewport. */
|
|
622
|
+
getLowestVpNode(complete = true) {
|
|
623
|
+
let bottomIdx: number;
|
|
629
624
|
if (complete) {
|
|
630
625
|
bottomIdx =
|
|
631
626
|
Math.floor(
|
|
@@ -639,17 +634,11 @@ export class Wunderbaum {
|
|
|
639
634
|
ROW_HEIGHT
|
|
640
635
|
) - 1;
|
|
641
636
|
}
|
|
642
|
-
|
|
643
|
-
this.
|
|
644
|
-
if (n._rowIdx === bottomIdx) {
|
|
645
|
-
node = n;
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
return <WunderbaumNode>node!;
|
|
637
|
+
bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
|
|
638
|
+
return this._getNodeByRowIdx(bottomIdx)!;
|
|
650
639
|
}
|
|
651
640
|
|
|
652
|
-
/** Return preceeding visible node in the viewport */
|
|
641
|
+
/** Return preceeding visible node in the viewport. */
|
|
653
642
|
protected _getPrevNodeInView(node?: WunderbaumNode, ofs = 1) {
|
|
654
643
|
this.visitRows(
|
|
655
644
|
(n) => {
|
|
@@ -663,7 +652,7 @@ export class Wunderbaum {
|
|
|
663
652
|
return node;
|
|
664
653
|
}
|
|
665
654
|
|
|
666
|
-
/** Return following visible node in the viewport */
|
|
655
|
+
/** Return following visible node in the viewport. */
|
|
667
656
|
protected _getNextNodeInView(node?: WunderbaumNode, ofs = 1) {
|
|
668
657
|
this.visitRows(
|
|
669
658
|
(n) => {
|
|
@@ -677,23 +666,27 @@ export class Wunderbaum {
|
|
|
677
666
|
return node;
|
|
678
667
|
}
|
|
679
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Append (or insert) a list of toplevel nodes.
|
|
671
|
+
*
|
|
672
|
+
* @see {@link WunderbaumNode.addChildren}
|
|
673
|
+
*/
|
|
680
674
|
addChildren(nodeData: any, options?: any): WunderbaumNode {
|
|
681
675
|
return this.root.addChildren(nodeData, options);
|
|
682
676
|
}
|
|
683
677
|
|
|
684
678
|
/**
|
|
685
|
-
* Apply a modification (or navigation) operation on the tree or active node
|
|
686
|
-
* @returns
|
|
679
|
+
* Apply a modification (or navigation) operation on the **tree or active node**.
|
|
687
680
|
*/
|
|
688
681
|
applyCommand(cmd: ApplyCommandType, opts?: any): any;
|
|
689
682
|
|
|
690
683
|
/**
|
|
691
|
-
* Apply a modification (or navigation) operation on a node
|
|
692
|
-
* @
|
|
684
|
+
* Apply a modification (or navigation) operation on a **node**.
|
|
685
|
+
* @see {@link WunderbaumNode.applyCommand}
|
|
693
686
|
*/
|
|
694
687
|
applyCommand(cmd: ApplyCommandType, node: WunderbaumNode, opts?: any): any;
|
|
695
688
|
|
|
696
|
-
|
|
689
|
+
/**
|
|
697
690
|
* Apply a modification or navigation operation.
|
|
698
691
|
*
|
|
699
692
|
* Most of these commands simply map to a node or tree method.
|
|
@@ -824,7 +817,8 @@ export class Wunderbaum {
|
|
|
824
817
|
this.root.children = null;
|
|
825
818
|
this.keyMap.clear();
|
|
826
819
|
this.refKeyMap.clear();
|
|
827
|
-
this.viewNodes.clear();
|
|
820
|
+
// this.viewNodes.clear();
|
|
821
|
+
this.treeRowCount = 0;
|
|
828
822
|
this.activeNode = null;
|
|
829
823
|
this.focusNode = null;
|
|
830
824
|
|
|
@@ -833,9 +827,9 @@ export class Wunderbaum {
|
|
|
833
827
|
// this._columnsById = {};
|
|
834
828
|
|
|
835
829
|
// Modification Status
|
|
836
|
-
this.changedSince = 0;
|
|
837
|
-
this.changes.clear();
|
|
838
|
-
this.changedNodes.clear();
|
|
830
|
+
// this.changedSince = 0;
|
|
831
|
+
// this.changes.clear();
|
|
832
|
+
// this.changedNodes.clear();
|
|
839
833
|
|
|
840
834
|
// // --- FILTER ---
|
|
841
835
|
// public filterMode: FilterModeType = null;
|
|
@@ -845,7 +839,7 @@ export class Wunderbaum {
|
|
|
845
839
|
// public cellNavMode = false;
|
|
846
840
|
// public lastQuicksearchTime = 0;
|
|
847
841
|
// public lastQuicksearchTerm = "";
|
|
848
|
-
this.
|
|
842
|
+
this.setModified(ChangeType.structure);
|
|
849
843
|
}
|
|
850
844
|
|
|
851
845
|
/**
|
|
@@ -867,10 +861,11 @@ export class Wunderbaum {
|
|
|
867
861
|
/**
|
|
868
862
|
* Return `tree.option.NAME` (also resolving if this is a callback).
|
|
869
863
|
*
|
|
870
|
-
* See also
|
|
871
|
-
* `tree.types[node.type].NAME`.
|
|
864
|
+
* See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
|
|
865
|
+
* to consider `node.NAME` setting and `tree.types[node.type].NAME`.
|
|
872
866
|
*
|
|
873
|
-
* @param name option name (use dot notation to access extension option, e.g.
|
|
867
|
+
* @param name option name (use dot notation to access extension option, e.g.
|
|
868
|
+
* `filter.mode`)
|
|
874
869
|
*/
|
|
875
870
|
getOption(name: string, defaultValue?: any): any {
|
|
876
871
|
let ext;
|
|
@@ -919,17 +914,13 @@ export class Wunderbaum {
|
|
|
919
914
|
|
|
920
915
|
/** Run code, but defer `updateViewport()` until done. */
|
|
921
916
|
runWithoutUpdate(func: () => any, hint = null): void {
|
|
922
|
-
// const prev = this._disableUpdate;
|
|
923
|
-
// const start = Date.now();
|
|
924
|
-
// this._disableUpdate = Date.now();
|
|
925
917
|
try {
|
|
926
918
|
this.enableUpdate(false);
|
|
927
|
-
|
|
919
|
+
const res = func();
|
|
920
|
+
util.assert(!(res instanceof Promise));
|
|
921
|
+
return res;
|
|
928
922
|
} finally {
|
|
929
923
|
this.enableUpdate(true);
|
|
930
|
-
// if (!prev && this._disableUpdate === start) {
|
|
931
|
-
// this._disableUpdate = 0;
|
|
932
|
-
// }
|
|
933
924
|
}
|
|
934
925
|
}
|
|
935
926
|
|
|
@@ -946,14 +937,15 @@ export class Wunderbaum {
|
|
|
946
937
|
}
|
|
947
938
|
|
|
948
939
|
/** Return the number of nodes in the data model.*/
|
|
949
|
-
count(visible = false) {
|
|
940
|
+
count(visible = false): number {
|
|
950
941
|
if (visible) {
|
|
951
|
-
return this.
|
|
942
|
+
return this.treeRowCount;
|
|
943
|
+
// return this.viewNodes.size;
|
|
952
944
|
}
|
|
953
945
|
return this.keyMap.size;
|
|
954
946
|
}
|
|
955
947
|
|
|
956
|
-
|
|
948
|
+
/** @internal sanity check. */
|
|
957
949
|
_check() {
|
|
958
950
|
let i = 0;
|
|
959
951
|
this.visit((n) => {
|
|
@@ -965,27 +957,32 @@ export class Wunderbaum {
|
|
|
965
957
|
// util.assert(this.keyMap.size === i);
|
|
966
958
|
}
|
|
967
959
|
|
|
968
|
-
/**
|
|
960
|
+
/**
|
|
961
|
+
* Find all nodes that matches condition.
|
|
969
962
|
*
|
|
970
963
|
* @param match title string to search for, or a
|
|
971
964
|
* callback function that returns `true` if a node is matched.
|
|
972
|
-
*
|
|
965
|
+
*
|
|
966
|
+
* @see {@link WunderbaumNode.findAll}
|
|
973
967
|
*/
|
|
974
968
|
findAll(match: string | MatcherType) {
|
|
975
969
|
return this.root.findAll(match);
|
|
976
970
|
}
|
|
977
971
|
|
|
978
|
-
/**
|
|
972
|
+
/**
|
|
973
|
+
* Find first node that matches condition.
|
|
979
974
|
*
|
|
980
975
|
* @param match title string to search for, or a
|
|
981
976
|
* callback function that returns `true` if a node is matched.
|
|
982
|
-
* @see
|
|
977
|
+
* @see {@link WunderbaumNode.findFirst}
|
|
978
|
+
*
|
|
983
979
|
*/
|
|
984
980
|
findFirst(match: string | MatcherType) {
|
|
985
981
|
return this.root.findFirst(match);
|
|
986
982
|
}
|
|
987
983
|
|
|
988
|
-
/**
|
|
984
|
+
/**
|
|
985
|
+
* Find the next visible node that starts with `match`, starting at `startNode`
|
|
989
986
|
* and wrap-around at the end.
|
|
990
987
|
*/
|
|
991
988
|
findNextNode(
|
|
@@ -1023,7 +1020,8 @@ export class Wunderbaum {
|
|
|
1023
1020
|
return res;
|
|
1024
1021
|
}
|
|
1025
1022
|
|
|
1026
|
-
/**
|
|
1023
|
+
/**
|
|
1024
|
+
* Find a node relative to another node.
|
|
1027
1025
|
*
|
|
1028
1026
|
* @param node
|
|
1029
1027
|
* @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
|
|
@@ -1033,7 +1031,7 @@ export class Wunderbaum {
|
|
|
1033
1031
|
*/
|
|
1034
1032
|
findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) {
|
|
1035
1033
|
let res = null;
|
|
1036
|
-
|
|
1034
|
+
const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
|
|
1037
1035
|
|
|
1038
1036
|
switch (where) {
|
|
1039
1037
|
case "parent":
|
|
@@ -1090,21 +1088,23 @@ export class Wunderbaum {
|
|
|
1090
1088
|
res = this._getNextNodeInView(node);
|
|
1091
1089
|
break;
|
|
1092
1090
|
case "pageDown":
|
|
1093
|
-
|
|
1094
|
-
// this.logDebug(where
|
|
1091
|
+
const bottomNode = this.getLowestVpNode();
|
|
1092
|
+
// this.logDebug(`${where}(${node}) -> ${bottomNode}`);
|
|
1095
1093
|
|
|
1096
|
-
if (
|
|
1094
|
+
if (node._rowIdx! < bottomNode._rowIdx!) {
|
|
1097
1095
|
res = bottomNode;
|
|
1098
1096
|
} else {
|
|
1099
1097
|
res = this._getNextNodeInView(node, pageSize);
|
|
1100
1098
|
}
|
|
1101
1099
|
break;
|
|
1102
1100
|
case "pageUp":
|
|
1103
|
-
if (
|
|
1104
|
-
res =
|
|
1101
|
+
if (node._rowIdx === 0) {
|
|
1102
|
+
res = node;
|
|
1105
1103
|
} else {
|
|
1106
|
-
|
|
1107
|
-
|
|
1104
|
+
const topNode = this.getTopmostVpNode();
|
|
1105
|
+
// this.logDebug(`${where}(${node}) -> ${topNode}`);
|
|
1106
|
+
|
|
1107
|
+
if (node._rowIdx! > topNode._rowIdx!) {
|
|
1108
1108
|
res = topNode;
|
|
1109
1109
|
} else {
|
|
1110
1110
|
res = this._getPrevNodeInView(node, pageSize);
|
|
@@ -1118,7 +1118,7 @@ export class Wunderbaum {
|
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
1120
1120
|
/**
|
|
1121
|
-
* Return the active cell of the currently active node or null.
|
|
1121
|
+
* Return the active cell (`span.wb-col`) of the currently active node or null.
|
|
1122
1122
|
*/
|
|
1123
1123
|
getActiveColElem() {
|
|
1124
1124
|
if (this.activeNode && this.activeColIdx >= 0) {
|
|
@@ -1157,9 +1157,11 @@ export class Wunderbaum {
|
|
|
1157
1157
|
static getEventInfo(event: Event) {
|
|
1158
1158
|
let target = <Element>event.target,
|
|
1159
1159
|
cl = target.classList,
|
|
1160
|
-
parentCol = target.closest(".wb-col"),
|
|
1160
|
+
parentCol = target.closest("span.wb-col") as HTMLSpanElement,
|
|
1161
1161
|
node = Wunderbaum.getNode(target),
|
|
1162
|
+
tree = node ? node.tree : Wunderbaum.getTree(event),
|
|
1162
1163
|
res = {
|
|
1164
|
+
tree: tree,
|
|
1163
1165
|
node: node,
|
|
1164
1166
|
region: NodeRegion.unknown,
|
|
1165
1167
|
colDef: undefined,
|
|
@@ -1189,13 +1191,15 @@ export class Wunderbaum {
|
|
|
1189
1191
|
res.colIdx = idx;
|
|
1190
1192
|
} else {
|
|
1191
1193
|
// Somewhere near the title
|
|
1192
|
-
|
|
1194
|
+
if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
|
|
1195
|
+
console.warn("getEventInfo(): not found", event, res);
|
|
1196
|
+
}
|
|
1193
1197
|
return res;
|
|
1194
1198
|
}
|
|
1195
1199
|
if (res.colIdx === -1) {
|
|
1196
1200
|
res.colIdx = 0;
|
|
1197
1201
|
}
|
|
1198
|
-
res.colDef =
|
|
1202
|
+
res.colDef = tree?.columns[res.colIdx];
|
|
1199
1203
|
res.colDef != null ? (res.colId = (<any>res.colDef).id) : 0;
|
|
1200
1204
|
// this.log("Event", event, res);
|
|
1201
1205
|
return res;
|
|
@@ -1223,7 +1227,8 @@ export class Wunderbaum {
|
|
|
1223
1227
|
return this._callMethod("edit.isEditingTitle");
|
|
1224
1228
|
}
|
|
1225
1229
|
|
|
1226
|
-
/**
|
|
1230
|
+
/**
|
|
1231
|
+
* Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
|
|
1227
1232
|
*/
|
|
1228
1233
|
isLoading(): boolean {
|
|
1229
1234
|
var res = false;
|
|
@@ -1238,8 +1243,10 @@ export class Wunderbaum {
|
|
|
1238
1243
|
return res;
|
|
1239
1244
|
}
|
|
1240
1245
|
|
|
1241
|
-
/** Alias for
|
|
1242
|
-
|
|
1246
|
+
/** Alias for {@link Wunderbaum.logDebug}.
|
|
1247
|
+
* @alias Wunderbaum.logDebug
|
|
1248
|
+
*/
|
|
1249
|
+
log = this.logDebug;
|
|
1243
1250
|
|
|
1244
1251
|
/** Log to console if opts.debugLevel >= 4 */
|
|
1245
1252
|
logDebug(...args: any[]) {
|
|
@@ -1257,7 +1264,7 @@ export class Wunderbaum {
|
|
|
1257
1264
|
}
|
|
1258
1265
|
}
|
|
1259
1266
|
|
|
1260
|
-
|
|
1267
|
+
/** Log to console if opts.debugLevel >= 3 */
|
|
1261
1268
|
logInfo(...args: any[]) {
|
|
1262
1269
|
if (this.options.debugLevel >= 3) {
|
|
1263
1270
|
Array.prototype.unshift.call(args, this.toString());
|
|
@@ -1288,85 +1295,13 @@ export class Wunderbaum {
|
|
|
1288
1295
|
}
|
|
1289
1296
|
}
|
|
1290
1297
|
|
|
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
1298
|
/**
|
|
1299
|
+
* Make sure that this node is scrolled into the viewport.
|
|
1364
1300
|
*
|
|
1365
1301
|
* @param {boolean | PlainObject} [effects=false] animation options.
|
|
1366
1302
|
* @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
|
|
1367
1303
|
* this node will remain visible in
|
|
1368
1304
|
* any case, even if `this` is outside the scroll pane.
|
|
1369
|
-
* Make sure that a node is scrolled into the viewport.
|
|
1370
1305
|
*/
|
|
1371
1306
|
scrollTo(opts: any) {
|
|
1372
1307
|
const MARGIN = 1;
|
|
@@ -1388,53 +1323,40 @@ export class Wunderbaum {
|
|
|
1388
1323
|
// Node is above viewport
|
|
1389
1324
|
newTop = nodeOfs + MARGIN;
|
|
1390
1325
|
}
|
|
1391
|
-
this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
|
|
1392
1326
|
if (newTop != null) {
|
|
1327
|
+
this.log(
|
|
1328
|
+
"scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop,
|
|
1329
|
+
height
|
|
1330
|
+
);
|
|
1393
1331
|
this.scrollContainer.scrollTop = newTop;
|
|
1394
|
-
this.
|
|
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);
|
|
1332
|
+
this.setModified(ChangeType.vscroll);
|
|
1411
1333
|
}
|
|
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
1334
|
}
|
|
1419
1335
|
|
|
1420
|
-
/**
|
|
1336
|
+
/**
|
|
1337
|
+
* Set column #colIdx to 'active'.
|
|
1338
|
+
*
|
|
1339
|
+
* This higlights the column header and -cells by adding the `wb-active` class.
|
|
1340
|
+
* Available in cell-nav and cell-edit mode, not in row-mode.
|
|
1341
|
+
*/
|
|
1421
1342
|
setColumn(colIdx: number) {
|
|
1422
1343
|
util.assert(this.navMode !== NavigationMode.row);
|
|
1423
1344
|
util.assert(0 <= colIdx && colIdx < this.columns.length);
|
|
1424
1345
|
this.activeColIdx = colIdx;
|
|
1425
|
-
|
|
1426
|
-
this.setModified(ChangeType.row, this.activeNode);
|
|
1346
|
+
|
|
1427
1347
|
// Update `wb-active` class for all headers
|
|
1428
1348
|
if (this.headerElement) {
|
|
1429
1349
|
for (let rowDiv of this.headerElement.children) {
|
|
1430
|
-
// for (let rowDiv of document.querySelector("div.wb-header").children) {
|
|
1431
1350
|
let i = 0;
|
|
1432
1351
|
for (let colDiv of rowDiv.children) {
|
|
1433
1352
|
(colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
|
|
1434
1353
|
}
|
|
1435
1354
|
}
|
|
1436
1355
|
}
|
|
1437
|
-
|
|
1356
|
+
|
|
1357
|
+
this.activeNode?.setModified(ChangeType.status);
|
|
1358
|
+
|
|
1359
|
+
// Update `wb-active` class for all cell spans
|
|
1438
1360
|
for (let rowDiv of this.nodeListElement.children) {
|
|
1439
1361
|
let i = 0;
|
|
1440
1362
|
for (let colDiv of rowDiv.children) {
|
|
@@ -1443,7 +1365,7 @@ export class Wunderbaum {
|
|
|
1443
1365
|
}
|
|
1444
1366
|
}
|
|
1445
1367
|
|
|
1446
|
-
/** */
|
|
1368
|
+
/** Set or remove keybaord focus to the tree container. */
|
|
1447
1369
|
setFocus(flag = true) {
|
|
1448
1370
|
if (flag) {
|
|
1449
1371
|
this.element.focus();
|
|
@@ -1452,35 +1374,78 @@ export class Wunderbaum {
|
|
|
1452
1374
|
}
|
|
1453
1375
|
}
|
|
1454
1376
|
|
|
1455
|
-
/** */
|
|
1377
|
+
/** Schedule an update request to reflect a tree change. */
|
|
1456
1378
|
setModified(change: ChangeType, options?: any): void;
|
|
1457
1379
|
|
|
1458
|
-
/** */
|
|
1380
|
+
/** Schedule an update request to reflect a single node modification. */
|
|
1381
|
+
setModified(change: ChangeType, node: WunderbaumNode, options?: any): void;
|
|
1382
|
+
|
|
1459
1383
|
setModified(
|
|
1460
1384
|
change: ChangeType,
|
|
1461
1385
|
node?: WunderbaumNode | any,
|
|
1462
1386
|
options?: any
|
|
1463
1387
|
): void {
|
|
1388
|
+
if (this._disableUpdateCount) {
|
|
1389
|
+
// Assuming that we redraw all when enableUpdate() is re-enabled.
|
|
1390
|
+
// this.log(
|
|
1391
|
+
// `IGNORED setModified(${change}) node=${node} (disable level ${this._disableUpdateCount})`
|
|
1392
|
+
// );
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
// this.log(`setModified(${change}) node=${node}`);
|
|
1464
1396
|
if (!(node instanceof WunderbaumNode)) {
|
|
1465
1397
|
options = node;
|
|
1466
1398
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1399
|
+
const immediate = !!util.getOption(options, "immediate");
|
|
1400
|
+
|
|
1401
|
+
switch (change) {
|
|
1402
|
+
case ChangeType.any:
|
|
1403
|
+
case ChangeType.structure:
|
|
1404
|
+
case ChangeType.header:
|
|
1405
|
+
this.changeRedrawRequestPending = true;
|
|
1406
|
+
this.updateViewport(immediate);
|
|
1407
|
+
break;
|
|
1408
|
+
case ChangeType.vscroll:
|
|
1409
|
+
this.updateViewport(immediate);
|
|
1410
|
+
break;
|
|
1411
|
+
case ChangeType.row:
|
|
1412
|
+
case ChangeType.data:
|
|
1413
|
+
case ChangeType.status:
|
|
1414
|
+
// Single nodes are immedialtely updated if already inside the viewport
|
|
1415
|
+
// (otherwise we can ignore)
|
|
1416
|
+
if (node._rowElem) {
|
|
1417
|
+
node.render({ change: change });
|
|
1418
|
+
}
|
|
1419
|
+
break;
|
|
1420
|
+
default:
|
|
1421
|
+
util.error(`Invalid change type ${change}`);
|
|
1469
1422
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/** Set the tree's navigation mode. */
|
|
1426
|
+
setNavigationMode(mode: NavigationMode) {
|
|
1427
|
+
// util.assert(this.cellNavMode);
|
|
1428
|
+
// util.assert(0 <= colIdx && colIdx < this.columns.length);
|
|
1429
|
+
if (mode === this.navMode) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const prevMode = this.navMode;
|
|
1433
|
+
const cellMode = mode !== NavigationMode.row;
|
|
1434
|
+
|
|
1435
|
+
this.navMode = mode;
|
|
1436
|
+
if (cellMode && prevMode === NavigationMode.row) {
|
|
1437
|
+
this.setColumn(0);
|
|
1480
1438
|
}
|
|
1481
|
-
|
|
1439
|
+
this.element.classList.toggle("wb-cell-mode", cellMode);
|
|
1440
|
+
this.element.classList.toggle(
|
|
1441
|
+
"wb-cell-edit-mode",
|
|
1442
|
+
mode === NavigationMode.cellEdit
|
|
1443
|
+
);
|
|
1444
|
+
// this.setModified(ChangeType.row, this.activeNode);
|
|
1445
|
+
this.activeNode?.setModified(ChangeType.status);
|
|
1482
1446
|
}
|
|
1483
1447
|
|
|
1448
|
+
/** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
|
|
1484
1449
|
setStatus(
|
|
1485
1450
|
status: NodeStatusType,
|
|
1486
1451
|
message?: string,
|
|
@@ -1490,111 +1455,289 @@ export class Wunderbaum {
|
|
|
1490
1455
|
}
|
|
1491
1456
|
|
|
1492
1457
|
/** Update column headers and width. */
|
|
1493
|
-
updateColumns(opts
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1458
|
+
updateColumns(opts?: any) {
|
|
1459
|
+
opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
|
|
1460
|
+
const minWidth = 4;
|
|
1461
|
+
const vpWidth = this.element.clientWidth;
|
|
1497
1462
|
let totalWeight = 0;
|
|
1498
1463
|
let fixedWidth = 0;
|
|
1499
1464
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
|
|
1465
|
+
let modified = false;
|
|
1466
|
+
|
|
1467
|
+
if (opts.calculateCols) {
|
|
1468
|
+
// Gather width requests
|
|
1469
|
+
this._columnsById = {};
|
|
1470
|
+
for (let col of this.columns) {
|
|
1471
|
+
this._columnsById[<string>col.id] = col;
|
|
1472
|
+
let cw = col.width;
|
|
1473
|
+
|
|
1474
|
+
if (!cw || cw === "*") {
|
|
1475
|
+
col._weight = 1.0;
|
|
1476
|
+
totalWeight += 1.0;
|
|
1477
|
+
} else if (typeof cw === "number") {
|
|
1478
|
+
col._weight = cw;
|
|
1479
|
+
totalWeight += cw;
|
|
1480
|
+
} else if (typeof cw === "string" && cw.endsWith("px")) {
|
|
1481
|
+
col._weight = 0;
|
|
1482
|
+
let px = parseFloat(cw.slice(0, -2));
|
|
1483
|
+
if (col._widthPx != px) {
|
|
1484
|
+
modified = true;
|
|
1485
|
+
col._widthPx = px;
|
|
1486
|
+
}
|
|
1487
|
+
fixedWidth += px;
|
|
1488
|
+
} else {
|
|
1489
|
+
util.error("Invalid column width: " + cw);
|
|
1518
1490
|
}
|
|
1519
|
-
fixedWidth += px;
|
|
1520
|
-
} else {
|
|
1521
|
-
util.error("Invalid column width: " + cw);
|
|
1522
1491
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1492
|
+
// Share remaining space between non-fixed columns
|
|
1493
|
+
const restPx = Math.max(0, vpWidth - fixedWidth);
|
|
1494
|
+
let ofsPx = 0;
|
|
1495
|
+
|
|
1496
|
+
for (let col of this.columns) {
|
|
1497
|
+
if (col._weight) {
|
|
1498
|
+
const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
|
|
1499
|
+
if (col._widthPx != px) {
|
|
1500
|
+
modified = true;
|
|
1501
|
+
col._widthPx = px;
|
|
1502
|
+
}
|
|
1534
1503
|
}
|
|
1504
|
+
col._ofsPx = ofsPx;
|
|
1505
|
+
ofsPx += col._widthPx;
|
|
1535
1506
|
}
|
|
1536
|
-
col._ofsPx = ofsPx;
|
|
1537
|
-
ofsPx += col._widthPx;
|
|
1538
1507
|
}
|
|
1539
1508
|
// Every column has now a calculated `_ofsPx` and `_widthPx`
|
|
1540
1509
|
// this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
|
|
1541
1510
|
// console.trace();
|
|
1542
1511
|
// util.error("BREAK");
|
|
1543
1512
|
if (modified) {
|
|
1544
|
-
this.
|
|
1545
|
-
if (opts.
|
|
1546
|
-
this.
|
|
1513
|
+
this._renderHeaderMarkup();
|
|
1514
|
+
if (opts.updateRows) {
|
|
1515
|
+
this._updateRows();
|
|
1547
1516
|
}
|
|
1548
1517
|
}
|
|
1549
1518
|
}
|
|
1550
1519
|
|
|
1551
|
-
/**
|
|
1520
|
+
/** Create/update header markup from `this.columns` definition.
|
|
1521
|
+
* @internal
|
|
1522
|
+
*/
|
|
1523
|
+
protected _renderHeaderMarkup() {
|
|
1524
|
+
if (!this.headerElement) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const headerRow = this.headerElement.querySelector(".wb-row")!;
|
|
1528
|
+
util.assert(headerRow);
|
|
1529
|
+
headerRow.innerHTML = "<span class='wb-col'></span>".repeat(
|
|
1530
|
+
this.columns.length
|
|
1531
|
+
);
|
|
1532
|
+
|
|
1533
|
+
for (let i = 0; i < this.columns.length; i++) {
|
|
1534
|
+
const col = this.columns[i];
|
|
1535
|
+
const colElem = <HTMLElement>headerRow.children[i];
|
|
1536
|
+
|
|
1537
|
+
colElem.style.left = col._ofsPx + "px";
|
|
1538
|
+
colElem.style.width = col._widthPx + "px";
|
|
1539
|
+
// colElem.textContent = col.title || col.id;
|
|
1540
|
+
const title = util.escapeHtml(col.title || col.id);
|
|
1541
|
+
colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
|
|
1542
|
+
// colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/** Render header and all rows that are visible in the viewport (async, throttled). */
|
|
1552
1547
|
updateViewport(immediate = false) {
|
|
1553
1548
|
// Call the `throttle` wrapper for `this._updateViewport()` which will
|
|
1554
1549
|
// execute immediately on the leading edge of a sequence:
|
|
1555
|
-
this._updateViewportThrottled();
|
|
1556
1550
|
if (immediate) {
|
|
1557
|
-
this.
|
|
1551
|
+
this._updateViewport();
|
|
1552
|
+
} else {
|
|
1553
|
+
this._updateViewportThrottled();
|
|
1558
1554
|
}
|
|
1559
1555
|
}
|
|
1560
1556
|
|
|
1557
|
+
/**
|
|
1558
|
+
* This is the actual update method, which is wrapped inside a throttle method.
|
|
1559
|
+
* This protected method should not be called directly but via
|
|
1560
|
+
* `tree.updateViewport()` or `tree.setModified()`.
|
|
1561
|
+
* It calls `updateColumns()` and `_updateRows()`.
|
|
1562
|
+
* @internal
|
|
1563
|
+
*/
|
|
1561
1564
|
protected _updateViewport() {
|
|
1562
|
-
if (this.
|
|
1565
|
+
if (this._disableUpdateCount) {
|
|
1566
|
+
this.log(
|
|
1567
|
+
`IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`
|
|
1568
|
+
);
|
|
1563
1569
|
return;
|
|
1564
1570
|
}
|
|
1571
|
+
const newNodesOnly = !this.changeRedrawRequestPending;
|
|
1572
|
+
this.changeRedrawRequestPending = false;
|
|
1573
|
+
|
|
1565
1574
|
let height = this.scrollContainer.clientHeight;
|
|
1566
|
-
// We cannot get the height for
|
|
1575
|
+
// We cannot get the height for absolute positioned parent, so look at first col
|
|
1567
1576
|
// let headerHeight = this.headerElement.clientHeight
|
|
1568
1577
|
// let headerHeight = this.headerElement.children[0].children[0].clientHeight;
|
|
1569
1578
|
const headerHeight = this.options.headerHeightPx;
|
|
1570
|
-
|
|
1571
|
-
let ofs = this.scrollContainer.scrollTop;
|
|
1579
|
+
const wantHeight = this.element.clientHeight - headerHeight;
|
|
1572
1580
|
|
|
1573
1581
|
if (Math.abs(height - wantHeight) > 1.0) {
|
|
1574
1582
|
// this.log("resize", height, wantHeight);
|
|
1575
1583
|
this.scrollContainer.style.height = wantHeight + "px";
|
|
1576
1584
|
height = wantHeight;
|
|
1577
1585
|
}
|
|
1586
|
+
// console.profile(`_updateViewport()`)
|
|
1587
|
+
|
|
1588
|
+
this.updateColumns({ updateRows: false });
|
|
1589
|
+
|
|
1590
|
+
this._updateRows({ newNodesOnly: newNodesOnly });
|
|
1591
|
+
|
|
1592
|
+
// console.profileEnd(`_updateViewport()`)
|
|
1578
1593
|
|
|
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
1594
|
this._callEvent("update");
|
|
1585
1595
|
}
|
|
1586
1596
|
|
|
1587
|
-
/**
|
|
1597
|
+
/**
|
|
1598
|
+
* Assert that TR order matches the natural node order
|
|
1599
|
+
* @internal
|
|
1600
|
+
*/
|
|
1601
|
+
protected _validateRows(): boolean {
|
|
1602
|
+
let trs = this.nodeListElement.childNodes;
|
|
1603
|
+
let i = 0;
|
|
1604
|
+
let prev = -1;
|
|
1605
|
+
let ok = true;
|
|
1606
|
+
trs.forEach((element) => {
|
|
1607
|
+
const tr = element as HTMLTableRowElement;
|
|
1608
|
+
const top = Number.parseInt(tr.style.top);
|
|
1609
|
+
const n = (<any>tr)._wb_node;
|
|
1610
|
+
// if (i < 4) {
|
|
1611
|
+
// console.info(
|
|
1612
|
+
// `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
|
|
1613
|
+
// );
|
|
1614
|
+
// }
|
|
1615
|
+
if (prev >= 0 && top !== prev + ROW_HEIGHT) {
|
|
1616
|
+
n.logWarn(
|
|
1617
|
+
`TR order mismatch at index ${i}: top=${top}px != ${
|
|
1618
|
+
prev + ROW_HEIGHT
|
|
1619
|
+
}`
|
|
1620
|
+
);
|
|
1621
|
+
// throw new Error("fault");
|
|
1622
|
+
ok = false;
|
|
1623
|
+
}
|
|
1624
|
+
prev = top;
|
|
1625
|
+
i++;
|
|
1626
|
+
});
|
|
1627
|
+
return ok;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/*
|
|
1631
|
+
* - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
|
|
1632
|
+
* - Store count of rows to `tree.treeRowCount`.
|
|
1633
|
+
* - Renumber `node._rowIdx` for all visible nodes.
|
|
1634
|
+
* - Calculate the index range that must be rendered to fill the viewport
|
|
1635
|
+
* (including upper and lower prefetch)
|
|
1636
|
+
* -
|
|
1637
|
+
*/
|
|
1638
|
+
protected _updateRows(opts?: any): boolean {
|
|
1639
|
+
const label = this.logTime("_updateRows");
|
|
1640
|
+
// this.log("_updateRows", opts)
|
|
1641
|
+
opts = Object.assign({ newNodesOnly: false }, opts);
|
|
1642
|
+
const newNodesOnly = !!opts.newNodesOnly;
|
|
1643
|
+
|
|
1644
|
+
const row_height = ROW_HEIGHT;
|
|
1645
|
+
const vp_height = this.scrollContainer.clientHeight;
|
|
1646
|
+
const prefetch = RENDER_MAX_PREFETCH;
|
|
1647
|
+
const ofs = this.scrollContainer.scrollTop;
|
|
1648
|
+
|
|
1649
|
+
let startIdx = Math.max(0, ofs / row_height - prefetch);
|
|
1650
|
+
startIdx = Math.floor(startIdx);
|
|
1651
|
+
// Make sure start is always even, so the alternating row colors don't
|
|
1652
|
+
// change when scrolling:
|
|
1653
|
+
if (startIdx % 2) {
|
|
1654
|
+
startIdx--;
|
|
1655
|
+
}
|
|
1656
|
+
let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
|
|
1657
|
+
endIdx = Math.ceil(endIdx);
|
|
1658
|
+
|
|
1659
|
+
// const obsoleteViewNodes = this.viewNodes;
|
|
1660
|
+
// this.viewNodes = new Set();
|
|
1661
|
+
// const viewNodes = this.viewNodes;
|
|
1662
|
+
// this.debug("render", opts);
|
|
1663
|
+
const obsoleteNodes = new Set<WunderbaumNode>();
|
|
1664
|
+
this.nodeListElement.childNodes.forEach((elem) => {
|
|
1665
|
+
const tr = elem as HTMLTableRowElement;
|
|
1666
|
+
obsoleteNodes.add((<any>tr)._wb_node);
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
let idx = 0;
|
|
1670
|
+
let top = 0;
|
|
1671
|
+
let modified = false;
|
|
1672
|
+
let prevElem: HTMLDivElement | "first" | "last" = "first";
|
|
1673
|
+
|
|
1674
|
+
this.visitRows(function (node) {
|
|
1675
|
+
// node.log("visit")
|
|
1676
|
+
const rowDiv = node._rowElem;
|
|
1677
|
+
|
|
1678
|
+
// Renumber all expanded nodes
|
|
1679
|
+
if (node._rowIdx !== idx) {
|
|
1680
|
+
node._rowIdx = idx;
|
|
1681
|
+
modified = true;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (idx < startIdx || idx > endIdx) {
|
|
1685
|
+
// row is outside viewport bounds
|
|
1686
|
+
if (rowDiv) {
|
|
1687
|
+
prevElem = rowDiv;
|
|
1688
|
+
}
|
|
1689
|
+
} else if (rowDiv && newNodesOnly) {
|
|
1690
|
+
obsoleteNodes.delete(node);
|
|
1691
|
+
// no need to update existing node markup
|
|
1692
|
+
rowDiv.style.top = idx * ROW_HEIGHT + "px";
|
|
1693
|
+
prevElem = rowDiv;
|
|
1694
|
+
} else {
|
|
1695
|
+
obsoleteNodes.delete(node);
|
|
1696
|
+
// Create new markup
|
|
1697
|
+
if (rowDiv) {
|
|
1698
|
+
rowDiv.style.top = idx * ROW_HEIGHT + "px";
|
|
1699
|
+
}
|
|
1700
|
+
node.render({ top: top, after: prevElem });
|
|
1701
|
+
// node.log("render", top, prevElem, "=>", node._rowElem);
|
|
1702
|
+
prevElem = node._rowElem!;
|
|
1703
|
+
}
|
|
1704
|
+
idx++;
|
|
1705
|
+
top += row_height;
|
|
1706
|
+
});
|
|
1707
|
+
this.treeRowCount = idx;
|
|
1708
|
+
for (const n of obsoleteNodes) {
|
|
1709
|
+
n._callEvent("discard");
|
|
1710
|
+
n.removeMarkup();
|
|
1711
|
+
}
|
|
1712
|
+
// Resize tree container
|
|
1713
|
+
this.nodeListElement.style.height = `${top}px`;
|
|
1714
|
+
// this.log(
|
|
1715
|
+
// `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
|
|
1716
|
+
// this.nodeListElement.style.height
|
|
1717
|
+
// );
|
|
1718
|
+
this.logTimeEnd(label);
|
|
1719
|
+
this._validateRows();
|
|
1720
|
+
return modified;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Call callback(node) for all nodes in hierarchical order (depth-first).
|
|
1588
1725
|
*
|
|
1589
1726
|
* @param {function} callback the callback function.
|
|
1590
|
-
* Return false to stop iteration, return "skip" to skip this node and
|
|
1727
|
+
* Return false to stop iteration, return "skip" to skip this node and
|
|
1728
|
+
* children only.
|
|
1591
1729
|
* @returns {boolean} false, if the iterator was stopped.
|
|
1592
1730
|
*/
|
|
1593
1731
|
visit(callback: (node: WunderbaumNode) => any) {
|
|
1594
1732
|
return this.root.visit(callback, false);
|
|
1595
1733
|
}
|
|
1596
1734
|
|
|
1597
|
-
/**
|
|
1735
|
+
/**
|
|
1736
|
+
* Call fn(node) for all nodes in vertical order, top down (or bottom up).
|
|
1737
|
+
*
|
|
1738
|
+
* Note that this considers expansion state, i.e. children of collapsed nodes
|
|
1739
|
+
* are skipped.
|
|
1740
|
+
*
|
|
1598
1741
|
* Stop iteration, if fn() returns false.<br>
|
|
1599
1742
|
* Return false if iteration was stopped.
|
|
1600
1743
|
*
|
|
@@ -1695,7 +1838,8 @@ export class Wunderbaum {
|
|
|
1695
1838
|
return true;
|
|
1696
1839
|
}
|
|
1697
1840
|
|
|
1698
|
-
/**
|
|
1841
|
+
/**
|
|
1842
|
+
* Call fn(node) for all nodes in vertical order, bottom up.
|
|
1699
1843
|
* @internal
|
|
1700
1844
|
*/
|
|
1701
1845
|
protected _visitRowsUp(
|
|
@@ -1750,20 +1894,37 @@ export class Wunderbaum {
|
|
|
1750
1894
|
return true;
|
|
1751
1895
|
}
|
|
1752
1896
|
|
|
1753
|
-
/**
|
|
1897
|
+
/**
|
|
1898
|
+
* Reload the tree with a new source.
|
|
1899
|
+
*
|
|
1900
|
+
* Previous data is cleared.
|
|
1901
|
+
* Pass `options.columns` to define a header (may also be part of `source.columns`).
|
|
1902
|
+
*/
|
|
1754
1903
|
load(source: any, options: any = {}) {
|
|
1755
1904
|
this.clear();
|
|
1756
1905
|
const columns = options.columns || source.columns;
|
|
1757
1906
|
if (columns) {
|
|
1758
1907
|
this.columns = options.columns;
|
|
1759
|
-
this.
|
|
1760
|
-
|
|
1908
|
+
// this._renderHeaderMarkup();
|
|
1909
|
+
this.updateColumns({ calculateCols: false });
|
|
1761
1910
|
}
|
|
1762
1911
|
return this.root.load(source);
|
|
1763
1912
|
}
|
|
1764
1913
|
|
|
1765
1914
|
/**
|
|
1915
|
+
* Disable render requests during operations that would trigger many updates.
|
|
1766
1916
|
*
|
|
1917
|
+
* ```js
|
|
1918
|
+
* try {
|
|
1919
|
+
* tree.enableUpdate(false);
|
|
1920
|
+
* // ... (long running operation that would trigger many updates)
|
|
1921
|
+
* foo();
|
|
1922
|
+
* // ... NOTE: make sure that async operations have finished, e.g.
|
|
1923
|
+
* await foo();
|
|
1924
|
+
* } finally {
|
|
1925
|
+
* tree.enableUpdate(true);
|
|
1926
|
+
* }
|
|
1927
|
+
* ```
|
|
1767
1928
|
*/
|
|
1768
1929
|
public enableUpdate(flag: boolean): void {
|
|
1769
1930
|
/*
|
|
@@ -1771,19 +1932,24 @@ export class Wunderbaum {
|
|
|
1771
1932
|
1 >-------------------------------------<
|
|
1772
1933
|
2 >--------------------<
|
|
1773
1934
|
3 >--------------------------<
|
|
1774
|
-
|
|
1775
|
-
5
|
|
1776
|
-
|
|
1777
1935
|
*/
|
|
1778
|
-
// this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
|
|
1779
1936
|
if (flag) {
|
|
1780
|
-
util.assert(
|
|
1937
|
+
util.assert(
|
|
1938
|
+
this._disableUpdateCount > 0,
|
|
1939
|
+
"enableUpdate(true) was called too often"
|
|
1940
|
+
);
|
|
1781
1941
|
this._disableUpdateCount--;
|
|
1942
|
+
// this.logDebug(
|
|
1943
|
+
// `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
|
|
1944
|
+
// );
|
|
1782
1945
|
if (this._disableUpdateCount === 0) {
|
|
1783
1946
|
this.updateViewport();
|
|
1784
1947
|
}
|
|
1785
1948
|
} else {
|
|
1786
1949
|
this._disableUpdateCount++;
|
|
1950
|
+
// this.logDebug(
|
|
1951
|
+
// `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
|
|
1952
|
+
// );
|
|
1787
1953
|
// this._disableUpdate = Date.now();
|
|
1788
1954
|
}
|
|
1789
1955
|
// return !flag; // return previous value
|