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