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/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 version: string = "@VERSION"; // Set to semver by 'grunt release'
52
- static sequence = 0;
52
+ protected static sequence = 0;
53
53
 
54
- /** The invisible root node, that holds all visible top level nodes. */
55
- readonly root: WunderbaumNode;
56
- readonly id: string;
57
-
58
- readonly element: HTMLDivElement;
59
- // readonly treeElement: HTMLDivElement;
60
- readonly headerElement: HTMLDivElement | null;
61
- readonly scrollContainer: HTMLDivElement;
62
- readonly nodeListElement: HTMLDivElement;
63
- readonly _updateViewportThrottled: DebouncedFunction<(...args: any) => void>;
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
- // protected rows: WunderbaumNode[] = [];
76
- // protected _rowCount = 0;
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 resizeObserver;
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.updateViewport();
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.updateViewport();
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
- // _renderHeader(){
420
- // const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
421
- // this.element.innerHTML = `
422
- // <div class='wb-header'>
423
- // <div class='wb-row'>
424
- // ${coldivs}
425
- // </div>
426
- // </div>`;
427
-
428
- // }
429
-
430
- /** Return a Wunderbaum instance, from element, index, or event.
416
+ /**
417
+ * Return a Wunderbaum instance, from element, id, index, or event.
431
418
  *
432
- * @example
433
- * getTree(); // Get first Wunderbaum instance on page
434
- * getTree(1); // Get second Wunderbaum instance on page
435
- * getTree(event); // Get tree for this mouse- or keyboard event
436
- * getTree("foo"); // Get tree for this `tree.options.id`
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
- /** Return a WunderbaumNode instance from element, event.
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
- /** Call tree method or extension method if defined.
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
- /** Call event handler if defined in tree.options.
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 topmost visible node in the viewport */
609
- protected _firstNodeInView(complete = true) {
610
- let topIdx: number, node: WunderbaumNode;
611
- if (complete) {
612
- topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
613
- } else {
614
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
615
- }
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 === topIdx) {
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 lowest visible node in the viewport */
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, node: WunderbaumNode;
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
- // TODO: start searching from active node
643
- this.visitRows((n) => {
644
- if (n._rowIdx === bottomIdx) {
645
- node = n;
646
- return false;
647
- }
648
- });
649
- return <WunderbaumNode>node!;
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
- * @returns
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.updateViewport();
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 [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
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. `filter.mode`)
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
- return func();
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.viewNodes.size;
948
+ return this.treeRowCount;
949
+ // return this.viewNodes.size;
952
950
  }
953
951
  return this.keyMap.size;
954
952
  }
955
953
 
956
- /* Internal sanity check. */
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
- /**Find all nodes that matches condition.
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
- * @see [[WunderbaumNode.findAll]]
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
- /**Find first node that matches condition.
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 [[WunderbaumNode.findFirst]]
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
- /** Find the next visible node that starts with `match`, starting at `startNode`
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
- /** Find a node relative to another node.
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
- let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
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
- let bottomNode = this._lastNodeInView();
1094
- // this.logDebug(where, this.focusNode, bottomNode);
1097
+ const bottomNode = this._lastNodeInView();
1098
+ // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
1095
1099
 
1096
- if (this.focusNode !== bottomNode) {
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 (this.focusNode && this.focusNode._rowIdx === 0) {
1104
- res = this.focusNode;
1107
+ if (node._rowIdx === 0) {
1108
+ res = node;
1105
1109
  } else {
1106
- let topNode = this._firstNodeInView();
1107
- if (this.focusNode !== topNode) {
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
- /** Return the first top level node if any (not the invisible root node).
1138
- * @returns {FancytreeNode | null}
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: FancytreeNode, region: TYPE} object for a mouse event.
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: FancytreeNode, region: TYPE} object
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
- console.warn("getEventInfo(): not found", event, res);
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 = node!.tree.columns[res.colIdx];
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
- /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
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 `logDebug` */
1242
- log = this.logDebug; // Alias
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
- /* Log to console if opts.debugLevel >= 3 */
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.updateViewport();
1395
- }
1396
- }
1397
-
1398
- /** */
1399
- setCellMode(mode: NavigationMode) {
1400
- // util.assert(this.cellNavMode);
1401
- // util.assert(0 <= colIdx && colIdx < this.columns.length);
1402
- if (mode === this.navMode) {
1403
- return;
1404
- }
1405
- const prevMode = this.navMode;
1406
- const cellMode = mode !== NavigationMode.row;
1407
-
1408
- this.navMode = mode;
1409
- if (cellMode && prevMode === NavigationMode.row) {
1410
- this.setColumn(0);
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
- if (!this.changedSince) {
1468
- this.changedSince = Date.now();
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
- this.changes.add(change);
1471
- if (change === ChangeType.structure) {
1472
- this.changedNodes.clear();
1473
- } else if (node && !this.changes.has(ChangeType.structure)) {
1474
- if (this.changedNodes.size < MAX_CHANGED_NODES) {
1475
- this.changedNodes.add(node);
1476
- } else {
1477
- this.changes.add(ChangeType.structure);
1478
- this.changedNodes.clear();
1479
- }
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
- // this.log("setModified(" + change + ")", node);
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: any) {
1494
- let modified = false;
1495
- let minWidth = 4;
1496
- let vpWidth = this.element.clientWidth;
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
- // Gather width requests
1501
- this._columnsById = {};
1502
- for (let col of this.columns) {
1503
- this._columnsById[<string>col.id] = col;
1504
- let cw = col.width;
1505
-
1506
- if (!cw || cw === "*") {
1507
- col._weight = 1.0;
1508
- totalWeight += 1.0;
1509
- } else if (typeof cw === "number") {
1510
- col._weight = cw;
1511
- totalWeight += cw;
1512
- } else if (typeof cw === "string" && cw.endsWith("px")) {
1513
- col._weight = 0;
1514
- let px = parseFloat(cw.slice(0, -2));
1515
- if (col._widthPx != px) {
1516
- modified = true;
1517
- col._widthPx = px;
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
- // Share remaining space between non-fixed columns
1525
- let restPx = Math.max(0, vpWidth - fixedWidth);
1526
- let ofsPx = 0;
1527
-
1528
- for (let col of this.columns) {
1529
- if (col._weight) {
1530
- let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
1531
- if (col._widthPx != px) {
1532
- modified = true;
1533
- col._widthPx = px;
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.renderHeader();
1545
- if (opts.render !== false) {
1546
- this.render();
1516
+ this._renderHeaderMarkup();
1517
+ if (opts.updateRows) {
1518
+ this._updateRows();
1547
1519
  }
1548
1520
  }
1549
1521
  }
1550
1522
 
1551
- /** Render all rows that are visible in the viewport. */
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._disableUpdate) {
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 abolut positioned parent, so look at first col
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
- let wantHeight = this.element.clientHeight - headerHeight;
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({ 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
- });
1589
+ this.updateColumns({ updateRows: false });
1590
+
1591
+ this._updateRows({ newNodesOnly: newNodesOnly });
1592
+
1584
1593
  this._callEvent("update");
1585
1594
  }
1586
1595
 
1587
- /** Call callback(node) for all nodes in hierarchical order (depth-first).
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 children only.
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
- /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
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
- /** Call fn(node) for all nodes in vertical order, bottom up.
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.renderHeader();
1760
- // this.updateColumns({ render: false });
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(this._disableUpdateCount > 0);
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