wunderbaum 0.0.2 → 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.
@@ -1,8 +1,9 @@
1
1
  /*!
2
2
  * Wunderbaum - util
3
3
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
4
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
5
5
  */
6
+ /** @module util */
6
7
  /** Readable names for `MouseEvent.button` */
7
8
  const MOUSE_BUTTONS = {
8
9
  0: "",
@@ -613,7 +614,7 @@ var util = /*#__PURE__*/Object.freeze({
613
614
  /*!
614
615
  * Wunderbaum - common
615
616
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
616
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
617
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
617
618
  */
618
619
  const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
619
620
  const ROW_HEIGHT = 22;
@@ -701,6 +702,8 @@ const KEY_TO_ACTION_DICT = {
701
702
  Home: "firstCol",
702
703
  "Control+End": "last",
703
704
  "Control+Home": "first",
705
+ "Meta+ArrowDown": "last",
706
+ "Meta+ArrowUp": "first",
704
707
  "*": "expandAll",
705
708
  Multiply: "expandAll",
706
709
  PageDown: "pageDown",
@@ -727,7 +730,7 @@ function makeNodeTitleStartMatcher(s) {
727
730
  /*!
728
731
  * Wunderbaum - wb_extension_base
729
732
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
730
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
733
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
731
734
  */
732
735
  class WunderbaumExtension {
733
736
  constructor(tree, id, defaults) {
@@ -1082,7 +1085,7 @@ function throttle(func, wait = 0, options = {}) {
1082
1085
  /*!
1083
1086
  * Wunderbaum - ext-filter
1084
1087
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1085
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1088
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1086
1089
  */
1087
1090
  const START_MARKER = "\uFFF7";
1088
1091
  const END_MARKER = "\uFFF8";
@@ -1376,7 +1379,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1376
1379
  /*!
1377
1380
  * Wunderbaum - ext-keynav
1378
1381
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1379
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1382
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1380
1383
  */
1381
1384
  class KeynavExtension extends WunderbaumExtension {
1382
1385
  constructor(tree) {
@@ -1443,7 +1446,7 @@ class KeynavExtension extends WunderbaumExtension {
1443
1446
  eventName = "Add"; // expand
1444
1447
  }
1445
1448
  else if (navModeOption === NavigationModeOption.startRow) {
1446
- tree.setCellMode(NavigationMode.cellNav);
1449
+ tree.setNavigationMode(NavigationMode.cellNav);
1447
1450
  return;
1448
1451
  }
1449
1452
  break;
@@ -1482,6 +1485,8 @@ class KeynavExtension extends WunderbaumExtension {
1482
1485
  case "Home":
1483
1486
  case "Control+End":
1484
1487
  case "Control+Home":
1488
+ case "Meta+ArrowDown":
1489
+ case "Meta+ArrowUp":
1485
1490
  case "PageDown":
1486
1491
  case "PageUp":
1487
1492
  node.navigate(eventName, { activate: activate, event: event });
@@ -1513,11 +1518,11 @@ class KeynavExtension extends WunderbaumExtension {
1513
1518
  break;
1514
1519
  case "Escape":
1515
1520
  if (tree.navMode === NavigationMode.cellEdit) {
1516
- tree.setCellMode(NavigationMode.cellNav);
1521
+ tree.setNavigationMode(NavigationMode.cellNav);
1517
1522
  handled = true;
1518
1523
  }
1519
1524
  else if (tree.navMode === NavigationMode.cellNav) {
1520
- tree.setCellMode(NavigationMode.row);
1525
+ tree.setNavigationMode(NavigationMode.row);
1521
1526
  handled = true;
1522
1527
  }
1523
1528
  break;
@@ -1527,7 +1532,7 @@ class KeynavExtension extends WunderbaumExtension {
1527
1532
  handled = true;
1528
1533
  }
1529
1534
  else if (navModeOption !== NavigationModeOption.cell) {
1530
- tree.setCellMode(NavigationMode.row);
1535
+ tree.setNavigationMode(NavigationMode.row);
1531
1536
  handled = true;
1532
1537
  }
1533
1538
  break;
@@ -1544,6 +1549,8 @@ class KeynavExtension extends WunderbaumExtension {
1544
1549
  case "Home":
1545
1550
  case "Control+End":
1546
1551
  case "Control+Home":
1552
+ case "Meta+ArrowDown":
1553
+ case "Meta+ArrowUp":
1547
1554
  case "PageDown":
1548
1555
  case "PageUp":
1549
1556
  node.navigate(eventName, { activate: activate, event: event });
@@ -1562,7 +1569,7 @@ class KeynavExtension extends WunderbaumExtension {
1562
1569
  /*!
1563
1570
  * Wunderbaum - ext-logger
1564
1571
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1565
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1572
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1566
1573
  */
1567
1574
  class LoggerExtension extends WunderbaumExtension {
1568
1575
  constructor(tree) {
@@ -1602,7 +1609,7 @@ class LoggerExtension extends WunderbaumExtension {
1602
1609
  /*!
1603
1610
  * Wunderbaum - ext-dnd
1604
1611
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1605
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1612
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1606
1613
  */
1607
1614
  const nodeMimeType = "application/x-wunderbaum-node";
1608
1615
  class DndExtension extends WunderbaumExtension {
@@ -1870,7 +1877,7 @@ class DndExtension extends WunderbaumExtension {
1870
1877
  /*!
1871
1878
  * Wunderbaum - drag_observer
1872
1879
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1873
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1880
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1874
1881
  */
1875
1882
  /**
1876
1883
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2004,7 +2011,7 @@ class DragObserver {
2004
2011
  /*!
2005
2012
  * Wunderbaum - ext-grid
2006
2013
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2007
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
2014
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2008
2015
  */
2009
2016
  class GridExtension extends WunderbaumExtension {
2010
2017
  constructor(tree) {
@@ -2041,7 +2048,7 @@ class GridExtension extends WunderbaumExtension {
2041
2048
  /*!
2042
2049
  * Wunderbaum - deferred
2043
2050
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2044
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
2051
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2045
2052
  */
2046
2053
  /**
2047
2054
  * Deferred is a ES6 Promise, that exposes the resolve() and reject()` method.
@@ -2084,7 +2091,7 @@ class Deferred {
2084
2091
  /*!
2085
2092
  * Wunderbaum - wunderbaum_node
2086
2093
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2087
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
2094
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2088
2095
  */
2089
2096
  /** Top-level properties that can be passed with `data`. */
2090
2097
  const NODE_PROPS = new Set([
@@ -2121,15 +2128,31 @@ const NODE_ATTRS = new Set([
2121
2128
  "unselectableIgnore",
2122
2129
  "unselectableStatus",
2123
2130
  ]);
2131
+ /**
2132
+ * A single tree node.
2133
+ *
2134
+ * **NOTE:** <br>
2135
+ * Generally you should not modify properties directly, since this may break
2136
+ * the internal bookkeeping.
2137
+ */
2124
2138
  class WunderbaumNode {
2125
2139
  constructor(tree, parent, data) {
2126
2140
  var _a, _b;
2141
+ /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
2142
+ * times within a tree (in this case we have 'clone nodes').
2143
+ * @see Use {@link setKey} to modify.
2144
+ */
2127
2145
  this.refKey = undefined;
2128
2146
  this.children = null;
2129
2147
  this.lazy = false;
2148
+ /** Expansion state.
2149
+ * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */
2130
2150
  this.expanded = false;
2151
+ /** Selection state.
2152
+ * @see {@link isSelected}, {@link setSelected}. */
2131
2153
  this.selected = false;
2132
- /** Additional classes added to `div.wb-row`. */
2154
+ /** Additional classes added to `div.wb-row`.
2155
+ * @see {@link addClass}, {@link removeClass}, {@link toggleClass}. */
2133
2156
  this.extraClasses = new Set();
2134
2157
  /** Custom data that was passed to the constructor */
2135
2158
  this.data = {};
@@ -2295,7 +2318,8 @@ class WunderbaumNode {
2295
2318
  }
2296
2319
  /**
2297
2320
  * Apply a modification (or navigation) operation.
2298
- * @see Wunderbaum#applyCommand
2321
+ *
2322
+ * @see {@link Wunderbaum.applyCommand}
2299
2323
  */
2300
2324
  applyCommand(cmd, opts) {
2301
2325
  return this.tree.applyCommand(cmd, this, opts);
@@ -2388,8 +2412,7 @@ class WunderbaumNode {
2388
2412
  }
2389
2413
  /** Find a node relative to self.
2390
2414
  *
2391
- * @param where The keyCode that would normally trigger this move,
2392
- * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up').
2415
+ * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()}
2393
2416
  */
2394
2417
  findRelatedNode(where, includeHidden = false) {
2395
2418
  return this.tree.findRelatedNode(this, where, includeHidden);
@@ -2690,7 +2713,7 @@ class WunderbaumNode {
2690
2713
  assert(!this.parent);
2691
2714
  tree.columns = data.columns;
2692
2715
  delete data.columns;
2693
- tree.renderHeader();
2716
+ tree.updateColumns({ calculateCols: false });
2694
2717
  }
2695
2718
  this._loadSourceObject(data);
2696
2719
  }
@@ -2726,19 +2749,20 @@ class WunderbaumNode {
2726
2749
  this.setStatus(NodeStatusType.ok);
2727
2750
  return;
2728
2751
  }
2729
- assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}` or false.");
2752
+ assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false.");
2730
2753
  await this.load(source); // also calls setStatus('ok')
2731
2754
  if (wasExpanded) {
2732
2755
  this.expanded = true;
2733
2756
  this.tree.setModified(ChangeType.structure);
2734
2757
  }
2735
2758
  else {
2736
- this.render(); // Fix expander icon to 'loaded'
2759
+ this.setModified(); // Fix expander icon to 'loaded'
2737
2760
  }
2738
2761
  }
2739
2762
  catch (e) {
2763
+ this.logError("Error during loadLazy()", e);
2764
+ this._callEvent("error", { error: e });
2740
2765
  this.setStatus(NodeStatusType.error, "" + e);
2741
- // } finally {
2742
2766
  }
2743
2767
  return;
2744
2768
  }
@@ -2906,31 +2930,33 @@ class WunderbaumNode {
2906
2930
  // Allow to pass 'ArrowLeft' instead of 'left'
2907
2931
  where = KEY_TO_ACTION_DICT[where] || where;
2908
2932
  // Otherwise activate or focus the related node
2909
- let node = this.findRelatedNode(where);
2910
- if (node) {
2911
- // setFocus/setActive will scroll later (if autoScroll is specified)
2912
- try {
2913
- node.makeVisible({ scrollIntoView: false });
2914
- }
2915
- catch (e) { } // #272
2916
- node.setFocus();
2917
- if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2918
- return Promise.resolve(this);
2919
- }
2920
- return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2933
+ const node = this.findRelatedNode(where);
2934
+ if (!node) {
2935
+ this.logWarn(`Could not find related node '${where}'.`);
2936
+ return Promise.resolve(this);
2921
2937
  }
2922
- this.logWarn("Could not find related node '" + where + "'.");
2923
- return Promise.resolve(this);
2938
+ // setFocus/setActive will scroll later (if autoScroll is specified)
2939
+ try {
2940
+ node.makeVisible({ scrollIntoView: false });
2941
+ }
2942
+ catch (e) { } // #272
2943
+ node.setFocus();
2944
+ if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2945
+ return Promise.resolve(this);
2946
+ }
2947
+ return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2924
2948
  }
2925
2949
  /** Delete this node and all descendants. */
2926
2950
  remove() {
2927
2951
  const tree = this.tree;
2928
2952
  const pos = this.parent.children.indexOf(this);
2953
+ this.triggerModify("remove");
2929
2954
  this.parent.children.splice(pos, 1);
2930
2955
  this.visit((n) => {
2931
2956
  n.removeMarkup();
2932
2957
  tree._unregisterNode(n);
2933
2958
  }, true);
2959
+ tree.setModified(ChangeType.structure);
2934
2960
  }
2935
2961
  /** Remove all descendants of this node. */
2936
2962
  removeChildren() {
@@ -3055,6 +3081,7 @@ class WunderbaumNode {
3055
3081
  const activeColIdx = tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
3056
3082
  // let colElems: HTMLElement[];
3057
3083
  const isNew = !rowDiv;
3084
+ assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
3058
3085
  assert(!this.isRootNode());
3059
3086
  //
3060
3087
  let rowClasses = ["wb-row"];
@@ -3106,7 +3133,7 @@ class WunderbaumNode {
3106
3133
  nodeElem.appendChild(elem);
3107
3134
  ofsTitlePx += ICON_WIDTH;
3108
3135
  }
3109
- if (treeOptions.minExpandLevel && level > treeOptions.minExpandLevel) {
3136
+ if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
3110
3137
  expanderSpan = document.createElement("i");
3111
3138
  nodeElem.appendChild(expanderSpan);
3112
3139
  ofsTitlePx += ICON_WIDTH;
@@ -3235,9 +3262,19 @@ class WunderbaumNode {
3235
3262
  });
3236
3263
  }
3237
3264
  // Attach to DOM as late as possible
3238
- // if (!this._rowElem) {
3239
- tree.nodeListElement.appendChild(rowDiv);
3240
- // }
3265
+ if (isNew) {
3266
+ const after = opts ? opts.after : "last";
3267
+ switch (after) {
3268
+ case "first":
3269
+ tree.nodeListElement.prepend(rowDiv);
3270
+ break;
3271
+ case "last":
3272
+ tree.nodeListElement.appendChild(rowDiv);
3273
+ break;
3274
+ default:
3275
+ opts.after.after(rowDiv);
3276
+ }
3277
+ }
3241
3278
  }
3242
3279
  /**
3243
3280
  * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
@@ -3311,14 +3348,15 @@ class WunderbaumNode {
3311
3348
  *
3312
3349
  * Evaluation sequence:
3313
3350
  *
3314
- * If `tree.options.<name>` is a callback that returns something, use that.
3315
- * Else if `node.<name>` is defined, use that.
3316
- * Else if `tree.types[<node.type>]` is a value, use that.
3317
- * Else if `tree.options.<name>` is a value, use that.
3318
- * Else use `defaultValue`.
3351
+ * - If `tree.options.<name>` is a callback that returns something, use that.
3352
+ * - Else if `node.<name>` is defined, use that.
3353
+ * - Else if `tree.types[<node.type>]` is a value, use that.
3354
+ * - Else if `tree.options.<name>` is a value, use that.
3355
+ * - Else use `defaultValue`.
3319
3356
  *
3320
3357
  * @param name name of the option property (on node and tree)
3321
3358
  * @param defaultValue return this if nothing else matched
3359
+ * {@link Wunderbaum.getOption|Wunderbaum.getOption()}
3322
3360
  */
3323
3361
  getOption(name, defaultValue) {
3324
3362
  let tree = this.tree;
@@ -3353,15 +3391,21 @@ class WunderbaumNode {
3353
3391
  // Use value from value options dict, fallback do default
3354
3392
  return value !== null && value !== void 0 ? value : defaultValue;
3355
3393
  }
3394
+ /** Make sure that this node is visible in the viewport.
3395
+ * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo()}
3396
+ */
3356
3397
  async scrollIntoView(options) {
3357
3398
  return this.tree.scrollTo(this);
3358
3399
  }
3400
+ /**
3401
+ * Activate this node, deactivate previous, send events, activate column and scroll int viewport.
3402
+ */
3359
3403
  async setActive(flag = true, options) {
3360
3404
  const tree = this.tree;
3361
3405
  const prev = tree.activeNode;
3362
3406
  const retrigger = options === null || options === void 0 ? void 0 : options.retrigger;
3363
- const noEvent = options === null || options === void 0 ? void 0 : options.noEvent;
3364
- if (!noEvent) {
3407
+ const noEvents = options === null || options === void 0 ? void 0 : options.noEvents;
3408
+ if (!noEvents) {
3365
3409
  let orgEvent = options === null || options === void 0 ? void 0 : options.event;
3366
3410
  if (flag) {
3367
3411
  if (prev !== this || retrigger) {
@@ -3399,19 +3443,18 @@ class WunderbaumNode {
3399
3443
  // requestAnimationFrame(() => {
3400
3444
  // this.scrollIntoView();
3401
3445
  // })
3402
- this.scrollIntoView();
3403
- }
3404
- setModified(change = ChangeType.status) {
3405
- assert(change === ChangeType.status);
3406
- this.tree.setModified(ChangeType.row, this);
3446
+ return this.scrollIntoView();
3407
3447
  }
3448
+ /**
3449
+ * Expand or collapse this node.
3450
+ */
3408
3451
  async setExpanded(flag = true, options) {
3409
3452
  // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
3410
3453
  if (!flag &&
3411
3454
  this.isExpanded() &&
3412
3455
  this.getLevel() < this.getOption("minExpandLevel") &&
3413
3456
  !getOption(options, "force")) {
3414
- this.logDebug("Ignored collapse request.");
3457
+ this.logDebug("Ignored collapse request below expandLevel.");
3415
3458
  return;
3416
3459
  }
3417
3460
  if (flag && this.lazy && this.children == null) {
@@ -3420,16 +3463,31 @@ class WunderbaumNode {
3420
3463
  this.expanded = flag;
3421
3464
  this.tree.setModified(ChangeType.structure);
3422
3465
  }
3423
- setIcon() {
3424
- throw new Error("Not yet implemented");
3425
- // this.setDirty(ChangeType.status);
3426
- }
3466
+ /**
3467
+ * Set keyboard focus here.
3468
+ * @see {@link setActive}
3469
+ */
3427
3470
  setFocus(flag = true, options) {
3428
3471
  const prev = this.tree.focusNode;
3429
3472
  this.tree.focusNode = this;
3430
3473
  prev === null || prev === void 0 ? void 0 : prev.setModified();
3431
3474
  this.setModified();
3432
3475
  }
3476
+ /** Set a new icon path or class. */
3477
+ setIcon() {
3478
+ throw new Error("Not yet implemented");
3479
+ // this.setModified();
3480
+ }
3481
+ /** Change node's {@link key} and/or {@link refKey}. */
3482
+ setKey(key, refKey) {
3483
+ throw new Error("Not yet implemented");
3484
+ }
3485
+ /** Schedule a render, typically called to update after a status or data change. */
3486
+ setModified(change = ChangeType.status) {
3487
+ assert(change === ChangeType.status);
3488
+ this.tree.setModified(ChangeType.row, this);
3489
+ }
3490
+ /** Modify the check/uncheck state. */
3433
3491
  setSelected(flag = true, options) {
3434
3492
  const prev = this.selected;
3435
3493
  if (!!flag !== prev) {
@@ -3438,10 +3496,9 @@ class WunderbaumNode {
3438
3496
  this.selected = !!flag;
3439
3497
  this.setModified();
3440
3498
  }
3441
- /** Show node status (ok, loading, error, noData) using styles and a dummy child node.
3442
- */
3499
+ /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
3443
3500
  setStatus(status, message, details) {
3444
- let tree = this.tree;
3501
+ const tree = this.tree;
3445
3502
  let statusNode = null;
3446
3503
  const _clearStatusNode = () => {
3447
3504
  // Remove dedicated dummy node, if any
@@ -3515,6 +3572,7 @@ class WunderbaumNode {
3515
3572
  tree.setModified(ChangeType.structure);
3516
3573
  return statusNode;
3517
3574
  }
3575
+ /** Rename this node. */
3518
3576
  setTitle(title) {
3519
3577
  this.title = title;
3520
3578
  this.setModified();
@@ -3538,10 +3596,16 @@ class WunderbaumNode {
3538
3596
  * @param {object} [extra]
3539
3597
  */
3540
3598
  triggerModify(operation, extra) {
3599
+ if (!this.parent) {
3600
+ return;
3601
+ }
3541
3602
  this.parent.triggerModifyChild(operation, this, extra);
3542
3603
  }
3543
- /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br>
3544
- * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
3604
+ /**
3605
+ * Call fn(node) for all child nodes in hierarchical order (depth-first).
3606
+ *
3607
+ * Stop iteration, if fn() returns false. Skip current branch, if fn()
3608
+ * returns "skip".<br>
3545
3609
  * Return false if iteration was stopped.
3546
3610
  *
3547
3611
  * @param {function} callback the callback function.
@@ -3585,7 +3649,8 @@ class WunderbaumNode {
3585
3649
  }
3586
3650
  return true;
3587
3651
  }
3588
- /** Call fn(node) for all sibling nodes.<br>
3652
+ /**
3653
+ * Call fn(node) for all sibling nodes.<br>
3589
3654
  * Stop iteration, if fn() returns false.<br>
3590
3655
  * Return false if iteration was stopped.
3591
3656
  *
@@ -3616,7 +3681,7 @@ WunderbaumNode.sequence = 0;
3616
3681
  /*!
3617
3682
  * Wunderbaum - ext-edit
3618
3683
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3619
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
3684
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
3620
3685
  */
3621
3686
  // const START_MARKER = "\uFFF7";
3622
3687
  class EditExtension extends WunderbaumExtension {
@@ -3734,7 +3799,7 @@ class EditExtension extends WunderbaumExtension {
3734
3799
  break;
3735
3800
  case "F2":
3736
3801
  if (trigger.indexOf("F2") >= 0) {
3737
- // tree.setCellMode(NavigationMode.cellEdit);
3802
+ // tree.setNavigationMode(NavigationMode.cellEdit);
3738
3803
  this.startEditTitle();
3739
3804
  return false;
3740
3805
  }
@@ -3775,6 +3840,7 @@ class EditExtension extends WunderbaumExtension {
3775
3840
  if (validity) {
3776
3841
  // Permanently apply input validations (CSS and tooltip)
3777
3842
  inputElem.addEventListener("keydown", (e) => {
3843
+ inputElem.setCustomValidity("");
3778
3844
  if (!inputElem.reportValidity()) ;
3779
3845
  });
3780
3846
  }
@@ -3815,6 +3881,11 @@ class EditExtension extends WunderbaumExtension {
3815
3881
  }
3816
3882
  node.logDebug(`stopEditTitle(${apply})`, opts, focusElem, newValue);
3817
3883
  if (apply && newValue !== null && newValue !== node.title) {
3884
+ const errMsg = focusElem.validationMessage;
3885
+ if (errMsg) {
3886
+ // input element's native validation failed
3887
+ throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`);
3888
+ }
3818
3889
  const colElem = node.getColElem(0);
3819
3890
  this._applyChange("edit.apply", node, colElem, {
3820
3891
  oldValue: node.title,
@@ -3901,8 +3972,8 @@ class EditExtension extends WunderbaumExtension {
3901
3972
  * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
3902
3973
  * Released under the MIT license.
3903
3974
  *
3904
- * @version v0.0.2
3905
- * @date Tue, 12 Apr 2022 18:36:21 GMT
3975
+ * @version v0.0.3
3976
+ * @date Mon, 18 Apr 2022 11:52:44 GMT
3906
3977
  */
3907
3978
  // const class_prefix = "wb-";
3908
3979
  // const node_props: string[] = ["title", "key", "refKey"];
@@ -3918,37 +3989,43 @@ class Wunderbaum {
3918
3989
  this.extensions = {};
3919
3990
  this.keyMap = new Map();
3920
3991
  this.refKeyMap = new Map();
3921
- this.viewNodes = new Set();
3922
- // protected rows: WunderbaumNode[] = [];
3923
- // protected _rowCount = 0;
3992
+ // protected viewNodes = new Set<WunderbaumNode>();
3993
+ this.treeRowCount = 0;
3994
+ this._disableUpdateCount = 0;
3924
3995
  // protected eventHandlers : Array<function> = [];
3996
+ /** Currently active node if any. */
3925
3997
  this.activeNode = null;
3998
+ /** Current node hat has keyboard focus if any. */
3926
3999
  this.focusNode = null;
3927
- this._disableUpdate = 0;
3928
- this._disableUpdateCount = 0;
3929
4000
  /** Shared properties, referenced by `node.type`. */
3930
4001
  this.types = {};
3931
4002
  /** List of column definitions. */
3932
4003
  this.columns = [];
3933
4004
  this._columnsById = {};
3934
4005
  // Modification Status
3935
- this.changedSince = 0;
3936
- this.changes = new Set();
3937
- this.changedNodes = new Set();
3938
- this.changeRedrawPending = false;
4006
+ // protected changedSince = 0;
4007
+ // protected changes = new Set<ChangeType>();
4008
+ // protected changedNodes = new Set<WunderbaumNode>();
4009
+ this.changeRedrawRequestPending = false;
4010
+ /** Expose some useful methods of the util.ts module as `tree._util`. */
4011
+ this._util = util;
3939
4012
  // --- FILTER ---
3940
4013
  this.filterMode = null;
3941
4014
  // --- KEYNAV ---
4015
+ /** @internal Use `setColumn()`/`getActiveColElem()`*/
3942
4016
  this.activeColIdx = 0;
4017
+ /** @internal */
3943
4018
  this.navMode = NavigationMode.row;
4019
+ /** @internal */
3944
4020
  this.lastQuicksearchTime = 0;
4021
+ /** @internal */
3945
4022
  this.lastQuicksearchTerm = "";
3946
4023
  // --- EDIT ---
3947
4024
  this.lastClickTime = 0;
3948
- // TODO: make accessible in compiled JS like this?
3949
- this._util = util;
3950
- /** Alias for `logDebug` */
3951
- this.log = this.logDebug; // Alias
4025
+ /** Alias for {@link Wunderbaum.logDebug}.
4026
+ * @alias Wunderbaum.logDebug
4027
+ */
4028
+ this.log = this.logDebug;
3952
4029
  let opts = (this.options = extend({
3953
4030
  id: null,
3954
4031
  source: null,
@@ -4182,37 +4259,18 @@ class Wunderbaum {
4182
4259
  forceClose: true,
4183
4260
  });
4184
4261
  }
4185
- // if (flag && !this.activeNode ) {
4186
- // setTimeout(() => {
4187
- // if (!this.activeNode) {
4188
- // const firstNode = this.getFirstChild();
4189
- // if (firstNode && !firstNode?.isStatusNode()) {
4190
- // firstNode.logInfo("Activate on focus", e);
4191
- // firstNode.setActive(true, { event: e });
4192
- // }
4193
- // }
4194
- // }, 10);
4195
- // }
4196
4262
  });
4197
4263
  }
4198
- /** */
4199
- // _renderHeader(){
4200
- // const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
4201
- // this.element.innerHTML = `
4202
- // <div class='wb-header'>
4203
- // <div class='wb-row'>
4204
- // ${coldivs}
4205
- // </div>
4206
- // </div>`;
4207
- // }
4208
- /** Return a Wunderbaum instance, from element, index, or event.
4264
+ /**
4265
+ * Return a Wunderbaum instance, from element, id, index, or event.
4209
4266
  *
4210
- * @example
4211
- * getTree(); // Get first Wunderbaum instance on page
4212
- * getTree(1); // Get second Wunderbaum instance on page
4213
- * getTree(event); // Get tree for this mouse- or keyboard event
4214
- * getTree("foo"); // Get tree for this `tree.options.id`
4267
+ * ```js
4268
+ * getTree(); // Get first Wunderbaum instance on page
4269
+ * getTree(1); // Get second Wunderbaum instance on page
4270
+ * getTree(event); // Get tree for this mouse- or keyboard event
4271
+ * getTree("foo"); // Get tree for this `tree.options.id`
4215
4272
  * getTree("#tree"); // Get tree for this matching element
4273
+ * ```
4216
4274
  */
4217
4275
  static getTree(el) {
4218
4276
  if (el instanceof Wunderbaum) {
@@ -4253,9 +4311,8 @@ class Wunderbaum {
4253
4311
  }
4254
4312
  return null;
4255
4313
  }
4256
- /** Return a WunderbaumNode instance from element, event.
4257
- *
4258
- * @param el
4314
+ /**
4315
+ * Return a WunderbaumNode instance from element or event.
4259
4316
  */
4260
4317
  static getNode(el) {
4261
4318
  if (!el) {
@@ -4277,7 +4334,7 @@ class Wunderbaum {
4277
4334
  }
4278
4335
  return null;
4279
4336
  }
4280
- /** */
4337
+ /** @internal */
4281
4338
  _registerExtension(extension) {
4282
4339
  this.extensionList.push(extension);
4283
4340
  this.extensions[extension.id] = extension;
@@ -4319,7 +4376,7 @@ class Wunderbaum {
4319
4376
  node.tree = null;
4320
4377
  node.parent = null;
4321
4378
  // node.title = "DISPOSED: " + node.title
4322
- this.viewNodes.delete(node);
4379
+ // this.viewNodes.delete(node);
4323
4380
  node.removeMarkup();
4324
4381
  }
4325
4382
  /** Call all hook methods of all registered extensions.*/
@@ -4337,7 +4394,9 @@ class Wunderbaum {
4337
4394
  }
4338
4395
  return res;
4339
4396
  }
4340
- /** Call tree method or extension method if defined.
4397
+ /**
4398
+ * Call tree method or extension method if defined.
4399
+ *
4341
4400
  * Example:
4342
4401
  * ```js
4343
4402
  * tree._callMethod("edit.startEdit", "arg1", "arg2")
@@ -4354,7 +4413,9 @@ class Wunderbaum {
4354
4413
  this.logError(`Calling undefined method '${name}()'.`);
4355
4414
  }
4356
4415
  }
4357
- /** Call event handler if defined in tree.options.
4416
+ /**
4417
+ * Call event handler if defined in tree or tree.EXTENSION options.
4418
+ *
4358
4419
  * Example:
4359
4420
  * ```js
4360
4421
  * tree._callEvent("edit.beforeEdit", {foo: 42})
@@ -4370,27 +4431,33 @@ class Wunderbaum {
4370
4431
  // this.logError(`Triggering undefined event '${name}'.`)
4371
4432
  }
4372
4433
  }
4373
- /** Return the topmost visible node in the viewport */
4374
- _firstNodeInView(complete = true) {
4375
- let topIdx, node;
4376
- if (complete) {
4377
- topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
4378
- }
4379
- else {
4380
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4381
- }
4434
+ /** Return the node for given row index. */
4435
+ _getNodeByRowIdx(idx) {
4382
4436
  // TODO: start searching from active node (reverse)
4437
+ let node = null;
4383
4438
  this.visitRows((n) => {
4384
- if (n._rowIdx === topIdx) {
4439
+ if (n._rowIdx === idx) {
4385
4440
  node = n;
4386
4441
  return false;
4387
4442
  }
4388
4443
  });
4389
4444
  return node;
4390
4445
  }
4391
- /** Return the lowest visible node in the viewport */
4446
+ /** Return the topmost visible node in the viewport. */
4447
+ _firstNodeInView(complete = true) {
4448
+ let topIdx;
4449
+ const gracePy = 1; // ignore subpixel scrolling
4450
+ if (complete) {
4451
+ topIdx = Math.ceil((this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT);
4452
+ }
4453
+ else {
4454
+ topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4455
+ }
4456
+ return this._getNodeByRowIdx(topIdx);
4457
+ }
4458
+ /** Return the lowest visible node in the viewport. */
4392
4459
  _lastNodeInView(complete = true) {
4393
- let bottomIdx, node;
4460
+ let bottomIdx;
4394
4461
  if (complete) {
4395
4462
  bottomIdx =
4396
4463
  Math.floor((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
@@ -4401,16 +4468,10 @@ class Wunderbaum {
4401
4468
  Math.ceil((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
4402
4469
  ROW_HEIGHT) - 1;
4403
4470
  }
4404
- // TODO: start searching from active node
4405
- this.visitRows((n) => {
4406
- if (n._rowIdx === bottomIdx) {
4407
- node = n;
4408
- return false;
4409
- }
4410
- });
4411
- return node;
4471
+ bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
4472
+ return this._getNodeByRowIdx(bottomIdx);
4412
4473
  }
4413
- /** Return preceeding visible node in the viewport */
4474
+ /** Return preceeding visible node in the viewport. */
4414
4475
  _getPrevNodeInView(node, ofs = 1) {
4415
4476
  this.visitRows((n) => {
4416
4477
  node = n;
@@ -4420,7 +4481,7 @@ class Wunderbaum {
4420
4481
  }, { reverse: true, start: node || this.getActiveNode() });
4421
4482
  return node;
4422
4483
  }
4423
- /** Return following visible node in the viewport */
4484
+ /** Return following visible node in the viewport. */
4424
4485
  _getNextNodeInView(node, ofs = 1) {
4425
4486
  this.visitRows((n) => {
4426
4487
  node = n;
@@ -4430,10 +4491,15 @@ class Wunderbaum {
4430
4491
  }, { reverse: false, start: node || this.getActiveNode() });
4431
4492
  return node;
4432
4493
  }
4494
+ /**
4495
+ * Append (or insert) a list of toplevel nodes.
4496
+ *
4497
+ * @see {@link WunderbaumNode.addChildren}
4498
+ */
4433
4499
  addChildren(nodeData, options) {
4434
4500
  return this.root.addChildren(nodeData, options);
4435
4501
  }
4436
- /*
4502
+ /**
4437
4503
  * Apply a modification or navigation operation.
4438
4504
  *
4439
4505
  * Most of these commands simply map to a node or tree method.
@@ -4558,16 +4624,17 @@ class Wunderbaum {
4558
4624
  this.root.children = null;
4559
4625
  this.keyMap.clear();
4560
4626
  this.refKeyMap.clear();
4561
- this.viewNodes.clear();
4627
+ // this.viewNodes.clear();
4628
+ this.treeRowCount = 0;
4562
4629
  this.activeNode = null;
4563
4630
  this.focusNode = null;
4564
4631
  // this.types = {};
4565
4632
  // this. columns =[];
4566
4633
  // this._columnsById = {};
4567
4634
  // Modification Status
4568
- this.changedSince = 0;
4569
- this.changes.clear();
4570
- this.changedNodes.clear();
4635
+ // this.changedSince = 0;
4636
+ // this.changes.clear();
4637
+ // this.changedNodes.clear();
4571
4638
  // // --- FILTER ---
4572
4639
  // public filterMode: FilterModeType = null;
4573
4640
  // // --- KEYNAV ---
@@ -4595,10 +4662,11 @@ class Wunderbaum {
4595
4662
  /**
4596
4663
  * Return `tree.option.NAME` (also resolving if this is a callback).
4597
4664
  *
4598
- * See also [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
4599
- * `tree.types[node.type].NAME`.
4665
+ * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
4666
+ * to consider `node.NAME` setting and `tree.types[node.type].NAME`.
4600
4667
  *
4601
- * @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
4668
+ * @param name option name (use dot notation to access extension option, e.g.
4669
+ * `filter.mode`)
4602
4670
  */
4603
4671
  getOption(name, defaultValue) {
4604
4672
  let ext;
@@ -4642,18 +4710,14 @@ class Wunderbaum {
4642
4710
  }
4643
4711
  /** Run code, but defer `updateViewport()` until done. */
4644
4712
  runWithoutUpdate(func, hint = null) {
4645
- // const prev = this._disableUpdate;
4646
- // const start = Date.now();
4647
- // this._disableUpdate = Date.now();
4648
4713
  try {
4649
4714
  this.enableUpdate(false);
4650
- return func();
4715
+ const res = func();
4716
+ assert(!(res instanceof Promise));
4717
+ return res;
4651
4718
  }
4652
4719
  finally {
4653
4720
  this.enableUpdate(true);
4654
- // if (!prev && this._disableUpdate === start) {
4655
- // this._disableUpdate = 0;
4656
- // }
4657
4721
  }
4658
4722
  }
4659
4723
  /** Recursively expand all expandable nodes (triggers lazy load id needed). */
@@ -4671,11 +4735,12 @@ class Wunderbaum {
4671
4735
  /** Return the number of nodes in the data model.*/
4672
4736
  count(visible = false) {
4673
4737
  if (visible) {
4674
- return this.viewNodes.size;
4738
+ return this.treeRowCount;
4739
+ // return this.viewNodes.size;
4675
4740
  }
4676
4741
  return this.keyMap.size;
4677
4742
  }
4678
- /* Internal sanity check. */
4743
+ /** @internal sanity check. */
4679
4744
  _check() {
4680
4745
  let i = 0;
4681
4746
  this.visit((n) => {
@@ -4686,25 +4751,30 @@ class Wunderbaum {
4686
4751
  }
4687
4752
  // util.assert(this.keyMap.size === i);
4688
4753
  }
4689
- /**Find all nodes that matches condition.
4754
+ /**
4755
+ * Find all nodes that matches condition.
4690
4756
  *
4691
4757
  * @param match title string to search for, or a
4692
4758
  * callback function that returns `true` if a node is matched.
4693
- * @see [[WunderbaumNode.findAll]]
4759
+ *
4760
+ * @see {@link WunderbaumNode.findAll}
4694
4761
  */
4695
4762
  findAll(match) {
4696
4763
  return this.root.findAll(match);
4697
4764
  }
4698
- /**Find first node that matches condition.
4765
+ /**
4766
+ * Find first node that matches condition.
4699
4767
  *
4700
4768
  * @param match title string to search for, or a
4701
4769
  * callback function that returns `true` if a node is matched.
4702
- * @see [[WunderbaumNode.findFirst]]
4770
+ * @see {@link WunderbaumNode.findFirst}
4771
+ *
4703
4772
  */
4704
4773
  findFirst(match) {
4705
4774
  return this.root.findFirst(match);
4706
4775
  }
4707
- /** Find the next visible node that starts with `match`, starting at `startNode`
4776
+ /**
4777
+ * Find the next visible node that starts with `match`, starting at `startNode`
4708
4778
  * and wrap-around at the end.
4709
4779
  */
4710
4780
  findNextNode(match, startNode) {
@@ -4734,7 +4804,8 @@ class Wunderbaum {
4734
4804
  }
4735
4805
  return res;
4736
4806
  }
4737
- /** Find a node relative to another node.
4807
+ /**
4808
+ * Find a node relative to another node.
4738
4809
  *
4739
4810
  * @param node
4740
4811
  * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
@@ -4744,7 +4815,7 @@ class Wunderbaum {
4744
4815
  */
4745
4816
  findRelatedNode(node, where, includeHidden = false) {
4746
4817
  let res = null;
4747
- let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4818
+ const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4748
4819
  switch (where) {
4749
4820
  case "parent":
4750
4821
  if (node.parent && node.parent.parent) {
@@ -4800,9 +4871,9 @@ class Wunderbaum {
4800
4871
  res = this._getNextNodeInView(node);
4801
4872
  break;
4802
4873
  case "pageDown":
4803
- let bottomNode = this._lastNodeInView();
4804
- // this.logDebug(where, this.focusNode, bottomNode);
4805
- if (this.focusNode !== bottomNode) {
4874
+ const bottomNode = this._lastNodeInView();
4875
+ // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
4876
+ if (node._rowIdx < bottomNode._rowIdx) {
4806
4877
  res = bottomNode;
4807
4878
  }
4808
4879
  else {
@@ -4810,12 +4881,13 @@ class Wunderbaum {
4810
4881
  }
4811
4882
  break;
4812
4883
  case "pageUp":
4813
- if (this.focusNode && this.focusNode._rowIdx === 0) {
4814
- res = this.focusNode;
4884
+ if (node._rowIdx === 0) {
4885
+ res = node;
4815
4886
  }
4816
4887
  else {
4817
- let topNode = this._firstNodeInView();
4818
- if (this.focusNode !== topNode) {
4888
+ const topNode = this._firstNodeInView();
4889
+ // this.logDebug(`${where}(${node}) -> ${topNode}`);
4890
+ if (node._rowIdx > topNode._rowIdx) {
4819
4891
  res = topNode;
4820
4892
  }
4821
4893
  else {
@@ -4829,7 +4901,7 @@ class Wunderbaum {
4829
4901
  return res;
4830
4902
  }
4831
4903
  /**
4832
- * Return the active cell of the currently active node or null.
4904
+ * Return the active cell (`span.wb-col`) of the currently active node or null.
4833
4905
  */
4834
4906
  getActiveColElem() {
4835
4907
  if (this.activeNode && this.activeColIdx >= 0) {
@@ -4895,7 +4967,7 @@ class Wunderbaum {
4895
4967
  }
4896
4968
  else {
4897
4969
  // Somewhere near the title
4898
- if (event.type !== "mousemove") {
4970
+ if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
4899
4971
  console.warn("getEventInfo(): not found", event, res);
4900
4972
  }
4901
4973
  return res;
@@ -4927,7 +4999,8 @@ class Wunderbaum {
4927
4999
  isEditing() {
4928
5000
  return this._callMethod("edit.isEditingTitle");
4929
5001
  }
4930
- /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
5002
+ /**
5003
+ * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
4931
5004
  */
4932
5005
  isLoading() {
4933
5006
  var res = false;
@@ -4954,7 +5027,7 @@ class Wunderbaum {
4954
5027
  console.error.apply(console, args);
4955
5028
  }
4956
5029
  }
4957
- /* Log to console if opts.debugLevel >= 3 */
5030
+ /** Log to console if opts.debugLevel >= 3 */
4958
5031
  logInfo(...args) {
4959
5032
  if (this.options.debugLevel >= 3) {
4960
5033
  Array.prototype.unshift.call(args, this.toString());
@@ -4981,75 +5054,6 @@ class Wunderbaum {
4981
5054
  console.warn.apply(console, args);
4982
5055
  }
4983
5056
  }
4984
- /** */
4985
- render(opts) {
4986
- const label = this.logTime("render");
4987
- let idx = 0;
4988
- let top = 0;
4989
- const height = ROW_HEIGHT;
4990
- let modified = false;
4991
- let start = opts === null || opts === void 0 ? void 0 : opts.startIdx;
4992
- let end = opts === null || opts === void 0 ? void 0 : opts.endIdx;
4993
- const obsoleteViewNodes = this.viewNodes;
4994
- const newNodesOnly = !!getOption(opts, "newNodesOnly");
4995
- this.viewNodes = new Set();
4996
- let viewNodes = this.viewNodes;
4997
- // this.debug("render", opts);
4998
- assert(start != null && end != null);
4999
- // Make sure start is always even, so the alternating row colors don't
5000
- // change when scrolling:
5001
- if (start % 2) {
5002
- start--;
5003
- }
5004
- this.visitRows(function (node) {
5005
- const prevIdx = node._rowIdx;
5006
- viewNodes.add(node);
5007
- obsoleteViewNodes.delete(node);
5008
- if (prevIdx !== idx) {
5009
- node._rowIdx = idx;
5010
- modified = true;
5011
- }
5012
- if (idx < start || idx > end) {
5013
- node._callEvent("discard");
5014
- node.removeMarkup();
5015
- }
5016
- else if (!node._rowElem || !newNodesOnly) {
5017
- node.render({ top: top });
5018
- // }else{
5019
- // node.log("ignrored render")
5020
- }
5021
- idx++;
5022
- top += height;
5023
- });
5024
- for (const prevNode of obsoleteViewNodes) {
5025
- prevNode._callEvent("discard");
5026
- prevNode.removeMarkup();
5027
- }
5028
- // Resize tree container
5029
- this.nodeListElement.style.height = "" + top + "px";
5030
- // this.log("render()", this.nodeListElement.style.height);
5031
- this.logTimeEnd(label);
5032
- return modified;
5033
- }
5034
- /**Recalc and apply header columns from `this.columns`. */
5035
- renderHeader() {
5036
- if (!this.headerElement) {
5037
- return;
5038
- }
5039
- const headerRow = this.headerElement.querySelector(".wb-row");
5040
- assert(headerRow);
5041
- headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5042
- for (let i = 0; i < this.columns.length; i++) {
5043
- const col = this.columns[i];
5044
- const colElem = headerRow.children[i];
5045
- colElem.style.left = col._ofsPx + "px";
5046
- colElem.style.width = col._widthPx + "px";
5047
- // colElem.textContent = col.title || col.id;
5048
- const title = escapeHtml(col.title || col.id);
5049
- colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
5050
- // colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
5051
- }
5052
- }
5053
5057
  /**
5054
5058
  * Make sure that this node is scrolled into the viewport.
5055
5059
  *
@@ -5083,24 +5087,12 @@ class Wunderbaum {
5083
5087
  this.setModified(ChangeType.vscroll);
5084
5088
  }
5085
5089
  }
5086
- /** */
5087
- setCellMode(mode) {
5088
- // util.assert(this.cellNavMode);
5089
- // util.assert(0 <= colIdx && colIdx < this.columns.length);
5090
- if (mode === this.navMode) {
5091
- return;
5092
- }
5093
- const prevMode = this.navMode;
5094
- const cellMode = mode !== NavigationMode.row;
5095
- this.navMode = mode;
5096
- if (cellMode && prevMode === NavigationMode.row) {
5097
- this.setColumn(0);
5098
- }
5099
- this.element.classList.toggle("wb-cell-mode", cellMode);
5100
- this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
5101
- this.setModified(ChangeType.row, this.activeNode);
5102
- }
5103
- /** */
5090
+ /**
5091
+ * Set column #colIdx to 'active'.
5092
+ *
5093
+ * This higlights the column header and -cells by adding the `wb-active` class.
5094
+ * Available in cell-nav and cell-edit mode, not in row-mode.
5095
+ */
5104
5096
  setColumn(colIdx) {
5105
5097
  assert(this.navMode !== NavigationMode.row);
5106
5098
  assert(0 <= colIdx && colIdx < this.columns.length);
@@ -5125,7 +5117,7 @@ class Wunderbaum {
5125
5117
  }
5126
5118
  }
5127
5119
  }
5128
- /** */
5120
+ /** Set or remove keybaord focus to the tree container. */
5129
5121
  setFocus(flag = true) {
5130
5122
  if (flag) {
5131
5123
  this.element.focus();
@@ -5134,20 +5126,24 @@ class Wunderbaum {
5134
5126
  this.element.blur();
5135
5127
  }
5136
5128
  }
5137
- /* */
5138
5129
  setModified(change, node, options) {
5130
+ if (this._disableUpdateCount) {
5131
+ // Assuming that we redraw all when enableUpdate() is re-enabled.
5132
+ // this.log(
5133
+ // `IGNORED setModified(${change}) node=${node} (disable level ${this._disableUpdateCount})`
5134
+ // );
5135
+ return;
5136
+ }
5137
+ // this.log(`setModified(${change}) node=${node}`);
5139
5138
  if (!(node instanceof WunderbaumNode)) {
5140
5139
  options = node;
5141
5140
  }
5142
- if (this._disableUpdate) {
5143
- return;
5144
- }
5145
5141
  const immediate = !!getOption(options, "immediate");
5146
5142
  switch (change) {
5147
5143
  case ChangeType.any:
5148
5144
  case ChangeType.structure:
5149
5145
  case ChangeType.header:
5150
- this.changeRedrawPending = true;
5146
+ this.changeRedrawRequestPending = true;
5151
5147
  this.updateViewport(immediate);
5152
5148
  break;
5153
5149
  case ChangeType.vscroll:
@@ -5164,84 +5160,111 @@ class Wunderbaum {
5164
5160
  default:
5165
5161
  error(`Invalid change type ${change}`);
5166
5162
  }
5167
- // if (!this.changedSince) {
5168
- // this.changedSince = Date.now();
5169
- // }
5170
- // this.changes.add(change);
5171
- // if (change === ChangeType.structure) {
5172
- // this.changedNodes.clear();
5173
- // } else if (node && !this.changes.has(ChangeType.structure)) {
5174
- // if (this.changedNodes.size < MAX_CHANGED_NODES) {
5175
- // this.changedNodes.add(node);
5176
- // } else {
5177
- // this.changes.add(ChangeType.structure);
5178
- // this.changedNodes.clear();
5179
- // }
5180
- // }
5181
- // this.log("setModified(" + change + ")", node);
5182
5163
  }
5164
+ /** Set the tree's navigation mode. */
5165
+ setNavigationMode(mode) {
5166
+ // util.assert(this.cellNavMode);
5167
+ // util.assert(0 <= colIdx && colIdx < this.columns.length);
5168
+ if (mode === this.navMode) {
5169
+ return;
5170
+ }
5171
+ const prevMode = this.navMode;
5172
+ const cellMode = mode !== NavigationMode.row;
5173
+ this.navMode = mode;
5174
+ if (cellMode && prevMode === NavigationMode.row) {
5175
+ this.setColumn(0);
5176
+ }
5177
+ this.element.classList.toggle("wb-cell-mode", cellMode);
5178
+ this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
5179
+ this.setModified(ChangeType.row, this.activeNode);
5180
+ }
5181
+ /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
5183
5182
  setStatus(status, message, details) {
5184
5183
  return this.root.setStatus(status, message, details);
5185
5184
  }
5186
5185
  /** Update column headers and width. */
5187
5186
  updateColumns(opts) {
5188
- let modified = false;
5189
- let minWidth = 4;
5190
- let vpWidth = this.element.clientWidth;
5187
+ opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
5188
+ const minWidth = 4;
5189
+ const vpWidth = this.element.clientWidth;
5191
5190
  let totalWeight = 0;
5192
5191
  let fixedWidth = 0;
5193
- // Gather width requests
5194
- this._columnsById = {};
5195
- for (let col of this.columns) {
5196
- this._columnsById[col.id] = col;
5197
- let cw = col.width;
5198
- if (!cw || cw === "*") {
5199
- col._weight = 1.0;
5200
- totalWeight += 1.0;
5201
- }
5202
- else if (typeof cw === "number") {
5203
- col._weight = cw;
5204
- totalWeight += cw;
5205
- }
5206
- else if (typeof cw === "string" && cw.endsWith("px")) {
5207
- col._weight = 0;
5208
- let px = parseFloat(cw.slice(0, -2));
5209
- if (col._widthPx != px) {
5210
- modified = true;
5211
- col._widthPx = px;
5192
+ let modified = false;
5193
+ if (opts.calculateCols) {
5194
+ // Gather width requests
5195
+ this._columnsById = {};
5196
+ for (let col of this.columns) {
5197
+ this._columnsById[col.id] = col;
5198
+ let cw = col.width;
5199
+ if (!cw || cw === "*") {
5200
+ col._weight = 1.0;
5201
+ totalWeight += 1.0;
5202
+ }
5203
+ else if (typeof cw === "number") {
5204
+ col._weight = cw;
5205
+ totalWeight += cw;
5206
+ }
5207
+ else if (typeof cw === "string" && cw.endsWith("px")) {
5208
+ col._weight = 0;
5209
+ let px = parseFloat(cw.slice(0, -2));
5210
+ if (col._widthPx != px) {
5211
+ modified = true;
5212
+ col._widthPx = px;
5213
+ }
5214
+ fixedWidth += px;
5215
+ }
5216
+ else {
5217
+ error("Invalid column width: " + cw);
5212
5218
  }
5213
- fixedWidth += px;
5214
5219
  }
5215
- else {
5216
- error("Invalid column width: " + cw);
5217
- }
5218
- }
5219
- // Share remaining space between non-fixed columns
5220
- let restPx = Math.max(0, vpWidth - fixedWidth);
5221
- let ofsPx = 0;
5222
- for (let col of this.columns) {
5223
- if (col._weight) {
5224
- let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5225
- if (col._widthPx != px) {
5226
- modified = true;
5227
- col._widthPx = px;
5220
+ // Share remaining space between non-fixed columns
5221
+ const restPx = Math.max(0, vpWidth - fixedWidth);
5222
+ let ofsPx = 0;
5223
+ for (let col of this.columns) {
5224
+ if (col._weight) {
5225
+ const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5226
+ if (col._widthPx != px) {
5227
+ modified = true;
5228
+ col._widthPx = px;
5229
+ }
5228
5230
  }
5231
+ col._ofsPx = ofsPx;
5232
+ ofsPx += col._widthPx;
5229
5233
  }
5230
- col._ofsPx = ofsPx;
5231
- ofsPx += col._widthPx;
5232
5234
  }
5233
5235
  // Every column has now a calculated `_ofsPx` and `_widthPx`
5234
5236
  // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
5235
5237
  // console.trace();
5236
5238
  // util.error("BREAK");
5237
5239
  if (modified) {
5238
- this.renderHeader();
5239
- if (opts.render !== false) {
5240
- this.render();
5240
+ this._renderHeaderMarkup();
5241
+ if (opts.updateRows) {
5242
+ this._updateRows();
5241
5243
  }
5242
5244
  }
5243
5245
  }
5244
- /** Render all rows that are visible in the viewport. */
5246
+ /** Create/update header markup from `this.columns` definition.
5247
+ * @internal
5248
+ */
5249
+ _renderHeaderMarkup() {
5250
+ if (!this.headerElement) {
5251
+ return;
5252
+ }
5253
+ const headerRow = this.headerElement.querySelector(".wb-row");
5254
+ assert(headerRow);
5255
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5256
+ for (let i = 0; i < this.columns.length; i++) {
5257
+ const col = this.columns[i];
5258
+ const colElem = headerRow.children[i];
5259
+ colElem.style.left = col._ofsPx + "px";
5260
+ colElem.style.width = col._widthPx + "px";
5261
+ // colElem.textContent = col.title || col.id;
5262
+ const title = escapeHtml(col.title || col.id);
5263
+ colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
5264
+ // colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
5265
+ }
5266
+ }
5267
+ /** Render header and all rows that are visible in the viewport (async, throttled). */
5245
5268
  updateViewport(immediate = false) {
5246
5269
  // Call the `throttle` wrapper for `this._updateViewport()` which will
5247
5270
  // execute immediately on the leading edge of a sequence:
@@ -5250,42 +5273,163 @@ class Wunderbaum {
5250
5273
  this._updateViewportThrottled.flush();
5251
5274
  }
5252
5275
  }
5276
+ /**
5277
+ * This is the actual update method, which is wrapped inside a throttle method.
5278
+ * This protected method should not be called directly but via
5279
+ * `tree.updateViewport()` or `tree.setModified()`.
5280
+ * It calls `updateColumns()` and `_updateRows()`.
5281
+ * @internal
5282
+ */
5253
5283
  _updateViewport() {
5254
- if (this._disableUpdate) {
5284
+ if (this._disableUpdateCount) {
5285
+ this.log(`IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`);
5255
5286
  return;
5256
5287
  }
5257
- const newNodesOnly = !this.changeRedrawPending;
5258
- this.changeRedrawPending = false;
5288
+ const newNodesOnly = !this.changeRedrawRequestPending;
5289
+ this.changeRedrawRequestPending = false;
5259
5290
  let height = this.scrollContainer.clientHeight;
5260
- // We cannot get the height for absolut positioned parent, so look at first col
5291
+ // We cannot get the height for absolute positioned parent, so look at first col
5261
5292
  // let headerHeight = this.headerElement.clientHeight
5262
5293
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
5263
5294
  const headerHeight = this.options.headerHeightPx;
5264
5295
  const wantHeight = this.element.clientHeight - headerHeight;
5265
- const ofs = this.scrollContainer.scrollTop;
5266
5296
  if (Math.abs(height - wantHeight) > 1.0) {
5267
5297
  // this.log("resize", height, wantHeight);
5268
5298
  this.scrollContainer.style.height = wantHeight + "px";
5269
5299
  height = wantHeight;
5270
5300
  }
5271
- this.updateColumns({ render: false });
5272
- this.render({
5273
- startIdx: Math.max(0, ofs / ROW_HEIGHT - RENDER_MAX_PREFETCH),
5274
- endIdx: Math.max(0, (ofs + height) / ROW_HEIGHT + RENDER_MAX_PREFETCH),
5275
- newNodesOnly: newNodesOnly,
5276
- });
5301
+ this.updateColumns({ updateRows: false });
5302
+ this._updateRows({ newNodesOnly: newNodesOnly });
5277
5303
  this._callEvent("update");
5278
5304
  }
5279
- /** Call callback(node) for all nodes in hierarchical order (depth-first).
5305
+ /**
5306
+ * Assert that TR order matches the natural node order
5307
+ * @internal
5308
+ */
5309
+ _validateRows() {
5310
+ let trs = this.nodeListElement.childNodes;
5311
+ let i = 0;
5312
+ let prev = -1;
5313
+ let ok = true;
5314
+ trs.forEach((element) => {
5315
+ const tr = element;
5316
+ const top = Number.parseInt(tr.style.top);
5317
+ const n = tr._wb_node;
5318
+ // if (i < 4) {
5319
+ // console.info(
5320
+ // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
5321
+ // );
5322
+ // }
5323
+ if (top <= prev) {
5324
+ console.warn(`TR order mismatch at index ${i}: top=${top}px, node=${n}`);
5325
+ // throw new Error("fault");
5326
+ ok = false;
5327
+ }
5328
+ prev = top;
5329
+ i++;
5330
+ });
5331
+ return ok;
5332
+ }
5333
+ /*
5334
+ * - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
5335
+ * - Store count of rows to `tree.treeRowCount`.
5336
+ * - Renumber `node._rowIdx` for all visible nodes.
5337
+ * - Calculate the index range that must be rendered to fill the viewport
5338
+ * (including upper and lower prefetch)
5339
+ * -
5340
+ */
5341
+ _updateRows(opts) {
5342
+ const label = this.logTime("_updateRows");
5343
+ opts = Object.assign({ newNodesOnly: false }, opts);
5344
+ const newNodesOnly = !!opts.newNodesOnly;
5345
+ const row_height = ROW_HEIGHT;
5346
+ const vp_height = this.scrollContainer.clientHeight;
5347
+ const prefetch = RENDER_MAX_PREFETCH;
5348
+ const ofs = this.scrollContainer.scrollTop;
5349
+ let startIdx = Math.max(0, ofs / row_height - prefetch);
5350
+ startIdx = Math.floor(startIdx);
5351
+ // Make sure start is always even, so the alternating row colors don't
5352
+ // change when scrolling:
5353
+ if (startIdx % 2) {
5354
+ startIdx--;
5355
+ }
5356
+ let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
5357
+ endIdx = Math.ceil(endIdx);
5358
+ // const obsoleteViewNodes = this.viewNodes;
5359
+ // this.viewNodes = new Set();
5360
+ // const viewNodes = this.viewNodes;
5361
+ // this.debug("render", opts);
5362
+ const obsoleteNodes = new Set();
5363
+ this.nodeListElement.childNodes.forEach((elem) => {
5364
+ const tr = elem;
5365
+ obsoleteNodes.add(tr._wb_node);
5366
+ });
5367
+ let idx = 0;
5368
+ let top = 0;
5369
+ let modified = false;
5370
+ let prevElem = "first";
5371
+ this.visitRows(function (node) {
5372
+ // console.log("visit", node)
5373
+ const rowDiv = node._rowElem;
5374
+ // Renumber all expanded nodes
5375
+ if (node._rowIdx !== idx) {
5376
+ node._rowIdx = idx;
5377
+ modified = true;
5378
+ }
5379
+ if (idx < startIdx || idx > endIdx) {
5380
+ // row is outside viewport bounds
5381
+ if (rowDiv) {
5382
+ prevElem = rowDiv;
5383
+ }
5384
+ }
5385
+ else if (rowDiv && newNodesOnly) {
5386
+ obsoleteNodes.delete(node);
5387
+ // no need to update existing node markup
5388
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
5389
+ prevElem = rowDiv;
5390
+ }
5391
+ else {
5392
+ obsoleteNodes.delete(node);
5393
+ // Create new markup
5394
+ node.render({ top: top, after: prevElem });
5395
+ // console.log("render", top, prevElem, "=>", node._rowElem);
5396
+ prevElem = node._rowElem;
5397
+ }
5398
+ idx++;
5399
+ top += row_height;
5400
+ });
5401
+ this.treeRowCount = idx;
5402
+ for (const n of obsoleteNodes) {
5403
+ n._callEvent("discard");
5404
+ n.removeMarkup();
5405
+ }
5406
+ // Resize tree container
5407
+ this.nodeListElement.style.height = `${top}px`;
5408
+ // this.log(
5409
+ // `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
5410
+ // this.nodeListElement.style.height
5411
+ // );
5412
+ this.logTimeEnd(label);
5413
+ this._validateRows();
5414
+ return modified;
5415
+ }
5416
+ /**
5417
+ * Call callback(node) for all nodes in hierarchical order (depth-first).
5280
5418
  *
5281
5419
  * @param {function} callback the callback function.
5282
- * Return false to stop iteration, return "skip" to skip this node and children only.
5420
+ * Return false to stop iteration, return "skip" to skip this node and
5421
+ * children only.
5283
5422
  * @returns {boolean} false, if the iterator was stopped.
5284
5423
  */
5285
5424
  visit(callback) {
5286
5425
  return this.root.visit(callback, false);
5287
5426
  }
5288
- /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
5427
+ /**
5428
+ * Call fn(node) for all nodes in vertical order, top down (or bottom up).
5429
+ *
5430
+ * Note that this considers expansion state, i.e. children of collapsed nodes
5431
+ * are skipped.
5432
+ *
5289
5433
  * Stop iteration, if fn() returns false.<br>
5290
5434
  * Return false if iteration was stopped.
5291
5435
  *
@@ -5365,7 +5509,8 @@ class Wunderbaum {
5365
5509
  }
5366
5510
  return true;
5367
5511
  }
5368
- /** Call fn(node) for all nodes in vertical order, bottom up.
5512
+ /**
5513
+ * Call fn(node) for all nodes in vertical order, bottom up.
5369
5514
  * @internal
5370
5515
  */
5371
5516
  _visitRowsUp(callback, opts) {
@@ -5409,19 +5554,36 @@ class Wunderbaum {
5409
5554
  }
5410
5555
  return true;
5411
5556
  }
5412
- /** . */
5557
+ /**
5558
+ * Reload the tree with a new source.
5559
+ *
5560
+ * Previous data is cleared.
5561
+ * Pass `options.columns` to define a header (may also be part of `source.columns`).
5562
+ */
5413
5563
  load(source, options = {}) {
5414
5564
  this.clear();
5415
5565
  const columns = options.columns || source.columns;
5416
5566
  if (columns) {
5417
5567
  this.columns = options.columns;
5418
- this.renderHeader();
5419
- // this.updateColumns({ render: false });
5568
+ // this._renderHeaderMarkup();
5569
+ this.updateColumns({ calculateCols: false });
5420
5570
  }
5421
5571
  return this.root.load(source);
5422
5572
  }
5423
5573
  /**
5574
+ * Disable render requests during operations that would trigger many updates.
5424
5575
  *
5576
+ * ```js
5577
+ * try {
5578
+ * tree.enableUpdate(false);
5579
+ * // ... (long running operation that would trigger many updates)
5580
+ * foo();
5581
+ * // ... NOTE: make sure that async operations have finished
5582
+ * await foo();
5583
+ * } finally {
5584
+ * tree.enableUpdate(true);
5585
+ * }
5586
+ * ```
5425
5587
  */
5426
5588
  enableUpdate(flag) {
5427
5589
  /*
@@ -5429,20 +5591,22 @@ class Wunderbaum {
5429
5591
  1 >-------------------------------------<
5430
5592
  2 >--------------------<
5431
5593
  3 >--------------------------<
5432
-
5433
- 5
5434
-
5435
5594
  */
5436
- // this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
5437
5595
  if (flag) {
5438
- assert(this._disableUpdateCount > 0);
5596
+ assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often");
5439
5597
  this._disableUpdateCount--;
5598
+ // this.logDebug(
5599
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5600
+ // );
5440
5601
  if (this._disableUpdateCount === 0) {
5441
5602
  this.updateViewport();
5442
5603
  }
5443
5604
  }
5444
5605
  else {
5445
5606
  this._disableUpdateCount++;
5607
+ // this.logDebug(
5608
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5609
+ // );
5446
5610
  // this._disableUpdate = Date.now();
5447
5611
  }
5448
5612
  // return !flag; // return previous value
@@ -5475,8 +5639,10 @@ class Wunderbaum {
5475
5639
  return this.extensions.filter.updateFilter();
5476
5640
  }
5477
5641
  }
5478
- Wunderbaum.version = "v0.0.2"; // Set to semver by 'grunt release'
5479
5642
  Wunderbaum.sequence = 0;
5643
+ /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
5644
+ Wunderbaum.version = "v0.0.3"; // Set to semver by 'grunt release'
5645
+ /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
5480
5646
  Wunderbaum.util = util;
5481
5647
 
5482
5648
  export { Wunderbaum };