wunderbaum 0.0.3 → 0.0.6

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
@@ -1,11 +1,12 @@
1
1
  /*!
2
2
  * wunderbaum.ts
3
3
  *
4
- * A tree control.
4
+ * A treegrid control.
5
5
  *
6
6
  * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
7
- * Released under the MIT license.
7
+ * https://github.com/mar10/wunderbaum
8
8
  *
9
+ * Released under the MIT license.
9
10
  * @version @VERSION
10
11
  * @date @DATE
11
12
  */
@@ -18,30 +19,43 @@ import { LoggerExtension } from "./wb_ext_logger";
18
19
  import { DndExtension } from "./wb_ext_dnd";
19
20
  import { GridExtension } from "./wb_ext_grid";
20
21
  import { ExtensionsDict, WunderbaumExtension } from "./wb_extension_base";
21
-
22
22
  import {
23
- NavigationMode,
23
+ ApplyCommandType,
24
24
  ChangeType,
25
- DEFAULT_DEBUGLEVEL,
25
+ ColumnDefinitionList,
26
26
  FilterModeType,
27
- makeNodeTitleStartMatcher,
28
27
  MatcherType,
29
- NavigationModeOption,
28
+ NavigationOptions,
30
29
  NodeStatusType,
30
+ NodeTypeDefinitions,
31
+ ScrollToOptions,
32
+ SetActiveOptions,
33
+ SetModifiedOptions,
34
+ SetStatusOptions,
35
+ TargetType as NodeRegion,
36
+ } from "./types";
37
+ import {
38
+ DEFAULT_DEBUGLEVEL,
39
+ makeNodeTitleStartMatcher,
31
40
  RENDER_MAX_PREFETCH,
32
41
  ROW_HEIGHT,
33
- TargetType as NodeRegion,
34
- ApplyCommandType,
35
42
  } from "./common";
36
43
  import { WunderbaumNode } from "./wb_node";
37
44
  import { Deferred } from "./deferred";
38
- import { DebouncedFunction, throttle } from "./debounce";
39
45
  import { EditExtension } from "./wb_ext_edit";
40
46
  import { WunderbaumOptions } from "./wb_options";
41
47
 
42
- // const class_prefix = "wb-";
43
- // const node_props: string[] = ["title", "key", "refKey"];
44
- // const MAX_CHANGED_NODES = 10;
48
+ class WbSystemRoot extends WunderbaumNode {
49
+ constructor(tree: Wunderbaum) {
50
+ super(tree, <WunderbaumNode>(<unknown>null), {
51
+ key: "__root__",
52
+ title: tree.id,
53
+ });
54
+ }
55
+ toString() {
56
+ return `WbSystemRoot@${this.key}<'${this.tree.id}'>`;
57
+ }
58
+ }
45
59
 
46
60
  /**
47
61
  * A persistent plain object or array.
@@ -50,6 +64,7 @@ import { WunderbaumOptions } from "./wb_options";
50
64
  */
51
65
  export class Wunderbaum {
52
66
  protected static sequence = 0;
67
+ protected enabled = true;
53
68
 
54
69
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
55
70
  public static version: string = "@VERSION"; // Set to semver by 'grunt release'
@@ -61,15 +76,15 @@ export class Wunderbaum {
61
76
  /** The `div` container element that was passed to the constructor. */
62
77
  public readonly element: HTMLDivElement;
63
78
  /** The `div.wb-header` element if any. */
64
- public readonly headerElement: HTMLDivElement | null;
79
+ public readonly headerElement: HTMLDivElement;
65
80
  /** The `div.wb-scroll-container` element that contains the `nodeListElement`. */
66
- public readonly scrollContainer: HTMLDivElement;
81
+ public readonly scrollContainerElement: HTMLDivElement;
67
82
  /** The `div.wb-node-list` element that contains all visible div.wb-row child elements. */
68
83
  public readonly nodeListElement: HTMLDivElement;
84
+ /** Contains additional data that was sent as response to an Ajax source load request. */
85
+ public readonly data: { [key: string]: any } = {};
69
86
 
70
- protected readonly _updateViewportThrottled: DebouncedFunction<
71
- (...args: any) => void
72
- >;
87
+ protected readonly _updateViewportThrottled: (...args: any) => void;
73
88
  protected extensionList: WunderbaumExtension[] = [];
74
89
  protected extensions: ExtensionsDict = {};
75
90
 
@@ -78,30 +93,25 @@ export class Wunderbaum {
78
93
 
79
94
  protected keyMap = new Map<string, WunderbaumNode>();
80
95
  protected refKeyMap = new Map<string, Set<WunderbaumNode>>();
81
- // protected viewNodes = new Set<WunderbaumNode>();
82
96
  protected treeRowCount = 0;
83
97
  protected _disableUpdateCount = 0;
84
98
 
85
- // protected eventHandlers : Array<function> = [];
86
-
87
99
  /** Currently active node if any. */
88
100
  public activeNode: WunderbaumNode | null = null;
89
101
  /** Current node hat has keyboard focus if any. */
90
102
  public focusNode: WunderbaumNode | null = null;
91
103
 
92
104
  /** Shared properties, referenced by `node.type`. */
93
- public types: { [key: string]: any } = {};
105
+ public types: NodeTypeDefinitions = {};
94
106
  /** List of column definitions. */
95
- public columns: any[] = [];
107
+ public columns: ColumnDefinitionList = []; // any[] = [];
96
108
 
97
109
  protected _columnsById: { [key: string]: any } = {};
98
110
  protected resizeObserver: ResizeObserver;
99
111
 
100
112
  // Modification Status
101
- // protected changedSince = 0;
102
- // protected changes = new Set<ChangeType>();
103
- // protected changedNodes = new Set<WunderbaumNode>();
104
113
  protected changeRedrawRequestPending = false;
114
+ protected changeScrollRequestPending = false;
105
115
 
106
116
  /** A Promise that is resolved when the tree was initialized (similar to `init(e)` event). */
107
117
  public readonly ready: Promise<any>;
@@ -117,7 +127,7 @@ export class Wunderbaum {
117
127
  /** @internal Use `setColumn()`/`getActiveColElem()`*/
118
128
  public activeColIdx = 0;
119
129
  /** @internal */
120
- public navMode = NavigationMode.row;
130
+ public _cellNavMode = false;
121
131
  /** @internal */
122
132
  public lastQuicksearchTime = 0;
123
133
  /** @internal */
@@ -130,22 +140,25 @@ export class Wunderbaum {
130
140
  let opts = (this.options = util.extend(
131
141
  {
132
142
  id: null,
133
- source: null, // URL for GET/PUT, ajax options, or callback
143
+ source: null, // URL for GET/PUT, Ajax options, or callback
134
144
  element: null, // <div class="wunderbaum">
135
145
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
136
146
  header: null, // Show/hide header (pass bool or string)
137
- headerHeightPx: ROW_HEIGHT,
147
+ // headerHeightPx: ROW_HEIGHT,
138
148
  rowHeightPx: ROW_HEIGHT,
139
149
  columns: null,
140
150
  types: null,
141
151
  // escapeTitles: true,
152
+ enabled: true,
153
+ fixedCol: false,
142
154
  showSpinner: false,
143
- checkbox: true,
155
+ checkbox: false,
144
156
  minExpandLevel: 0,
145
157
  updateThrottleWait: 200,
146
158
  skeleton: false,
159
+ connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
147
160
  // --- KeyNav ---
148
- navigationMode: NavigationModeOption.startRow,
161
+ navigationModeOption: null, // NavigationOptions.startRow,
149
162
  quicksearch: true,
150
163
  // --- Events ---
151
164
  change: util.noop,
@@ -189,10 +202,7 @@ export class Wunderbaum {
189
202
  });
190
203
 
191
204
  this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
192
- this.root = new WunderbaumNode(this, <WunderbaumNode>(<unknown>null), {
193
- key: "__root__",
194
- // title: "__root__",
195
- });
205
+ this.root = new WbSystemRoot(this);
196
206
 
197
207
  this._registerExtension(new KeynavExtension(this));
198
208
  this._registerExtension(new EditExtension(this));
@@ -201,41 +211,23 @@ export class Wunderbaum {
201
211
  this._registerExtension(new GridExtension(this));
202
212
  this._registerExtension(new LoggerExtension(this));
203
213
 
214
+ this._updateViewportThrottled = util.adaptiveThrottle(
215
+ this._updateViewportImmediately.bind(this),
216
+ {}
217
+ );
218
+
204
219
  // --- Evaluate options
205
220
  this.columns = opts.columns;
206
221
  delete opts.columns;
207
- if (!this.columns) {
208
- let defaultName = typeof opts.header === "string" ? opts.header : this.id;
209
- this.columns = [{ id: "*", title: defaultName, width: "*" }];
222
+ if (!this.columns || !this.columns.length) {
223
+ const title = typeof opts.header === "string" ? opts.header : this.id;
224
+ this.columns = [{ id: "*", title: title, width: "*" }];
210
225
  }
211
226
 
212
- this.types = opts.types || {};
213
- delete opts.types;
214
- // Convert `TYPE.classes` to a Set
215
- for (let t of Object.values(this.types) as any) {
216
- if (t.classes) {
217
- t.classes = util.toSet(t.classes);
218
- }
227
+ if (opts.types) {
228
+ this.setTypes(opts.types, true);
219
229
  }
220
-
221
- if (this.columns.length === 1) {
222
- opts.navigationMode = NavigationModeOption.row;
223
- }
224
-
225
- if (
226
- opts.navigationMode === NavigationModeOption.cell ||
227
- opts.navigationMode === NavigationModeOption.startCell
228
- ) {
229
- this.navMode = NavigationMode.cellNav;
230
- }
231
-
232
- this._updateViewportThrottled = throttle(
233
- () => {
234
- this._updateViewport();
235
- },
236
- opts.updateThrottleWait,
237
- { leading: true, trailing: true }
238
- );
230
+ delete opts.types;
239
231
 
240
232
  // --- Create Markup
241
233
  this.element = util.elemFromSelector(opts.element) as HTMLDivElement;
@@ -270,11 +262,14 @@ export class Wunderbaum {
270
262
  ) as HTMLDivElement;
271
263
  for (const colDiv of rowElement.querySelectorAll("div")) {
272
264
  this.columns.push({
273
- id: colDiv.dataset.id || null,
274
- text: "" + colDiv.textContent,
265
+ id: colDiv.dataset.id || `col_${this.columns.length}`,
266
+ // id: colDiv.dataset.id || null,
267
+ title: "" + colDiv.textContent,
268
+ // text: "" + colDiv.textContent,
269
+ width: "*", // TODO: read from header span
275
270
  });
276
271
  }
277
- } else if (wantHeader) {
272
+ } else {
278
273
  // We need a row div, the rest will be computed from `this.columns`
279
274
  const coldivs = "<span class='wb-col'></span>".repeat(
280
275
  this.columns.length
@@ -285,9 +280,13 @@ export class Wunderbaum {
285
280
  ${coldivs}
286
281
  </div>
287
282
  </div>`;
288
- // this.updateColumns({ render: false });
289
- } else {
290
- this.element.innerHTML = "";
283
+
284
+ if (!wantHeader) {
285
+ const he = this.element.querySelector(
286
+ "div.wb-header"
287
+ ) as HTMLDivElement;
288
+ he.style.display = "none";
289
+ }
291
290
  }
292
291
 
293
292
  //
@@ -295,22 +294,27 @@ export class Wunderbaum {
295
294
  <div class="wb-scroll-container">
296
295
  <div class="wb-node-list"></div>
297
296
  </div>`;
298
- this.scrollContainer = this.element.querySelector(
297
+ this.scrollContainerElement = this.element.querySelector(
299
298
  "div.wb-scroll-container"
300
299
  ) as HTMLDivElement;
301
- this.nodeListElement = this.scrollContainer.querySelector(
300
+ this.nodeListElement = this.scrollContainerElement.querySelector(
302
301
  "div.wb-node-list"
303
302
  ) as HTMLDivElement;
304
303
  this.headerElement = this.element.querySelector(
305
304
  "div.wb-header"
306
305
  ) as HTMLDivElement;
307
306
 
308
- if (this.columns.length > 1) {
309
- this.element.classList.add("wb-grid");
310
- }
307
+ this.element.classList.toggle("wb-grid", this.columns.length > 1);
311
308
 
312
309
  this._initExtensions();
313
310
 
311
+ // --- apply initial options
312
+ ["enabled", "fixedCol"].forEach((optName) => {
313
+ if (opts[optName] != null) {
314
+ this.setOption(optName, opts[optName]);
315
+ }
316
+ });
317
+
314
318
  // --- Load initial data
315
319
  if (opts.source) {
316
320
  if (opts.showSpinner) {
@@ -319,6 +323,16 @@ export class Wunderbaum {
319
323
  }
320
324
  this.load(opts.source)
321
325
  .then(() => {
326
+ // The source may have defined columns, so we may adjust the nav mode
327
+ if (opts.navigationModeOption == null) {
328
+ if (this.isGrid()) {
329
+ this.setNavigationOption(NavigationOptions.cell);
330
+ } else {
331
+ this.setNavigationOption(NavigationOptions.row);
332
+ }
333
+ } else {
334
+ this.setNavigationOption(opts.navigationModeOption);
335
+ }
322
336
  readyDeferred.resolve();
323
337
  })
324
338
  .catch((error) => {
@@ -332,27 +346,41 @@ export class Wunderbaum {
332
346
  readyDeferred.resolve();
333
347
  }
334
348
 
335
- // TODO: This is sometimes required, because this.element.clientWidth
336
- // has a wrong value at start???
337
- setTimeout(() => {
338
- this.updateViewport();
339
- }, 50);
349
+ // Async mode is sometimes required, because this.element.clientWidth
350
+ // has a wrong value at start???
351
+ this.setModified(ChangeType.any);
340
352
 
341
353
  // --- Bind listeners
342
- this.scrollContainer.addEventListener("scroll", (e: Event) => {
354
+ this.element.addEventListener("scroll", (e: Event) => {
355
+ // this.log("scroll", e);
343
356
  this.setModified(ChangeType.vscroll);
344
357
  });
358
+ // this.scrollContainerElement.addEventListener("scroll", (e: Event) => {
359
+ // this.log("scroll", e)
360
+ // this.setModified(ChangeType.vscroll);
361
+ // });
345
362
 
346
363
  this.resizeObserver = new ResizeObserver((entries) => {
347
364
  this.setModified(ChangeType.vscroll);
348
- console.log("ResizeObserver: Size changed", entries);
365
+ // this.log("ResizeObserver: Size changed", entries);
349
366
  });
350
367
  this.resizeObserver.observe(this.element);
351
368
 
352
369
  util.onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
353
370
  const info = Wunderbaum.getEventInfo(e);
354
371
  const node = info.node;
355
-
372
+ // this.log("click", info, e);
373
+
374
+ // if( (e.target as HTMLElement).matches("input[type=checkbox]")){
375
+ // // Click on an embedded checkbox triggers a change event.
376
+ // // We return here, before the `setActive()` performs a render
377
+ // this.log("click - cb", info, e);
378
+ // // e.preventDefault()
379
+ // setTimeout(()=>{
380
+ // // (e.target as HTMLElement).click()
381
+ // }, 50)
382
+ // // return
383
+ // }
356
384
  if (
357
385
  this._callEvent("click", { event: e, node: node, info: info }) === false
358
386
  ) {
@@ -384,17 +412,17 @@ export class Wunderbaum {
384
412
  node.setSelected(!node.isSelected());
385
413
  }
386
414
  }
387
- // if(e.target.classList.)
388
- // this.log("click", info);
389
415
  this.lastClickTime = Date.now();
390
416
  });
391
417
 
392
418
  util.onEvent(this.element, "keydown", (e) => {
393
419
  const info = Wunderbaum.getEventInfo(e);
394
420
  const eventName = util.eventToString(e);
421
+ const node = info.node || this.getFocusNode();
422
+
395
423
  this._callHook("onKeyEvent", {
396
424
  event: e,
397
- node: info.node,
425
+ node: node,
398
426
  info: info,
399
427
  eventName: eventName,
400
428
  });
@@ -582,17 +610,17 @@ export class Wunderbaum {
582
610
  * tree._callEvent("edit.beforeEdit", {foo: 42})
583
611
  * ```
584
612
  */
585
- _callEvent(name: string, extra?: any): any {
586
- const [p, n] = name.split(".");
613
+ _callEvent(type: string, extra?: any): any {
614
+ const [p, n] = type.split(".");
587
615
  const opts = this.options as any;
588
616
  const func = n ? opts[p][n] : opts[p];
589
617
  if (func) {
590
618
  return func.call(
591
619
  this,
592
- util.extend({ name: name, tree: this, util: this._util }, extra)
620
+ util.extend({ type: type, tree: this, util: this._util }, extra)
593
621
  );
594
622
  // } else {
595
- // this.logError(`Triggering undefined event '${name}'.`)
623
+ // this.logError(`Triggering undefined event '${type}'.`)
596
624
  }
597
625
  }
598
626
 
@@ -610,35 +638,33 @@ export class Wunderbaum {
610
638
  }
611
639
 
612
640
  /** Return the topmost visible node in the viewport. */
613
- protected _firstNodeInView(complete = true) {
641
+ getTopmostVpNode(complete = true) {
642
+ const gracePx = 1; // ignore subpixel scrolling
643
+ const scrollParent = this.element;
644
+ // const headerHeight = this.headerElement.clientHeight; // May be 0
645
+ const scrollTop = scrollParent.scrollTop; // + headerHeight;
614
646
  let topIdx: number;
615
- const gracePy = 1; // ignore subpixel scrolling
616
647
 
617
648
  if (complete) {
618
- topIdx = Math.ceil(
619
- (this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT
620
- );
649
+ topIdx = Math.ceil((scrollTop - gracePx) / ROW_HEIGHT);
621
650
  } else {
622
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
651
+ topIdx = Math.floor(scrollTop / ROW_HEIGHT);
623
652
  }
624
653
  return this._getNodeByRowIdx(topIdx)!;
625
654
  }
626
655
 
627
656
  /** Return the lowest visible node in the viewport. */
628
- protected _lastNodeInView(complete = true) {
657
+ getLowestVpNode(complete = true) {
658
+ const scrollParent = this.element;
659
+ const headerHeight = this.headerElement.clientHeight; // May be 0
660
+ const scrollTop = scrollParent.scrollTop;
661
+ const clientHeight = scrollParent.clientHeight - headerHeight;
629
662
  let bottomIdx: number;
663
+
630
664
  if (complete) {
631
- bottomIdx =
632
- Math.floor(
633
- (this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
634
- ROW_HEIGHT
635
- ) - 1;
665
+ bottomIdx = Math.floor((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
636
666
  } else {
637
- bottomIdx =
638
- Math.ceil(
639
- (this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
640
- ROW_HEIGHT
641
- ) - 1;
667
+ bottomIdx = Math.ceil((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
642
668
  }
643
669
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
644
670
  return this._getNodeByRowIdx(bottomIdx)!;
@@ -868,7 +894,7 @@ export class Wunderbaum {
868
894
  * Return `tree.option.NAME` (also resolving if this is a callback).
869
895
  *
870
896
  * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
871
- * to consider `node.NAME` setting and `tree.types[node.type].NAME`.
897
+ * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`.
872
898
  *
873
899
  * @param name option name (use dot notation to access extension option, e.g.
874
900
  * `filter.mode`)
@@ -889,36 +915,52 @@ export class Wunderbaum {
889
915
  value = value({ type: "resolve", tree: this });
890
916
  }
891
917
  // Use value from value options dict, fallback do default
918
+ // console.info(name, value, opts)
892
919
  return value ?? defaultValue;
893
920
  }
894
921
 
895
922
  /**
896
- *
897
- * @param name
898
- * @param value
923
+ * Set tree option.
924
+ * Use dot notation to set plugin option, e.g. "filter.mode".
899
925
  */
900
926
  setOption(name: string, value: any): void {
901
- if (name.indexOf(".") === -1) {
902
- (this.options as any)[name] = value;
903
- // switch (name) {
904
- // case value:
905
- // break;
906
- // default:
907
- // break;
908
- // }
927
+ // this.log(`setOption(${name}, ${value})`);
928
+ if (name.indexOf(".") >= 0) {
929
+ const parts = name.split(".");
930
+ const ext = this.extensions[parts[0]];
931
+ ext!.setPluginOption(parts[1], value);
909
932
  return;
910
933
  }
911
- const parts = name.split(".");
912
- const ext = this.extensions[parts[0]];
913
- ext!.setPluginOption(parts[1], value);
934
+ (this.options as any)[name] = value;
935
+ switch (name) {
936
+ case "checkbox":
937
+ this.setModified(ChangeType.any, { removeMarkup: true });
938
+ break;
939
+ case "enabled":
940
+ this.setEnabled(!!value);
941
+ break;
942
+ case "fixedCol":
943
+ this.element.classList.toggle("wb-fixed-col", !!value);
944
+ break;
945
+ }
914
946
  }
915
947
 
916
- /**Return true if the tree (or one of its nodes) has the input focus. */
948
+ /** Return true if the tree (or one of its nodes) has the input focus. */
917
949
  hasFocus() {
918
950
  return this.element.contains(document.activeElement);
919
951
  }
920
952
 
921
- /** Run code, but defer `updateViewport()` until done. */
953
+ /**
954
+ * Return true if the tree displays a header. Grids have a header unless the
955
+ * `header` option is set to `false`. Plain trees have a header if the `header`
956
+ * option is a string or `true`.
957
+ */
958
+ hasHeader() {
959
+ const header = this.options.header;
960
+ return this.isGrid() ? header !== false : !!header;
961
+ }
962
+
963
+ /** Run code, but defer rendering of viewport until done. */
922
964
  runWithoutUpdate(func: () => any, hint = null): void {
923
965
  try {
924
966
  this.enableUpdate(false);
@@ -942,6 +984,18 @@ export class Wunderbaum {
942
984
  }
943
985
  }
944
986
 
987
+ /** Recursively select all nodes. */
988
+ selectAll(flag: boolean = true) {
989
+ try {
990
+ this.enableUpdate(false);
991
+ this.visit((node) => {
992
+ node.setSelected(flag);
993
+ });
994
+ } finally {
995
+ this.enableUpdate(true);
996
+ }
997
+ }
998
+
945
999
  /** Return the number of nodes in the data model.*/
946
1000
  count(visible = false): number {
947
1001
  if (visible) {
@@ -987,6 +1041,18 @@ export class Wunderbaum {
987
1041
  return this.root.findFirst(match);
988
1042
  }
989
1043
 
1044
+ /**
1045
+ * Find first node that matches condition.
1046
+ *
1047
+ * @param match title string to search for, or a
1048
+ * callback function that returns `true` if a node is matched.
1049
+ * @see {@link WunderbaumNode.findFirst}
1050
+ *
1051
+ */
1052
+ findKey(key: string): WunderbaumNode | undefined {
1053
+ return this.keyMap.get(key);
1054
+ }
1055
+
990
1056
  /**
991
1057
  * Find the next visible node that starts with `match`, starting at `startNode`
992
1058
  * and wrap-around at the end.
@@ -1037,7 +1103,9 @@ export class Wunderbaum {
1037
1103
  */
1038
1104
  findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) {
1039
1105
  let res = null;
1040
- const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
1106
+ const pageSize = Math.floor(
1107
+ this.scrollContainerElement.clientHeight / ROW_HEIGHT
1108
+ );
1041
1109
 
1042
1110
  switch (where) {
1043
1111
  case "parent":
@@ -1094,7 +1162,7 @@ export class Wunderbaum {
1094
1162
  res = this._getNextNodeInView(node);
1095
1163
  break;
1096
1164
  case "pageDown":
1097
- const bottomNode = this._lastNodeInView();
1165
+ const bottomNode = this.getLowestVpNode();
1098
1166
  // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
1099
1167
 
1100
1168
  if (node._rowIdx! < bottomNode._rowIdx!) {
@@ -1107,7 +1175,7 @@ export class Wunderbaum {
1107
1175
  if (node._rowIdx === 0) {
1108
1176
  res = node;
1109
1177
  } else {
1110
- const topNode = this._firstNodeInView();
1178
+ const topNode = this.getTopmostVpNode();
1111
1179
  // this.logDebug(`${where}(${node}) -> ${topNode}`);
1112
1180
 
1113
1181
  if (node._rowIdx! > topNode._rowIdx!) {
@@ -1195,6 +1263,9 @@ export class Wunderbaum {
1195
1263
  parentCol
1196
1264
  );
1197
1265
  res.colIdx = idx;
1266
+ } else if (cl.contains("wb-row")) {
1267
+ // Plain tree
1268
+ res.region = NodeRegion.title;
1198
1269
  } else {
1199
1270
  // Somewhere near the title
1200
1271
  if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
@@ -1205,7 +1276,7 @@ export class Wunderbaum {
1205
1276
  if (res.colIdx === -1) {
1206
1277
  res.colIdx = 0;
1207
1278
  }
1208
- res.colDef = tree?.columns[res.colIdx];
1279
+ res.colDef = <any>tree?.columns[res.colIdx];
1209
1280
  res.colDef != null ? (res.colId = (<any>res.colDef).id) : 0;
1210
1281
  // this.log("Event", event, res);
1211
1282
  return res;
@@ -1225,7 +1296,7 @@ export class Wunderbaum {
1225
1296
  * @internal
1226
1297
  */
1227
1298
  toString() {
1228
- return "Wunderbaum<'" + this.id + "'>";
1299
+ return `Wunderbaum<'${this.id}'>`;
1229
1300
  }
1230
1301
 
1231
1302
  /** Return true if any node is currently in edit-title mode. */
@@ -1302,72 +1373,130 @@ export class Wunderbaum {
1302
1373
  }
1303
1374
 
1304
1375
  /**
1305
- * Make sure that this node is scrolled into the viewport.
1306
- *
1307
- * @param {boolean | PlainObject} [effects=false] animation options.
1308
- * @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
1309
- * this node will remain visible in
1310
- * any case, even if `this` is outside the scroll pane.
1376
+ * Make sure that this node is vertically scrolled into the viewport.
1311
1377
  */
1312
- scrollTo(opts: any) {
1313
- const MARGIN = 1;
1314
- const node = opts.node || this.getActiveNode();
1315
- util.assert(node._rowIdx != null);
1316
- const curTop = this.scrollContainer.scrollTop;
1317
- const height = this.scrollContainer.clientHeight;
1318
- const nodeOfs = node._rowIdx * ROW_HEIGHT;
1319
- let newTop;
1320
-
1321
- if (nodeOfs > curTop) {
1322
- if (nodeOfs + ROW_HEIGHT < curTop + height) {
1378
+ scrollTo(nodeOrOpts: ScrollToOptions | WunderbaumNode) {
1379
+ const PADDING = 2; // leave some pixels between viewport bounds
1380
+
1381
+ let node;
1382
+ WunderbaumNode;
1383
+ let opts: ScrollToOptions | undefined;
1384
+
1385
+ if (nodeOrOpts instanceof WunderbaumNode) {
1386
+ node = nodeOrOpts;
1387
+ } else {
1388
+ opts = nodeOrOpts;
1389
+ node = opts.node;
1390
+ }
1391
+ util.assert(node && node._rowIdx != null);
1392
+
1393
+ const scrollParent = this.element;
1394
+ const headerHeight = this.headerElement.clientHeight; // May be 0
1395
+ const scrollTop = scrollParent.scrollTop;
1396
+ const vpHeight = scrollParent.clientHeight;
1397
+ const rowTop = node._rowIdx! * ROW_HEIGHT + headerHeight;
1398
+ const vpTop = headerHeight;
1399
+ const vpRowTop = rowTop - scrollTop;
1400
+ const vpRowBottom = vpRowTop + ROW_HEIGHT;
1401
+
1402
+ // this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts );
1403
+ let newScrollTop: number | null = null;
1404
+ if (vpRowTop >= vpTop) {
1405
+ if (vpRowBottom <= vpHeight) {
1323
1406
  // Already in view
1407
+ // this.log("Already in view");
1324
1408
  } else {
1325
1409
  // Node is below viewport
1326
- newTop = nodeOfs - height + ROW_HEIGHT - MARGIN;
1410
+ // this.log("Below viewport");
1411
+ newScrollTop = rowTop + ROW_HEIGHT - vpHeight + PADDING; // leave some pixels between vieeport bounds
1327
1412
  }
1328
- } else if (nodeOfs < curTop) {
1413
+ } else {
1329
1414
  // Node is above viewport
1330
- newTop = nodeOfs + MARGIN;
1415
+ // this.log("Above viewport");
1416
+ newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between vieeport bounds
1331
1417
  }
1332
- if (newTop != null) {
1333
- this.log(
1334
- "scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop,
1335
- height
1336
- );
1337
- this.scrollContainer.scrollTop = newTop;
1338
- this.setModified(ChangeType.vscroll);
1418
+ if (newScrollTop != null) {
1419
+ this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`);
1420
+ scrollParent.scrollTop = newScrollTop;
1421
+ // this.setModified(ChangeType.vscroll);
1339
1422
  }
1340
1423
  }
1341
1424
 
1425
+ /**
1426
+ * Make sure that this node is horizontally scrolled into the viewport.
1427
+ * Called by {@link setColumn}.
1428
+ */
1429
+ protected scrollToHorz() {
1430
+ // const PADDING = 1;
1431
+ const fixedWidth = this.columns[0]._widthPx!;
1432
+ const vpWidth = this.element.clientWidth;
1433
+ const scrollLeft = this.element.scrollLeft;
1434
+ // if (scrollLeft <= 0) {
1435
+ // return; // Not scrolled horizontally: Nothing to do
1436
+ // }
1437
+ const colElem = this.getActiveColElem()!;
1438
+ const colLeft = Number.parseInt(colElem?.style.left, 10);
1439
+ const colRight = colLeft + Number.parseInt(colElem?.style.width, 10);
1440
+ let newLeft = scrollLeft;
1441
+
1442
+ if (colLeft - scrollLeft < fixedWidth) {
1443
+ // The current column is scrolled behind the left fixed column
1444
+ newLeft = colLeft - fixedWidth;
1445
+ } else if (colRight - scrollLeft > vpWidth) {
1446
+ // The current column is scrolled outside the right side
1447
+ newLeft = colRight - vpWidth;
1448
+ }
1449
+ // util.assert(node._rowIdx != null);
1450
+ // const curLeft = this.scrollContainer.scrollLeft;
1451
+ this.log(
1452
+ `scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`
1453
+ );
1454
+ // const nodeOfs = node._rowIdx * ROW_HEIGHT;
1455
+ // let newLeft;
1456
+
1457
+ this.element.scrollLeft = newLeft;
1458
+ // this.setModified(ChangeType.vscroll);
1459
+ // }
1460
+ }
1342
1461
  /**
1343
1462
  * Set column #colIdx to 'active'.
1344
1463
  *
1345
1464
  * 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.
1465
+ * Available in cell-nav mode only.
1347
1466
  */
1348
1467
  setColumn(colIdx: number) {
1349
- util.assert(this.navMode !== NavigationMode.row);
1468
+ util.assert(this.isCellNav());
1350
1469
  util.assert(0 <= colIdx && colIdx < this.columns.length);
1351
1470
  this.activeColIdx = colIdx;
1352
- // node.setActive(true, { column: tree.activeColIdx + 1 });
1353
- this.setModified(ChangeType.row, this.activeNode);
1471
+
1354
1472
  // Update `wb-active` class for all headers
1355
- if (this.headerElement) {
1473
+ if (this.hasHeader()) {
1356
1474
  for (let rowDiv of this.headerElement.children) {
1357
- // for (let rowDiv of document.querySelector("div.wb-header").children) {
1358
1475
  let i = 0;
1359
1476
  for (let colDiv of rowDiv.children) {
1360
1477
  (colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
1361
1478
  }
1362
1479
  }
1363
1480
  }
1364
- // Update `wb-active` class for all cell divs
1481
+
1482
+ this.activeNode?.setModified(ChangeType.status);
1483
+
1484
+ // Update `wb-active` class for all cell spans
1365
1485
  for (let rowDiv of this.nodeListElement.children) {
1366
1486
  let i = 0;
1367
1487
  for (let colDiv of rowDiv.children) {
1368
1488
  (colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
1369
1489
  }
1370
1490
  }
1491
+ // Vertical scroll into view
1492
+ // if (this.options.fixedCol) {
1493
+ this.scrollToHorz();
1494
+ // }
1495
+ }
1496
+
1497
+ /** Set or remove keybaord focus to the tree container. */
1498
+ setActiveNode(key: string, flag: boolean = true, options?: SetActiveOptions) {
1499
+ this.findKey(key)?.setActive(flag, options);
1371
1500
  }
1372
1501
 
1373
1502
  /** Set or remove keybaord focus to the tree container. */
@@ -1388,7 +1517,7 @@ export class Wunderbaum {
1388
1517
  setModified(
1389
1518
  change: ChangeType,
1390
1519
  node?: WunderbaumNode | any,
1391
- options?: any
1520
+ options?: SetModifiedOptions
1392
1521
  ): void {
1393
1522
  if (this._disableUpdateCount) {
1394
1523
  // Assuming that we redraw all when enableUpdate() is re-enabled.
@@ -1402,73 +1531,159 @@ export class Wunderbaum {
1402
1531
  options = node;
1403
1532
  }
1404
1533
  const immediate = !!util.getOption(options, "immediate");
1534
+ const removeMarkup = !!util.getOption(options, "removeMarkup");
1535
+ if (removeMarkup) {
1536
+ this.visit((n) => {
1537
+ n.removeMarkup();
1538
+ });
1539
+ }
1405
1540
 
1541
+ let callUpdate = false;
1406
1542
  switch (change) {
1407
1543
  case ChangeType.any:
1408
1544
  case ChangeType.structure:
1409
1545
  case ChangeType.header:
1410
1546
  this.changeRedrawRequestPending = true;
1411
- this.updateViewport(immediate);
1547
+ callUpdate = true;
1412
1548
  break;
1413
1549
  case ChangeType.vscroll:
1414
- this.updateViewport(immediate);
1550
+ this.changeScrollRequestPending = true;
1551
+ callUpdate = true;
1415
1552
  break;
1416
1553
  case ChangeType.row:
1554
+ case ChangeType.data:
1417
1555
  case ChangeType.status:
1418
- // Single nodes are immedialtely updated if already inside the viewport
1556
+ // Single nodes are immediately updated if already inside the viewport
1419
1557
  // (otherwise we can ignore)
1420
1558
  if (node._rowElem) {
1421
- node.render();
1559
+ node.render({ change: change });
1422
1560
  }
1423
1561
  break;
1424
1562
  default:
1425
1563
  util.error(`Invalid change type ${change}`);
1426
1564
  }
1565
+ if (callUpdate) {
1566
+ if (immediate) {
1567
+ this._updateViewportImmediately();
1568
+ } else {
1569
+ this._updateViewportThrottled();
1570
+ }
1571
+ }
1572
+ }
1573
+
1574
+ /** Disable mouse and keyboard interaction (return prev. state). */
1575
+ setEnabled(flag: boolean = true): boolean {
1576
+ const prev = this.enabled;
1577
+ this.enabled = !!flag;
1578
+ this.element.classList.toggle("wb-disabled", !flag);
1579
+ return prev;
1580
+ }
1581
+
1582
+ /** Return false if tree is disabled. */
1583
+ isEnabled(): boolean {
1584
+ return this.enabled;
1585
+ }
1586
+
1587
+ /** Return true if tree has more than one column, i.e. has additional data columns. */
1588
+ isGrid(): boolean {
1589
+ return this.columns && this.columns.length > 1;
1590
+ }
1591
+
1592
+ /** Return true if cell-navigation mode is acive. */
1593
+ isCellNav(): boolean {
1594
+ return !!this._cellNavMode;
1595
+ }
1596
+ /** Return true if row-navigation mode is acive. */
1597
+ isRowNav(): boolean {
1598
+ return !this._cellNavMode;
1427
1599
  }
1428
1600
 
1429
1601
  /** 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) {
1602
+ setCellNav(flag: boolean = true) {
1603
+ const prev = this._cellNavMode;
1604
+ // if (flag === prev) {
1605
+ // return;
1606
+ // }
1607
+ this._cellNavMode = !!flag;
1608
+ if (flag && !prev) {
1609
+ // switch from row to cell mode
1610
+ this.setColumn(0);
1611
+ }
1612
+ this.element.classList.toggle("wb-cell-mode", flag);
1613
+ this.activeNode?.setModified(ChangeType.status);
1614
+ }
1615
+
1616
+ /** Set the tree's navigation mode option. */
1617
+ setNavigationOption(mode: NavigationOptions, reset = false) {
1618
+ if (!this.isGrid() && mode !== NavigationOptions.row) {
1619
+ this.logWarn("Plain trees only support row navigation mode.");
1434
1620
  return;
1435
1621
  }
1436
- const prevMode = this.navMode;
1437
- const cellMode = mode !== NavigationMode.row;
1622
+ this.options.navigationModeOption = mode;
1438
1623
 
1439
- this.navMode = mode;
1440
- if (cellMode && prevMode === NavigationMode.row) {
1441
- this.setColumn(0);
1624
+ switch (mode) {
1625
+ case NavigationOptions.cell:
1626
+ this.setCellNav(true);
1627
+ break;
1628
+ case NavigationOptions.row:
1629
+ this.setCellNav(false);
1630
+ break;
1631
+ case NavigationOptions.startCell:
1632
+ if (reset) {
1633
+ this.setCellNav(true);
1634
+ }
1635
+ break;
1636
+ case NavigationOptions.startRow:
1637
+ if (reset) {
1638
+ this.setCellNav(false);
1639
+ }
1640
+ break;
1641
+ default:
1642
+ util.error(`Invalid mode '${mode}'`);
1442
1643
  }
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);
1449
1644
  }
1450
1645
 
1451
1646
  /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
1452
1647
  setStatus(
1453
1648
  status: NodeStatusType,
1454
- message?: string,
1455
- details?: string
1649
+ options?: SetStatusOptions
1456
1650
  ): WunderbaumNode | null {
1457
- return this.root.setStatus(status, message, details);
1651
+ return this.root.setStatus(status, options);
1652
+ }
1653
+ /** Add or redefine node type definitions. */
1654
+ setTypes(types: any, replace = true) {
1655
+ util.assert(util.isPlainObject(types));
1656
+ if (replace) {
1657
+ this.types = types;
1658
+ } else {
1659
+ util.extend(this.types, types);
1660
+ }
1661
+ // Convert `TYPE.classes` to a Set
1662
+ for (let t of Object.values(this.types) as any) {
1663
+ if (t.classes) {
1664
+ t.classes = util.toSet(t.classes);
1665
+ }
1666
+ }
1458
1667
  }
1459
-
1460
1668
  /** Update column headers and width. */
1461
1669
  updateColumns(opts?: any) {
1462
1670
  opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
1463
- const minWidth = 4;
1671
+ const defaultMinWidth = 4;
1464
1672
  const vpWidth = this.element.clientWidth;
1673
+ const isGrid = this.isGrid();
1674
+
1675
+ let totalWidth = 0;
1465
1676
  let totalWeight = 0;
1466
1677
  let fixedWidth = 0;
1467
-
1468
1678
  let modified = false;
1469
1679
 
1680
+ this.element.classList.toggle("wb-grid", isGrid);
1681
+ if (!isGrid && this.isCellNav()) {
1682
+ this.setCellNav(false);
1683
+ }
1684
+
1470
1685
  if (opts.calculateCols) {
1471
- // Gather width requests
1686
+ // Gather width definitions
1472
1687
  this._columnsById = {};
1473
1688
  for (let col of this.columns) {
1474
1689
  this._columnsById[<string>col.id] = col;
@@ -1489,7 +1704,7 @@ export class Wunderbaum {
1489
1704
  }
1490
1705
  fixedWidth += px;
1491
1706
  } else {
1492
- util.error("Invalid column width: " + cw);
1707
+ util.error(`Invalid column width: ${cw}`);
1493
1708
  }
1494
1709
  }
1495
1710
  // Share remaining space between non-fixed columns
@@ -1497,7 +1712,17 @@ export class Wunderbaum {
1497
1712
  let ofsPx = 0;
1498
1713
 
1499
1714
  for (let col of this.columns) {
1715
+ let minWidth: number;
1716
+
1500
1717
  if (col._weight) {
1718
+ const cmw = col.minWidth;
1719
+ if (typeof cmw === "number") {
1720
+ minWidth = cmw;
1721
+ } else if (typeof cmw === "string" && cmw.endsWith("px")) {
1722
+ minWidth = parseFloat(cmw.slice(0, -2));
1723
+ } else {
1724
+ minWidth = defaultMinWidth;
1725
+ }
1501
1726
  const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
1502
1727
  if (col._widthPx != px) {
1503
1728
  modified = true;
@@ -1505,9 +1730,17 @@ export class Wunderbaum {
1505
1730
  }
1506
1731
  }
1507
1732
  col._ofsPx = ofsPx;
1508
- ofsPx += col._widthPx;
1733
+ ofsPx += col._widthPx!;
1509
1734
  }
1735
+ totalWidth = ofsPx;
1510
1736
  }
1737
+ // if (this.options.fixedCol) {
1738
+ // 'position: fixed' requires that the content has the correct size
1739
+ const tw = `${totalWidth}px`;
1740
+ this.headerElement.style.width = tw;
1741
+ this.scrollContainerElement!.style.width = tw;
1742
+ // }
1743
+
1511
1744
  // Every column has now a calculated `_ofsPx` and `_widthPx`
1512
1745
  // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
1513
1746
  // console.trace();
@@ -1524,108 +1757,138 @@ export class Wunderbaum {
1524
1757
  * @internal
1525
1758
  */
1526
1759
  protected _renderHeaderMarkup() {
1527
- if (!this.headerElement) {
1760
+ util.assert(this.headerElement);
1761
+ const wantHeader = this.hasHeader();
1762
+ util.setElemDisplay(this.headerElement, wantHeader);
1763
+ if (!wantHeader) {
1528
1764
  return;
1529
1765
  }
1766
+ const colCount = this.columns.length;
1530
1767
  const headerRow = this.headerElement.querySelector(".wb-row")!;
1531
1768
  util.assert(headerRow);
1532
- headerRow.innerHTML = "<span class='wb-col'></span>".repeat(
1533
- this.columns.length
1534
- );
1769
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(colCount);
1535
1770
 
1536
- for (let i = 0; i < this.columns.length; i++) {
1771
+ for (let i = 0; i < colCount; i++) {
1537
1772
  const col = this.columns[i];
1538
1773
  const colElem = <HTMLElement>headerRow.children[i];
1539
1774
 
1540
1775
  colElem.style.left = col._ofsPx + "px";
1541
1776
  colElem.style.width = col._widthPx + "px";
1542
- // colElem.textContent = col.title || col.id;
1777
+
1543
1778
  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>`;
1779
+ let tooltip = "";
1780
+ if (col.tooltip) {
1781
+ tooltip = util.escapeTooltip(col.tooltip);
1782
+ tooltip = ` title="${tooltip}"`;
1783
+ }
1784
+ let resizer = "";
1785
+ if (i < colCount - 1) {
1786
+ resizer = '<span class="wb-col-resizer"></span>';
1787
+ }
1788
+ colElem.innerHTML = `<span class="wb-col-title"${tooltip}>${title}</span>${resizer}`;
1546
1789
  }
1547
1790
  }
1548
1791
 
1549
- /** Render header and all rows that are visible in the viewport (async, throttled). */
1550
- updateViewport(immediate = false) {
1551
- // Call the `throttle` wrapper for `this._updateViewport()` which will
1552
- // execute immediately on the leading edge of a sequence:
1553
- this._updateViewportThrottled();
1554
- if (immediate) {
1555
- this._updateViewportThrottled.flush();
1792
+ /**
1793
+ * Render pending changes that were scheduled using {@link WunderbaumNode.setModified} if any.
1794
+ *
1795
+ * This is hardly ever neccessary, since we normally either
1796
+ * - call `setModified(ChangeType.TYPE)` (async, throttled), or
1797
+ * - call `setModified(ChangeType.TYPE, {immediate: true})` (synchronous)
1798
+ *
1799
+ * `updatePendingModifications()` will only force immediate execution of
1800
+ * pending async changes if any.
1801
+ */
1802
+ updatePendingModifications() {
1803
+ if (this.changeRedrawRequestPending || this.changeScrollRequestPending) {
1804
+ this._updateViewportImmediately();
1556
1805
  }
1557
1806
  }
1558
1807
 
1559
1808
  /**
1560
1809
  * 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
1810
  * It calls `updateColumns()` and `_updateRows()`.
1811
+ *
1812
+ * This protected method should not be called directly but via
1813
+ * {@link WunderbaumNode.setModified}`, {@link Wunderbaum.setModified},
1814
+ * or {@link Wunderbaum.updatePendingModifications}.
1564
1815
  * @internal
1565
1816
  */
1566
- protected _updateViewport() {
1817
+ protected _updateViewportImmediately() {
1567
1818
  if (this._disableUpdateCount) {
1568
1819
  this.log(
1569
- `IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`
1820
+ `IGNORED _updateViewportImmediately() disable level: ${this._disableUpdateCount}`
1570
1821
  );
1571
1822
  return;
1572
1823
  }
1573
1824
  const newNodesOnly = !this.changeRedrawRequestPending;
1574
1825
  this.changeRedrawRequestPending = false;
1826
+ this.changeScrollRequestPending = false;
1575
1827
 
1576
- let height = this.scrollContainer.clientHeight;
1828
+ let height = this.scrollContainerElement.clientHeight;
1577
1829
  // We cannot get the height for absolute positioned parent, so look at first col
1578
1830
  // let headerHeight = this.headerElement.clientHeight
1579
1831
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
1580
- const headerHeight = this.options.headerHeightPx;
1832
+ // const headerHeight = this.options.headerHeightPx;
1833
+ const headerHeight = this.headerElement.clientHeight; // May be 0
1581
1834
  const wantHeight = this.element.clientHeight - headerHeight;
1582
1835
 
1583
1836
  if (Math.abs(height - wantHeight) > 1.0) {
1584
1837
  // this.log("resize", height, wantHeight);
1585
- this.scrollContainer.style.height = wantHeight + "px";
1838
+ this.scrollContainerElement.style.height = wantHeight + "px";
1586
1839
  height = wantHeight;
1587
1840
  }
1841
+ // console.profile(`_updateViewportImmediately()`)
1588
1842
 
1589
1843
  this.updateColumns({ updateRows: false });
1590
1844
 
1591
1845
  this._updateRows({ newNodesOnly: newNodesOnly });
1592
1846
 
1847
+ // console.profileEnd(`_updateViewportImmediately()`)
1848
+
1849
+ if (this.options.connectTopBreadcrumb) {
1850
+ let path = this.getTopmostVpNode(true)?.getPath(false, "title", " > ");
1851
+ path = path ? path + " >" : "";
1852
+ this.options.connectTopBreadcrumb.textContent = path;
1853
+ }
1593
1854
  this._callEvent("update");
1594
1855
  }
1595
1856
 
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
- }
1857
+ // /**
1858
+ // * Assert that TR order matches the natural node order
1859
+ // * @internal
1860
+ // */
1861
+ // protected _validateRows(): boolean {
1862
+ // let trs = this.nodeListElement.childNodes;
1863
+ // let i = 0;
1864
+ // let prev = -1;
1865
+ // let ok = true;
1866
+ // trs.forEach((element) => {
1867
+ // const tr = element as HTMLTableRowElement;
1868
+ // const top = Number.parseInt(tr.style.top);
1869
+ // const n = (<any>tr)._wb_node;
1870
+ // // if (i < 4) {
1871
+ // // console.info(
1872
+ // // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
1873
+ // // );
1874
+ // // }
1875
+ // if (prev >= 0 && top !== prev + ROW_HEIGHT) {
1876
+ // n.logWarn(
1877
+ // `TR order mismatch at index ${i}: top=${top}px != ${
1878
+ // prev + ROW_HEIGHT
1879
+ // }`
1880
+ // );
1881
+ // // throw new Error("fault");
1882
+ // ok = false;
1883
+ // }
1884
+ // prev = top;
1885
+ // i++;
1886
+ // });
1887
+ // return ok;
1888
+ // }
1626
1889
 
1627
1890
  /*
1628
- * - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
1891
+ * - Traverse all *visible* nodes of the whole tree, i.e. skip collapsed nodes.
1629
1892
  * - Store count of rows to `tree.treeRowCount`.
1630
1893
  * - Renumber `node._rowIdx` for all visible nodes.
1631
1894
  * - Calculate the index range that must be rendered to fill the viewport
@@ -1633,15 +1896,15 @@ export class Wunderbaum {
1633
1896
  * -
1634
1897
  */
1635
1898
  protected _updateRows(opts?: any): boolean {
1636
- const label = this.logTime("_updateRows");
1637
-
1899
+ // const label = this.logTime("_updateRows");
1900
+ // this.log("_updateRows", opts)
1638
1901
  opts = Object.assign({ newNodesOnly: false }, opts);
1639
1902
  const newNodesOnly = !!opts.newNodesOnly;
1640
1903
 
1641
1904
  const row_height = ROW_HEIGHT;
1642
- const vp_height = this.scrollContainer.clientHeight;
1905
+ const vp_height = this.element.clientHeight;
1643
1906
  const prefetch = RENDER_MAX_PREFETCH;
1644
- const ofs = this.scrollContainer.scrollTop;
1907
+ const ofs = this.element.scrollTop;
1645
1908
 
1646
1909
  let startIdx = Math.max(0, ofs / row_height - prefetch);
1647
1910
  startIdx = Math.floor(startIdx);
@@ -1669,7 +1932,7 @@ export class Wunderbaum {
1669
1932
  let prevElem: HTMLDivElement | "first" | "last" = "first";
1670
1933
 
1671
1934
  this.visitRows(function (node) {
1672
- // console.log("visit", node)
1935
+ // node.log("visit")
1673
1936
  const rowDiv = node._rowElem;
1674
1937
 
1675
1938
  // Renumber all expanded nodes
@@ -1691,8 +1954,11 @@ export class Wunderbaum {
1691
1954
  } else {
1692
1955
  obsoleteNodes.delete(node);
1693
1956
  // Create new markup
1957
+ if (rowDiv) {
1958
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
1959
+ }
1694
1960
  node.render({ top: top, after: prevElem });
1695
- // console.log("render", top, prevElem, "=>", node._rowElem);
1961
+ // node.log("render", top, prevElem, "=>", node._rowElem);
1696
1962
  prevElem = node._rowElem!;
1697
1963
  }
1698
1964
  idx++;
@@ -1709,8 +1975,8 @@ export class Wunderbaum {
1709
1975
  // `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
1710
1976
  // this.nodeListElement.style.height
1711
1977
  // );
1712
- this.logTimeEnd(label);
1713
- this._validateRows();
1978
+ // this.logTimeEnd(label);
1979
+ // this._validateRows();
1714
1980
  return modified;
1715
1981
  }
1716
1982
 
@@ -1891,17 +2157,11 @@ export class Wunderbaum {
1891
2157
  /**
1892
2158
  * Reload the tree with a new source.
1893
2159
  *
1894
- * Previous data is cleared.
1895
- * Pass `options.columns` to define a header (may also be part of `source.columns`).
2160
+ * Previous data is cleared. Note that also column- and type defintions may
2161
+ * be passed with the `source` object.
1896
2162
  */
1897
- load(source: any, options: any = {}) {
2163
+ load(source: any) {
1898
2164
  this.clear();
1899
- const columns = options.columns || source.columns;
1900
- if (columns) {
1901
- this.columns = options.columns;
1902
- // this._renderHeaderMarkup();
1903
- this.updateColumns({ calculateCols: false });
1904
- }
1905
2165
  return this.root.load(source);
1906
2166
  }
1907
2167
 
@@ -1913,7 +2173,7 @@ export class Wunderbaum {
1913
2173
  * tree.enableUpdate(false);
1914
2174
  * // ... (long running operation that would trigger many updates)
1915
2175
  * foo();
1916
- * // ... NOTE: make sure that async operations have finished
2176
+ * // ... NOTE: make sure that async operations have finished, e.g.
1917
2177
  * await foo();
1918
2178
  * } finally {
1919
2179
  * tree.enableUpdate(true);
@@ -1937,7 +2197,9 @@ export class Wunderbaum {
1937
2197
  // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
1938
2198
  // );
1939
2199
  if (this._disableUpdateCount === 0) {
1940
- this.updateViewport();
2200
+ // this.changeRedrawRequestPending = true; // make sure, we re-render all markup
2201
+ // this.updateViewport();
2202
+ this.setModified(ChangeType.any, { immediate: true });
1941
2203
  }
1942
2204
  } else {
1943
2205
  this._disableUpdateCount++;