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.
@@ -7,8 +7,9 @@
7
7
  /*!
8
8
  * Wunderbaum - util
9
9
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
10
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
10
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
11
11
  */
12
+ /** @module util */
12
13
  /** Readable names for `MouseEvent.button` */
13
14
  const MOUSE_BUTTONS = {
14
15
  0: "",
@@ -619,7 +620,7 @@
619
620
  /*!
620
621
  * Wunderbaum - common
621
622
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
622
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
623
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
623
624
  */
624
625
  const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
625
626
  const ROW_HEIGHT = 22;
@@ -707,6 +708,8 @@
707
708
  Home: "firstCol",
708
709
  "Control+End": "last",
709
710
  "Control+Home": "first",
711
+ "Meta+ArrowDown": "last",
712
+ "Meta+ArrowUp": "first",
710
713
  "*": "expandAll",
711
714
  Multiply: "expandAll",
712
715
  PageDown: "pageDown",
@@ -733,7 +736,7 @@
733
736
  /*!
734
737
  * Wunderbaum - wb_extension_base
735
738
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
736
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
739
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
737
740
  */
738
741
  class WunderbaumExtension {
739
742
  constructor(tree, id, defaults) {
@@ -1088,7 +1091,7 @@
1088
1091
  /*!
1089
1092
  * Wunderbaum - ext-filter
1090
1093
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1091
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1094
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1092
1095
  */
1093
1096
  const START_MARKER = "\uFFF7";
1094
1097
  const END_MARKER = "\uFFF8";
@@ -1382,7 +1385,7 @@
1382
1385
  /*!
1383
1386
  * Wunderbaum - ext-keynav
1384
1387
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1385
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1388
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1386
1389
  */
1387
1390
  class KeynavExtension extends WunderbaumExtension {
1388
1391
  constructor(tree) {
@@ -1449,7 +1452,7 @@
1449
1452
  eventName = "Add"; // expand
1450
1453
  }
1451
1454
  else if (navModeOption === NavigationModeOption.startRow) {
1452
- tree.setCellMode(NavigationMode.cellNav);
1455
+ tree.setNavigationMode(NavigationMode.cellNav);
1453
1456
  return;
1454
1457
  }
1455
1458
  break;
@@ -1488,6 +1491,8 @@
1488
1491
  case "Home":
1489
1492
  case "Control+End":
1490
1493
  case "Control+Home":
1494
+ case "Meta+ArrowDown":
1495
+ case "Meta+ArrowUp":
1491
1496
  case "PageDown":
1492
1497
  case "PageUp":
1493
1498
  node.navigate(eventName, { activate: activate, event: event });
@@ -1519,11 +1524,11 @@
1519
1524
  break;
1520
1525
  case "Escape":
1521
1526
  if (tree.navMode === NavigationMode.cellEdit) {
1522
- tree.setCellMode(NavigationMode.cellNav);
1527
+ tree.setNavigationMode(NavigationMode.cellNav);
1523
1528
  handled = true;
1524
1529
  }
1525
1530
  else if (tree.navMode === NavigationMode.cellNav) {
1526
- tree.setCellMode(NavigationMode.row);
1531
+ tree.setNavigationMode(NavigationMode.row);
1527
1532
  handled = true;
1528
1533
  }
1529
1534
  break;
@@ -1533,7 +1538,7 @@
1533
1538
  handled = true;
1534
1539
  }
1535
1540
  else if (navModeOption !== NavigationModeOption.cell) {
1536
- tree.setCellMode(NavigationMode.row);
1541
+ tree.setNavigationMode(NavigationMode.row);
1537
1542
  handled = true;
1538
1543
  }
1539
1544
  break;
@@ -1550,6 +1555,8 @@
1550
1555
  case "Home":
1551
1556
  case "Control+End":
1552
1557
  case "Control+Home":
1558
+ case "Meta+ArrowDown":
1559
+ case "Meta+ArrowUp":
1553
1560
  case "PageDown":
1554
1561
  case "PageUp":
1555
1562
  node.navigate(eventName, { activate: activate, event: event });
@@ -1568,7 +1575,7 @@
1568
1575
  /*!
1569
1576
  * Wunderbaum - ext-logger
1570
1577
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1571
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1578
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1572
1579
  */
1573
1580
  class LoggerExtension extends WunderbaumExtension {
1574
1581
  constructor(tree) {
@@ -1608,7 +1615,7 @@
1608
1615
  /*!
1609
1616
  * Wunderbaum - ext-dnd
1610
1617
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1611
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1618
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1612
1619
  */
1613
1620
  const nodeMimeType = "application/x-wunderbaum-node";
1614
1621
  class DndExtension extends WunderbaumExtension {
@@ -1876,7 +1883,7 @@
1876
1883
  /*!
1877
1884
  * Wunderbaum - drag_observer
1878
1885
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1879
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
1886
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1880
1887
  */
1881
1888
  /**
1882
1889
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2010,7 +2017,7 @@
2010
2017
  /*!
2011
2018
  * Wunderbaum - ext-grid
2012
2019
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2013
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
2020
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2014
2021
  */
2015
2022
  class GridExtension extends WunderbaumExtension {
2016
2023
  constructor(tree) {
@@ -2047,7 +2054,7 @@
2047
2054
  /*!
2048
2055
  * Wunderbaum - deferred
2049
2056
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2050
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
2057
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2051
2058
  */
2052
2059
  /**
2053
2060
  * Deferred is a ES6 Promise, that exposes the resolve() and reject()` method.
@@ -2090,7 +2097,7 @@
2090
2097
  /*!
2091
2098
  * Wunderbaum - wunderbaum_node
2092
2099
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2093
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
2100
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2094
2101
  */
2095
2102
  /** Top-level properties that can be passed with `data`. */
2096
2103
  const NODE_PROPS = new Set([
@@ -2127,15 +2134,31 @@
2127
2134
  "unselectableIgnore",
2128
2135
  "unselectableStatus",
2129
2136
  ]);
2137
+ /**
2138
+ * A single tree node.
2139
+ *
2140
+ * **NOTE:** <br>
2141
+ * Generally you should not modify properties directly, since this may break
2142
+ * the internal bookkeeping.
2143
+ */
2130
2144
  class WunderbaumNode {
2131
2145
  constructor(tree, parent, data) {
2132
2146
  var _a, _b;
2147
+ /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
2148
+ * times within a tree (in this case we have 'clone nodes').
2149
+ * @see Use {@link setKey} to modify.
2150
+ */
2133
2151
  this.refKey = undefined;
2134
2152
  this.children = null;
2135
2153
  this.lazy = false;
2154
+ /** Expansion state.
2155
+ * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */
2136
2156
  this.expanded = false;
2157
+ /** Selection state.
2158
+ * @see {@link isSelected}, {@link setSelected}. */
2137
2159
  this.selected = false;
2138
- /** Additional classes added to `div.wb-row`. */
2160
+ /** Additional classes added to `div.wb-row`.
2161
+ * @see {@link addClass}, {@link removeClass}, {@link toggleClass}. */
2139
2162
  this.extraClasses = new Set();
2140
2163
  /** Custom data that was passed to the constructor */
2141
2164
  this.data = {};
@@ -2301,7 +2324,8 @@
2301
2324
  }
2302
2325
  /**
2303
2326
  * Apply a modification (or navigation) operation.
2304
- * @see Wunderbaum#applyCommand
2327
+ *
2328
+ * @see {@link Wunderbaum.applyCommand}
2305
2329
  */
2306
2330
  applyCommand(cmd, opts) {
2307
2331
  return this.tree.applyCommand(cmd, this, opts);
@@ -2394,8 +2418,7 @@
2394
2418
  }
2395
2419
  /** Find a node relative to self.
2396
2420
  *
2397
- * @param where The keyCode that would normally trigger this move,
2398
- * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up').
2421
+ * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()}
2399
2422
  */
2400
2423
  findRelatedNode(where, includeHidden = false) {
2401
2424
  return this.tree.findRelatedNode(this, where, includeHidden);
@@ -2696,7 +2719,7 @@
2696
2719
  assert(!this.parent);
2697
2720
  tree.columns = data.columns;
2698
2721
  delete data.columns;
2699
- tree.renderHeader();
2722
+ tree.updateColumns({ calculateCols: false });
2700
2723
  }
2701
2724
  this._loadSourceObject(data);
2702
2725
  }
@@ -2732,19 +2755,20 @@
2732
2755
  this.setStatus(NodeStatusType.ok);
2733
2756
  return;
2734
2757
  }
2735
- assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}` or false.");
2758
+ assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false.");
2736
2759
  await this.load(source); // also calls setStatus('ok')
2737
2760
  if (wasExpanded) {
2738
2761
  this.expanded = true;
2739
2762
  this.tree.setModified(ChangeType.structure);
2740
2763
  }
2741
2764
  else {
2742
- this.render(); // Fix expander icon to 'loaded'
2765
+ this.setModified(); // Fix expander icon to 'loaded'
2743
2766
  }
2744
2767
  }
2745
2768
  catch (e) {
2769
+ this.logError("Error during loadLazy()", e);
2770
+ this._callEvent("error", { error: e });
2746
2771
  this.setStatus(NodeStatusType.error, "" + e);
2747
- // } finally {
2748
2772
  }
2749
2773
  return;
2750
2774
  }
@@ -2912,31 +2936,33 @@
2912
2936
  // Allow to pass 'ArrowLeft' instead of 'left'
2913
2937
  where = KEY_TO_ACTION_DICT[where] || where;
2914
2938
  // Otherwise activate or focus the related node
2915
- let node = this.findRelatedNode(where);
2916
- if (node) {
2917
- // setFocus/setActive will scroll later (if autoScroll is specified)
2918
- try {
2919
- node.makeVisible({ scrollIntoView: false });
2920
- }
2921
- catch (e) { } // #272
2922
- node.setFocus();
2923
- if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2924
- return Promise.resolve(this);
2925
- }
2926
- return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2939
+ const node = this.findRelatedNode(where);
2940
+ if (!node) {
2941
+ this.logWarn(`Could not find related node '${where}'.`);
2942
+ return Promise.resolve(this);
2927
2943
  }
2928
- this.logWarn("Could not find related node '" + where + "'.");
2929
- return Promise.resolve(this);
2944
+ // setFocus/setActive will scroll later (if autoScroll is specified)
2945
+ try {
2946
+ node.makeVisible({ scrollIntoView: false });
2947
+ }
2948
+ catch (e) { } // #272
2949
+ node.setFocus();
2950
+ if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2951
+ return Promise.resolve(this);
2952
+ }
2953
+ return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2930
2954
  }
2931
2955
  /** Delete this node and all descendants. */
2932
2956
  remove() {
2933
2957
  const tree = this.tree;
2934
2958
  const pos = this.parent.children.indexOf(this);
2959
+ this.triggerModify("remove");
2935
2960
  this.parent.children.splice(pos, 1);
2936
2961
  this.visit((n) => {
2937
2962
  n.removeMarkup();
2938
2963
  tree._unregisterNode(n);
2939
2964
  }, true);
2965
+ tree.setModified(ChangeType.structure);
2940
2966
  }
2941
2967
  /** Remove all descendants of this node. */
2942
2968
  removeChildren() {
@@ -3061,6 +3087,7 @@
3061
3087
  const activeColIdx = tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
3062
3088
  // let colElems: HTMLElement[];
3063
3089
  const isNew = !rowDiv;
3090
+ assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
3064
3091
  assert(!this.isRootNode());
3065
3092
  //
3066
3093
  let rowClasses = ["wb-row"];
@@ -3112,7 +3139,7 @@
3112
3139
  nodeElem.appendChild(elem);
3113
3140
  ofsTitlePx += ICON_WIDTH;
3114
3141
  }
3115
- if (treeOptions.minExpandLevel && level > treeOptions.minExpandLevel) {
3142
+ if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
3116
3143
  expanderSpan = document.createElement("i");
3117
3144
  nodeElem.appendChild(expanderSpan);
3118
3145
  ofsTitlePx += ICON_WIDTH;
@@ -3241,9 +3268,19 @@
3241
3268
  });
3242
3269
  }
3243
3270
  // Attach to DOM as late as possible
3244
- // if (!this._rowElem) {
3245
- tree.nodeListElement.appendChild(rowDiv);
3246
- // }
3271
+ if (isNew) {
3272
+ const after = opts ? opts.after : "last";
3273
+ switch (after) {
3274
+ case "first":
3275
+ tree.nodeListElement.prepend(rowDiv);
3276
+ break;
3277
+ case "last":
3278
+ tree.nodeListElement.appendChild(rowDiv);
3279
+ break;
3280
+ default:
3281
+ opts.after.after(rowDiv);
3282
+ }
3283
+ }
3247
3284
  }
3248
3285
  /**
3249
3286
  * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
@@ -3317,14 +3354,15 @@
3317
3354
  *
3318
3355
  * Evaluation sequence:
3319
3356
  *
3320
- * If `tree.options.<name>` is a callback that returns something, use that.
3321
- * Else if `node.<name>` is defined, use that.
3322
- * Else if `tree.types[<node.type>]` is a value, use that.
3323
- * Else if `tree.options.<name>` is a value, use that.
3324
- * Else use `defaultValue`.
3357
+ * - If `tree.options.<name>` is a callback that returns something, use that.
3358
+ * - Else if `node.<name>` is defined, use that.
3359
+ * - Else if `tree.types[<node.type>]` is a value, use that.
3360
+ * - Else if `tree.options.<name>` is a value, use that.
3361
+ * - Else use `defaultValue`.
3325
3362
  *
3326
3363
  * @param name name of the option property (on node and tree)
3327
3364
  * @param defaultValue return this if nothing else matched
3365
+ * {@link Wunderbaum.getOption|Wunderbaum.getOption()}
3328
3366
  */
3329
3367
  getOption(name, defaultValue) {
3330
3368
  let tree = this.tree;
@@ -3359,15 +3397,21 @@
3359
3397
  // Use value from value options dict, fallback do default
3360
3398
  return value !== null && value !== void 0 ? value : defaultValue;
3361
3399
  }
3400
+ /** Make sure that this node is visible in the viewport.
3401
+ * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo()}
3402
+ */
3362
3403
  async scrollIntoView(options) {
3363
3404
  return this.tree.scrollTo(this);
3364
3405
  }
3406
+ /**
3407
+ * Activate this node, deactivate previous, send events, activate column and scroll int viewport.
3408
+ */
3365
3409
  async setActive(flag = true, options) {
3366
3410
  const tree = this.tree;
3367
3411
  const prev = tree.activeNode;
3368
3412
  const retrigger = options === null || options === void 0 ? void 0 : options.retrigger;
3369
- const noEvent = options === null || options === void 0 ? void 0 : options.noEvent;
3370
- if (!noEvent) {
3413
+ const noEvents = options === null || options === void 0 ? void 0 : options.noEvents;
3414
+ if (!noEvents) {
3371
3415
  let orgEvent = options === null || options === void 0 ? void 0 : options.event;
3372
3416
  if (flag) {
3373
3417
  if (prev !== this || retrigger) {
@@ -3405,19 +3449,18 @@
3405
3449
  // requestAnimationFrame(() => {
3406
3450
  // this.scrollIntoView();
3407
3451
  // })
3408
- this.scrollIntoView();
3409
- }
3410
- setModified(change = ChangeType.status) {
3411
- assert(change === ChangeType.status);
3412
- this.tree.setModified(ChangeType.row, this);
3452
+ return this.scrollIntoView();
3413
3453
  }
3454
+ /**
3455
+ * Expand or collapse this node.
3456
+ */
3414
3457
  async setExpanded(flag = true, options) {
3415
3458
  // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
3416
3459
  if (!flag &&
3417
3460
  this.isExpanded() &&
3418
3461
  this.getLevel() < this.getOption("minExpandLevel") &&
3419
3462
  !getOption(options, "force")) {
3420
- this.logDebug("Ignored collapse request.");
3463
+ this.logDebug("Ignored collapse request below expandLevel.");
3421
3464
  return;
3422
3465
  }
3423
3466
  if (flag && this.lazy && this.children == null) {
@@ -3426,16 +3469,31 @@
3426
3469
  this.expanded = flag;
3427
3470
  this.tree.setModified(ChangeType.structure);
3428
3471
  }
3429
- setIcon() {
3430
- throw new Error("Not yet implemented");
3431
- // this.setDirty(ChangeType.status);
3432
- }
3472
+ /**
3473
+ * Set keyboard focus here.
3474
+ * @see {@link setActive}
3475
+ */
3433
3476
  setFocus(flag = true, options) {
3434
3477
  const prev = this.tree.focusNode;
3435
3478
  this.tree.focusNode = this;
3436
3479
  prev === null || prev === void 0 ? void 0 : prev.setModified();
3437
3480
  this.setModified();
3438
3481
  }
3482
+ /** Set a new icon path or class. */
3483
+ setIcon() {
3484
+ throw new Error("Not yet implemented");
3485
+ // this.setModified();
3486
+ }
3487
+ /** Change node's {@link key} and/or {@link refKey}. */
3488
+ setKey(key, refKey) {
3489
+ throw new Error("Not yet implemented");
3490
+ }
3491
+ /** Schedule a render, typically called to update after a status or data change. */
3492
+ setModified(change = ChangeType.status) {
3493
+ assert(change === ChangeType.status);
3494
+ this.tree.setModified(ChangeType.row, this);
3495
+ }
3496
+ /** Modify the check/uncheck state. */
3439
3497
  setSelected(flag = true, options) {
3440
3498
  const prev = this.selected;
3441
3499
  if (!!flag !== prev) {
@@ -3444,10 +3502,9 @@
3444
3502
  this.selected = !!flag;
3445
3503
  this.setModified();
3446
3504
  }
3447
- /** Show node status (ok, loading, error, noData) using styles and a dummy child node.
3448
- */
3505
+ /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
3449
3506
  setStatus(status, message, details) {
3450
- let tree = this.tree;
3507
+ const tree = this.tree;
3451
3508
  let statusNode = null;
3452
3509
  const _clearStatusNode = () => {
3453
3510
  // Remove dedicated dummy node, if any
@@ -3521,6 +3578,7 @@
3521
3578
  tree.setModified(ChangeType.structure);
3522
3579
  return statusNode;
3523
3580
  }
3581
+ /** Rename this node. */
3524
3582
  setTitle(title) {
3525
3583
  this.title = title;
3526
3584
  this.setModified();
@@ -3544,10 +3602,16 @@
3544
3602
  * @param {object} [extra]
3545
3603
  */
3546
3604
  triggerModify(operation, extra) {
3605
+ if (!this.parent) {
3606
+ return;
3607
+ }
3547
3608
  this.parent.triggerModifyChild(operation, this, extra);
3548
3609
  }
3549
- /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br>
3550
- * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
3610
+ /**
3611
+ * Call fn(node) for all child nodes in hierarchical order (depth-first).
3612
+ *
3613
+ * Stop iteration, if fn() returns false. Skip current branch, if fn()
3614
+ * returns "skip".<br>
3551
3615
  * Return false if iteration was stopped.
3552
3616
  *
3553
3617
  * @param {function} callback the callback function.
@@ -3591,7 +3655,8 @@
3591
3655
  }
3592
3656
  return true;
3593
3657
  }
3594
- /** Call fn(node) for all sibling nodes.<br>
3658
+ /**
3659
+ * Call fn(node) for all sibling nodes.<br>
3595
3660
  * Stop iteration, if fn() returns false.<br>
3596
3661
  * Return false if iteration was stopped.
3597
3662
  *
@@ -3622,7 +3687,7 @@
3622
3687
  /*!
3623
3688
  * Wunderbaum - ext-edit
3624
3689
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3625
- * v0.0.2, Tue, 12 Apr 2022 18:36:21 GMT (https://github.com/mar10/wunderbaum)
3690
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
3626
3691
  */
3627
3692
  // const START_MARKER = "\uFFF7";
3628
3693
  class EditExtension extends WunderbaumExtension {
@@ -3740,7 +3805,7 @@
3740
3805
  break;
3741
3806
  case "F2":
3742
3807
  if (trigger.indexOf("F2") >= 0) {
3743
- // tree.setCellMode(NavigationMode.cellEdit);
3808
+ // tree.setNavigationMode(NavigationMode.cellEdit);
3744
3809
  this.startEditTitle();
3745
3810
  return false;
3746
3811
  }
@@ -3781,6 +3846,7 @@
3781
3846
  if (validity) {
3782
3847
  // Permanently apply input validations (CSS and tooltip)
3783
3848
  inputElem.addEventListener("keydown", (e) => {
3849
+ inputElem.setCustomValidity("");
3784
3850
  if (!inputElem.reportValidity()) ;
3785
3851
  });
3786
3852
  }
@@ -3821,6 +3887,11 @@
3821
3887
  }
3822
3888
  node.logDebug(`stopEditTitle(${apply})`, opts, focusElem, newValue);
3823
3889
  if (apply && newValue !== null && newValue !== node.title) {
3890
+ const errMsg = focusElem.validationMessage;
3891
+ if (errMsg) {
3892
+ // input element's native validation failed
3893
+ throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`);
3894
+ }
3824
3895
  const colElem = node.getColElem(0);
3825
3896
  this._applyChange("edit.apply", node, colElem, {
3826
3897
  oldValue: node.title,
@@ -3907,8 +3978,8 @@
3907
3978
  * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
3908
3979
  * Released under the MIT license.
3909
3980
  *
3910
- * @version v0.0.2
3911
- * @date Tue, 12 Apr 2022 18:36:21 GMT
3981
+ * @version v0.0.3
3982
+ * @date Mon, 18 Apr 2022 11:52:44 GMT
3912
3983
  */
3913
3984
  // const class_prefix = "wb-";
3914
3985
  // const node_props: string[] = ["title", "key", "refKey"];
@@ -3924,37 +3995,43 @@
3924
3995
  this.extensions = {};
3925
3996
  this.keyMap = new Map();
3926
3997
  this.refKeyMap = new Map();
3927
- this.viewNodes = new Set();
3928
- // protected rows: WunderbaumNode[] = [];
3929
- // protected _rowCount = 0;
3998
+ // protected viewNodes = new Set<WunderbaumNode>();
3999
+ this.treeRowCount = 0;
4000
+ this._disableUpdateCount = 0;
3930
4001
  // protected eventHandlers : Array<function> = [];
4002
+ /** Currently active node if any. */
3931
4003
  this.activeNode = null;
4004
+ /** Current node hat has keyboard focus if any. */
3932
4005
  this.focusNode = null;
3933
- this._disableUpdate = 0;
3934
- this._disableUpdateCount = 0;
3935
4006
  /** Shared properties, referenced by `node.type`. */
3936
4007
  this.types = {};
3937
4008
  /** List of column definitions. */
3938
4009
  this.columns = [];
3939
4010
  this._columnsById = {};
3940
4011
  // Modification Status
3941
- this.changedSince = 0;
3942
- this.changes = new Set();
3943
- this.changedNodes = new Set();
3944
- this.changeRedrawPending = false;
4012
+ // protected changedSince = 0;
4013
+ // protected changes = new Set<ChangeType>();
4014
+ // protected changedNodes = new Set<WunderbaumNode>();
4015
+ this.changeRedrawRequestPending = false;
4016
+ /** Expose some useful methods of the util.ts module as `tree._util`. */
4017
+ this._util = util;
3945
4018
  // --- FILTER ---
3946
4019
  this.filterMode = null;
3947
4020
  // --- KEYNAV ---
4021
+ /** @internal Use `setColumn()`/`getActiveColElem()`*/
3948
4022
  this.activeColIdx = 0;
4023
+ /** @internal */
3949
4024
  this.navMode = NavigationMode.row;
4025
+ /** @internal */
3950
4026
  this.lastQuicksearchTime = 0;
4027
+ /** @internal */
3951
4028
  this.lastQuicksearchTerm = "";
3952
4029
  // --- EDIT ---
3953
4030
  this.lastClickTime = 0;
3954
- // TODO: make accessible in compiled JS like this?
3955
- this._util = util;
3956
- /** Alias for `logDebug` */
3957
- this.log = this.logDebug; // Alias
4031
+ /** Alias for {@link Wunderbaum.logDebug}.
4032
+ * @alias Wunderbaum.logDebug
4033
+ */
4034
+ this.log = this.logDebug;
3958
4035
  let opts = (this.options = extend({
3959
4036
  id: null,
3960
4037
  source: null,
@@ -4188,37 +4265,18 @@
4188
4265
  forceClose: true,
4189
4266
  });
4190
4267
  }
4191
- // if (flag && !this.activeNode ) {
4192
- // setTimeout(() => {
4193
- // if (!this.activeNode) {
4194
- // const firstNode = this.getFirstChild();
4195
- // if (firstNode && !firstNode?.isStatusNode()) {
4196
- // firstNode.logInfo("Activate on focus", e);
4197
- // firstNode.setActive(true, { event: e });
4198
- // }
4199
- // }
4200
- // }, 10);
4201
- // }
4202
4268
  });
4203
4269
  }
4204
- /** */
4205
- // _renderHeader(){
4206
- // const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
4207
- // this.element.innerHTML = `
4208
- // <div class='wb-header'>
4209
- // <div class='wb-row'>
4210
- // ${coldivs}
4211
- // </div>
4212
- // </div>`;
4213
- // }
4214
- /** Return a Wunderbaum instance, from element, index, or event.
4270
+ /**
4271
+ * Return a Wunderbaum instance, from element, id, index, or event.
4215
4272
  *
4216
- * @example
4217
- * getTree(); // Get first Wunderbaum instance on page
4218
- * getTree(1); // Get second Wunderbaum instance on page
4219
- * getTree(event); // Get tree for this mouse- or keyboard event
4220
- * getTree("foo"); // Get tree for this `tree.options.id`
4273
+ * ```js
4274
+ * getTree(); // Get first Wunderbaum instance on page
4275
+ * getTree(1); // Get second Wunderbaum instance on page
4276
+ * getTree(event); // Get tree for this mouse- or keyboard event
4277
+ * getTree("foo"); // Get tree for this `tree.options.id`
4221
4278
  * getTree("#tree"); // Get tree for this matching element
4279
+ * ```
4222
4280
  */
4223
4281
  static getTree(el) {
4224
4282
  if (el instanceof Wunderbaum) {
@@ -4259,9 +4317,8 @@
4259
4317
  }
4260
4318
  return null;
4261
4319
  }
4262
- /** Return a WunderbaumNode instance from element, event.
4263
- *
4264
- * @param el
4320
+ /**
4321
+ * Return a WunderbaumNode instance from element or event.
4265
4322
  */
4266
4323
  static getNode(el) {
4267
4324
  if (!el) {
@@ -4283,7 +4340,7 @@
4283
4340
  }
4284
4341
  return null;
4285
4342
  }
4286
- /** */
4343
+ /** @internal */
4287
4344
  _registerExtension(extension) {
4288
4345
  this.extensionList.push(extension);
4289
4346
  this.extensions[extension.id] = extension;
@@ -4325,7 +4382,7 @@
4325
4382
  node.tree = null;
4326
4383
  node.parent = null;
4327
4384
  // node.title = "DISPOSED: " + node.title
4328
- this.viewNodes.delete(node);
4385
+ // this.viewNodes.delete(node);
4329
4386
  node.removeMarkup();
4330
4387
  }
4331
4388
  /** Call all hook methods of all registered extensions.*/
@@ -4343,7 +4400,9 @@
4343
4400
  }
4344
4401
  return res;
4345
4402
  }
4346
- /** Call tree method or extension method if defined.
4403
+ /**
4404
+ * Call tree method or extension method if defined.
4405
+ *
4347
4406
  * Example:
4348
4407
  * ```js
4349
4408
  * tree._callMethod("edit.startEdit", "arg1", "arg2")
@@ -4360,7 +4419,9 @@
4360
4419
  this.logError(`Calling undefined method '${name}()'.`);
4361
4420
  }
4362
4421
  }
4363
- /** Call event handler if defined in tree.options.
4422
+ /**
4423
+ * Call event handler if defined in tree or tree.EXTENSION options.
4424
+ *
4364
4425
  * Example:
4365
4426
  * ```js
4366
4427
  * tree._callEvent("edit.beforeEdit", {foo: 42})
@@ -4376,27 +4437,33 @@
4376
4437
  // this.logError(`Triggering undefined event '${name}'.`)
4377
4438
  }
4378
4439
  }
4379
- /** Return the topmost visible node in the viewport */
4380
- _firstNodeInView(complete = true) {
4381
- let topIdx, node;
4382
- if (complete) {
4383
- topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
4384
- }
4385
- else {
4386
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4387
- }
4440
+ /** Return the node for given row index. */
4441
+ _getNodeByRowIdx(idx) {
4388
4442
  // TODO: start searching from active node (reverse)
4443
+ let node = null;
4389
4444
  this.visitRows((n) => {
4390
- if (n._rowIdx === topIdx) {
4445
+ if (n._rowIdx === idx) {
4391
4446
  node = n;
4392
4447
  return false;
4393
4448
  }
4394
4449
  });
4395
4450
  return node;
4396
4451
  }
4397
- /** Return the lowest visible node in the viewport */
4452
+ /** Return the topmost visible node in the viewport. */
4453
+ _firstNodeInView(complete = true) {
4454
+ let topIdx;
4455
+ const gracePy = 1; // ignore subpixel scrolling
4456
+ if (complete) {
4457
+ topIdx = Math.ceil((this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT);
4458
+ }
4459
+ else {
4460
+ topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4461
+ }
4462
+ return this._getNodeByRowIdx(topIdx);
4463
+ }
4464
+ /** Return the lowest visible node in the viewport. */
4398
4465
  _lastNodeInView(complete = true) {
4399
- let bottomIdx, node;
4466
+ let bottomIdx;
4400
4467
  if (complete) {
4401
4468
  bottomIdx =
4402
4469
  Math.floor((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
@@ -4407,16 +4474,10 @@
4407
4474
  Math.ceil((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
4408
4475
  ROW_HEIGHT) - 1;
4409
4476
  }
4410
- // TODO: start searching from active node
4411
- this.visitRows((n) => {
4412
- if (n._rowIdx === bottomIdx) {
4413
- node = n;
4414
- return false;
4415
- }
4416
- });
4417
- return node;
4477
+ bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
4478
+ return this._getNodeByRowIdx(bottomIdx);
4418
4479
  }
4419
- /** Return preceeding visible node in the viewport */
4480
+ /** Return preceeding visible node in the viewport. */
4420
4481
  _getPrevNodeInView(node, ofs = 1) {
4421
4482
  this.visitRows((n) => {
4422
4483
  node = n;
@@ -4426,7 +4487,7 @@
4426
4487
  }, { reverse: true, start: node || this.getActiveNode() });
4427
4488
  return node;
4428
4489
  }
4429
- /** Return following visible node in the viewport */
4490
+ /** Return following visible node in the viewport. */
4430
4491
  _getNextNodeInView(node, ofs = 1) {
4431
4492
  this.visitRows((n) => {
4432
4493
  node = n;
@@ -4436,10 +4497,15 @@
4436
4497
  }, { reverse: false, start: node || this.getActiveNode() });
4437
4498
  return node;
4438
4499
  }
4500
+ /**
4501
+ * Append (or insert) a list of toplevel nodes.
4502
+ *
4503
+ * @see {@link WunderbaumNode.addChildren}
4504
+ */
4439
4505
  addChildren(nodeData, options) {
4440
4506
  return this.root.addChildren(nodeData, options);
4441
4507
  }
4442
- /*
4508
+ /**
4443
4509
  * Apply a modification or navigation operation.
4444
4510
  *
4445
4511
  * Most of these commands simply map to a node or tree method.
@@ -4564,16 +4630,17 @@
4564
4630
  this.root.children = null;
4565
4631
  this.keyMap.clear();
4566
4632
  this.refKeyMap.clear();
4567
- this.viewNodes.clear();
4633
+ // this.viewNodes.clear();
4634
+ this.treeRowCount = 0;
4568
4635
  this.activeNode = null;
4569
4636
  this.focusNode = null;
4570
4637
  // this.types = {};
4571
4638
  // this. columns =[];
4572
4639
  // this._columnsById = {};
4573
4640
  // Modification Status
4574
- this.changedSince = 0;
4575
- this.changes.clear();
4576
- this.changedNodes.clear();
4641
+ // this.changedSince = 0;
4642
+ // this.changes.clear();
4643
+ // this.changedNodes.clear();
4577
4644
  // // --- FILTER ---
4578
4645
  // public filterMode: FilterModeType = null;
4579
4646
  // // --- KEYNAV ---
@@ -4601,10 +4668,11 @@
4601
4668
  /**
4602
4669
  * Return `tree.option.NAME` (also resolving if this is a callback).
4603
4670
  *
4604
- * See also [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
4605
- * `tree.types[node.type].NAME`.
4671
+ * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
4672
+ * to consider `node.NAME` setting and `tree.types[node.type].NAME`.
4606
4673
  *
4607
- * @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
4674
+ * @param name option name (use dot notation to access extension option, e.g.
4675
+ * `filter.mode`)
4608
4676
  */
4609
4677
  getOption(name, defaultValue) {
4610
4678
  let ext;
@@ -4648,18 +4716,14 @@
4648
4716
  }
4649
4717
  /** Run code, but defer `updateViewport()` until done. */
4650
4718
  runWithoutUpdate(func, hint = null) {
4651
- // const prev = this._disableUpdate;
4652
- // const start = Date.now();
4653
- // this._disableUpdate = Date.now();
4654
4719
  try {
4655
4720
  this.enableUpdate(false);
4656
- return func();
4721
+ const res = func();
4722
+ assert(!(res instanceof Promise));
4723
+ return res;
4657
4724
  }
4658
4725
  finally {
4659
4726
  this.enableUpdate(true);
4660
- // if (!prev && this._disableUpdate === start) {
4661
- // this._disableUpdate = 0;
4662
- // }
4663
4727
  }
4664
4728
  }
4665
4729
  /** Recursively expand all expandable nodes (triggers lazy load id needed). */
@@ -4677,11 +4741,12 @@
4677
4741
  /** Return the number of nodes in the data model.*/
4678
4742
  count(visible = false) {
4679
4743
  if (visible) {
4680
- return this.viewNodes.size;
4744
+ return this.treeRowCount;
4745
+ // return this.viewNodes.size;
4681
4746
  }
4682
4747
  return this.keyMap.size;
4683
4748
  }
4684
- /* Internal sanity check. */
4749
+ /** @internal sanity check. */
4685
4750
  _check() {
4686
4751
  let i = 0;
4687
4752
  this.visit((n) => {
@@ -4692,25 +4757,30 @@
4692
4757
  }
4693
4758
  // util.assert(this.keyMap.size === i);
4694
4759
  }
4695
- /**Find all nodes that matches condition.
4760
+ /**
4761
+ * Find all nodes that matches condition.
4696
4762
  *
4697
4763
  * @param match title string to search for, or a
4698
4764
  * callback function that returns `true` if a node is matched.
4699
- * @see [[WunderbaumNode.findAll]]
4765
+ *
4766
+ * @see {@link WunderbaumNode.findAll}
4700
4767
  */
4701
4768
  findAll(match) {
4702
4769
  return this.root.findAll(match);
4703
4770
  }
4704
- /**Find first node that matches condition.
4771
+ /**
4772
+ * Find first node that matches condition.
4705
4773
  *
4706
4774
  * @param match title string to search for, or a
4707
4775
  * callback function that returns `true` if a node is matched.
4708
- * @see [[WunderbaumNode.findFirst]]
4776
+ * @see {@link WunderbaumNode.findFirst}
4777
+ *
4709
4778
  */
4710
4779
  findFirst(match) {
4711
4780
  return this.root.findFirst(match);
4712
4781
  }
4713
- /** Find the next visible node that starts with `match`, starting at `startNode`
4782
+ /**
4783
+ * Find the next visible node that starts with `match`, starting at `startNode`
4714
4784
  * and wrap-around at the end.
4715
4785
  */
4716
4786
  findNextNode(match, startNode) {
@@ -4740,7 +4810,8 @@
4740
4810
  }
4741
4811
  return res;
4742
4812
  }
4743
- /** Find a node relative to another node.
4813
+ /**
4814
+ * Find a node relative to another node.
4744
4815
  *
4745
4816
  * @param node
4746
4817
  * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
@@ -4750,7 +4821,7 @@
4750
4821
  */
4751
4822
  findRelatedNode(node, where, includeHidden = false) {
4752
4823
  let res = null;
4753
- let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4824
+ const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4754
4825
  switch (where) {
4755
4826
  case "parent":
4756
4827
  if (node.parent && node.parent.parent) {
@@ -4806,9 +4877,9 @@
4806
4877
  res = this._getNextNodeInView(node);
4807
4878
  break;
4808
4879
  case "pageDown":
4809
- let bottomNode = this._lastNodeInView();
4810
- // this.logDebug(where, this.focusNode, bottomNode);
4811
- if (this.focusNode !== bottomNode) {
4880
+ const bottomNode = this._lastNodeInView();
4881
+ // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
4882
+ if (node._rowIdx < bottomNode._rowIdx) {
4812
4883
  res = bottomNode;
4813
4884
  }
4814
4885
  else {
@@ -4816,12 +4887,13 @@
4816
4887
  }
4817
4888
  break;
4818
4889
  case "pageUp":
4819
- if (this.focusNode && this.focusNode._rowIdx === 0) {
4820
- res = this.focusNode;
4890
+ if (node._rowIdx === 0) {
4891
+ res = node;
4821
4892
  }
4822
4893
  else {
4823
- let topNode = this._firstNodeInView();
4824
- if (this.focusNode !== topNode) {
4894
+ const topNode = this._firstNodeInView();
4895
+ // this.logDebug(`${where}(${node}) -> ${topNode}`);
4896
+ if (node._rowIdx > topNode._rowIdx) {
4825
4897
  res = topNode;
4826
4898
  }
4827
4899
  else {
@@ -4835,7 +4907,7 @@
4835
4907
  return res;
4836
4908
  }
4837
4909
  /**
4838
- * Return the active cell of the currently active node or null.
4910
+ * Return the active cell (`span.wb-col`) of the currently active node or null.
4839
4911
  */
4840
4912
  getActiveColElem() {
4841
4913
  if (this.activeNode && this.activeColIdx >= 0) {
@@ -4901,7 +4973,7 @@
4901
4973
  }
4902
4974
  else {
4903
4975
  // Somewhere near the title
4904
- if (event.type !== "mousemove") {
4976
+ if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
4905
4977
  console.warn("getEventInfo(): not found", event, res);
4906
4978
  }
4907
4979
  return res;
@@ -4933,7 +5005,8 @@
4933
5005
  isEditing() {
4934
5006
  return this._callMethod("edit.isEditingTitle");
4935
5007
  }
4936
- /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
5008
+ /**
5009
+ * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
4937
5010
  */
4938
5011
  isLoading() {
4939
5012
  var res = false;
@@ -4960,7 +5033,7 @@
4960
5033
  console.error.apply(console, args);
4961
5034
  }
4962
5035
  }
4963
- /* Log to console if opts.debugLevel >= 3 */
5036
+ /** Log to console if opts.debugLevel >= 3 */
4964
5037
  logInfo(...args) {
4965
5038
  if (this.options.debugLevel >= 3) {
4966
5039
  Array.prototype.unshift.call(args, this.toString());
@@ -4987,75 +5060,6 @@
4987
5060
  console.warn.apply(console, args);
4988
5061
  }
4989
5062
  }
4990
- /** */
4991
- render(opts) {
4992
- const label = this.logTime("render");
4993
- let idx = 0;
4994
- let top = 0;
4995
- const height = ROW_HEIGHT;
4996
- let modified = false;
4997
- let start = opts === null || opts === void 0 ? void 0 : opts.startIdx;
4998
- let end = opts === null || opts === void 0 ? void 0 : opts.endIdx;
4999
- const obsoleteViewNodes = this.viewNodes;
5000
- const newNodesOnly = !!getOption(opts, "newNodesOnly");
5001
- this.viewNodes = new Set();
5002
- let viewNodes = this.viewNodes;
5003
- // this.debug("render", opts);
5004
- assert(start != null && end != null);
5005
- // Make sure start is always even, so the alternating row colors don't
5006
- // change when scrolling:
5007
- if (start % 2) {
5008
- start--;
5009
- }
5010
- this.visitRows(function (node) {
5011
- const prevIdx = node._rowIdx;
5012
- viewNodes.add(node);
5013
- obsoleteViewNodes.delete(node);
5014
- if (prevIdx !== idx) {
5015
- node._rowIdx = idx;
5016
- modified = true;
5017
- }
5018
- if (idx < start || idx > end) {
5019
- node._callEvent("discard");
5020
- node.removeMarkup();
5021
- }
5022
- else if (!node._rowElem || !newNodesOnly) {
5023
- node.render({ top: top });
5024
- // }else{
5025
- // node.log("ignrored render")
5026
- }
5027
- idx++;
5028
- top += height;
5029
- });
5030
- for (const prevNode of obsoleteViewNodes) {
5031
- prevNode._callEvent("discard");
5032
- prevNode.removeMarkup();
5033
- }
5034
- // Resize tree container
5035
- this.nodeListElement.style.height = "" + top + "px";
5036
- // this.log("render()", this.nodeListElement.style.height);
5037
- this.logTimeEnd(label);
5038
- return modified;
5039
- }
5040
- /**Recalc and apply header columns from `this.columns`. */
5041
- renderHeader() {
5042
- if (!this.headerElement) {
5043
- return;
5044
- }
5045
- const headerRow = this.headerElement.querySelector(".wb-row");
5046
- assert(headerRow);
5047
- headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5048
- for (let i = 0; i < this.columns.length; i++) {
5049
- const col = this.columns[i];
5050
- const colElem = headerRow.children[i];
5051
- colElem.style.left = col._ofsPx + "px";
5052
- colElem.style.width = col._widthPx + "px";
5053
- // colElem.textContent = col.title || col.id;
5054
- const title = escapeHtml(col.title || col.id);
5055
- colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
5056
- // colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
5057
- }
5058
- }
5059
5063
  /**
5060
5064
  * Make sure that this node is scrolled into the viewport.
5061
5065
  *
@@ -5089,24 +5093,12 @@
5089
5093
  this.setModified(ChangeType.vscroll);
5090
5094
  }
5091
5095
  }
5092
- /** */
5093
- setCellMode(mode) {
5094
- // util.assert(this.cellNavMode);
5095
- // util.assert(0 <= colIdx && colIdx < this.columns.length);
5096
- if (mode === this.navMode) {
5097
- return;
5098
- }
5099
- const prevMode = this.navMode;
5100
- const cellMode = mode !== NavigationMode.row;
5101
- this.navMode = mode;
5102
- if (cellMode && prevMode === NavigationMode.row) {
5103
- this.setColumn(0);
5104
- }
5105
- this.element.classList.toggle("wb-cell-mode", cellMode);
5106
- this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
5107
- this.setModified(ChangeType.row, this.activeNode);
5108
- }
5109
- /** */
5096
+ /**
5097
+ * Set column #colIdx to 'active'.
5098
+ *
5099
+ * This higlights the column header and -cells by adding the `wb-active` class.
5100
+ * Available in cell-nav and cell-edit mode, not in row-mode.
5101
+ */
5110
5102
  setColumn(colIdx) {
5111
5103
  assert(this.navMode !== NavigationMode.row);
5112
5104
  assert(0 <= colIdx && colIdx < this.columns.length);
@@ -5131,7 +5123,7 @@
5131
5123
  }
5132
5124
  }
5133
5125
  }
5134
- /** */
5126
+ /** Set or remove keybaord focus to the tree container. */
5135
5127
  setFocus(flag = true) {
5136
5128
  if (flag) {
5137
5129
  this.element.focus();
@@ -5140,20 +5132,24 @@
5140
5132
  this.element.blur();
5141
5133
  }
5142
5134
  }
5143
- /* */
5144
5135
  setModified(change, node, options) {
5136
+ if (this._disableUpdateCount) {
5137
+ // Assuming that we redraw all when enableUpdate() is re-enabled.
5138
+ // this.log(
5139
+ // `IGNORED setModified(${change}) node=${node} (disable level ${this._disableUpdateCount})`
5140
+ // );
5141
+ return;
5142
+ }
5143
+ // this.log(`setModified(${change}) node=${node}`);
5145
5144
  if (!(node instanceof WunderbaumNode)) {
5146
5145
  options = node;
5147
5146
  }
5148
- if (this._disableUpdate) {
5149
- return;
5150
- }
5151
5147
  const immediate = !!getOption(options, "immediate");
5152
5148
  switch (change) {
5153
5149
  case ChangeType.any:
5154
5150
  case ChangeType.structure:
5155
5151
  case ChangeType.header:
5156
- this.changeRedrawPending = true;
5152
+ this.changeRedrawRequestPending = true;
5157
5153
  this.updateViewport(immediate);
5158
5154
  break;
5159
5155
  case ChangeType.vscroll:
@@ -5170,84 +5166,111 @@
5170
5166
  default:
5171
5167
  error(`Invalid change type ${change}`);
5172
5168
  }
5173
- // if (!this.changedSince) {
5174
- // this.changedSince = Date.now();
5175
- // }
5176
- // this.changes.add(change);
5177
- // if (change === ChangeType.structure) {
5178
- // this.changedNodes.clear();
5179
- // } else if (node && !this.changes.has(ChangeType.structure)) {
5180
- // if (this.changedNodes.size < MAX_CHANGED_NODES) {
5181
- // this.changedNodes.add(node);
5182
- // } else {
5183
- // this.changes.add(ChangeType.structure);
5184
- // this.changedNodes.clear();
5185
- // }
5186
- // }
5187
- // this.log("setModified(" + change + ")", node);
5188
5169
  }
5170
+ /** Set the tree's navigation mode. */
5171
+ setNavigationMode(mode) {
5172
+ // util.assert(this.cellNavMode);
5173
+ // util.assert(0 <= colIdx && colIdx < this.columns.length);
5174
+ if (mode === this.navMode) {
5175
+ return;
5176
+ }
5177
+ const prevMode = this.navMode;
5178
+ const cellMode = mode !== NavigationMode.row;
5179
+ this.navMode = mode;
5180
+ if (cellMode && prevMode === NavigationMode.row) {
5181
+ this.setColumn(0);
5182
+ }
5183
+ this.element.classList.toggle("wb-cell-mode", cellMode);
5184
+ this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
5185
+ this.setModified(ChangeType.row, this.activeNode);
5186
+ }
5187
+ /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
5189
5188
  setStatus(status, message, details) {
5190
5189
  return this.root.setStatus(status, message, details);
5191
5190
  }
5192
5191
  /** Update column headers and width. */
5193
5192
  updateColumns(opts) {
5194
- let modified = false;
5195
- let minWidth = 4;
5196
- let vpWidth = this.element.clientWidth;
5193
+ opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
5194
+ const minWidth = 4;
5195
+ const vpWidth = this.element.clientWidth;
5197
5196
  let totalWeight = 0;
5198
5197
  let fixedWidth = 0;
5199
- // Gather width requests
5200
- this._columnsById = {};
5201
- for (let col of this.columns) {
5202
- this._columnsById[col.id] = col;
5203
- let cw = col.width;
5204
- if (!cw || cw === "*") {
5205
- col._weight = 1.0;
5206
- totalWeight += 1.0;
5207
- }
5208
- else if (typeof cw === "number") {
5209
- col._weight = cw;
5210
- totalWeight += cw;
5211
- }
5212
- else if (typeof cw === "string" && cw.endsWith("px")) {
5213
- col._weight = 0;
5214
- let px = parseFloat(cw.slice(0, -2));
5215
- if (col._widthPx != px) {
5216
- modified = true;
5217
- col._widthPx = px;
5198
+ let modified = false;
5199
+ if (opts.calculateCols) {
5200
+ // Gather width requests
5201
+ this._columnsById = {};
5202
+ for (let col of this.columns) {
5203
+ this._columnsById[col.id] = col;
5204
+ let cw = col.width;
5205
+ if (!cw || cw === "*") {
5206
+ col._weight = 1.0;
5207
+ totalWeight += 1.0;
5208
+ }
5209
+ else if (typeof cw === "number") {
5210
+ col._weight = cw;
5211
+ totalWeight += cw;
5212
+ }
5213
+ else if (typeof cw === "string" && cw.endsWith("px")) {
5214
+ col._weight = 0;
5215
+ let px = parseFloat(cw.slice(0, -2));
5216
+ if (col._widthPx != px) {
5217
+ modified = true;
5218
+ col._widthPx = px;
5219
+ }
5220
+ fixedWidth += px;
5221
+ }
5222
+ else {
5223
+ error("Invalid column width: " + cw);
5218
5224
  }
5219
- fixedWidth += px;
5220
5225
  }
5221
- else {
5222
- error("Invalid column width: " + cw);
5223
- }
5224
- }
5225
- // Share remaining space between non-fixed columns
5226
- let restPx = Math.max(0, vpWidth - fixedWidth);
5227
- let ofsPx = 0;
5228
- for (let col of this.columns) {
5229
- if (col._weight) {
5230
- let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5231
- if (col._widthPx != px) {
5232
- modified = true;
5233
- col._widthPx = px;
5226
+ // Share remaining space between non-fixed columns
5227
+ const restPx = Math.max(0, vpWidth - fixedWidth);
5228
+ let ofsPx = 0;
5229
+ for (let col of this.columns) {
5230
+ if (col._weight) {
5231
+ const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5232
+ if (col._widthPx != px) {
5233
+ modified = true;
5234
+ col._widthPx = px;
5235
+ }
5234
5236
  }
5237
+ col._ofsPx = ofsPx;
5238
+ ofsPx += col._widthPx;
5235
5239
  }
5236
- col._ofsPx = ofsPx;
5237
- ofsPx += col._widthPx;
5238
5240
  }
5239
5241
  // Every column has now a calculated `_ofsPx` and `_widthPx`
5240
5242
  // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
5241
5243
  // console.trace();
5242
5244
  // util.error("BREAK");
5243
5245
  if (modified) {
5244
- this.renderHeader();
5245
- if (opts.render !== false) {
5246
- this.render();
5246
+ this._renderHeaderMarkup();
5247
+ if (opts.updateRows) {
5248
+ this._updateRows();
5247
5249
  }
5248
5250
  }
5249
5251
  }
5250
- /** Render all rows that are visible in the viewport. */
5252
+ /** Create/update header markup from `this.columns` definition.
5253
+ * @internal
5254
+ */
5255
+ _renderHeaderMarkup() {
5256
+ if (!this.headerElement) {
5257
+ return;
5258
+ }
5259
+ const headerRow = this.headerElement.querySelector(".wb-row");
5260
+ assert(headerRow);
5261
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5262
+ for (let i = 0; i < this.columns.length; i++) {
5263
+ const col = this.columns[i];
5264
+ const colElem = headerRow.children[i];
5265
+ colElem.style.left = col._ofsPx + "px";
5266
+ colElem.style.width = col._widthPx + "px";
5267
+ // colElem.textContent = col.title || col.id;
5268
+ const title = escapeHtml(col.title || col.id);
5269
+ colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
5270
+ // colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
5271
+ }
5272
+ }
5273
+ /** Render header and all rows that are visible in the viewport (async, throttled). */
5251
5274
  updateViewport(immediate = false) {
5252
5275
  // Call the `throttle` wrapper for `this._updateViewport()` which will
5253
5276
  // execute immediately on the leading edge of a sequence:
@@ -5256,42 +5279,163 @@
5256
5279
  this._updateViewportThrottled.flush();
5257
5280
  }
5258
5281
  }
5282
+ /**
5283
+ * This is the actual update method, which is wrapped inside a throttle method.
5284
+ * This protected method should not be called directly but via
5285
+ * `tree.updateViewport()` or `tree.setModified()`.
5286
+ * It calls `updateColumns()` and `_updateRows()`.
5287
+ * @internal
5288
+ */
5259
5289
  _updateViewport() {
5260
- if (this._disableUpdate) {
5290
+ if (this._disableUpdateCount) {
5291
+ this.log(`IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`);
5261
5292
  return;
5262
5293
  }
5263
- const newNodesOnly = !this.changeRedrawPending;
5264
- this.changeRedrawPending = false;
5294
+ const newNodesOnly = !this.changeRedrawRequestPending;
5295
+ this.changeRedrawRequestPending = false;
5265
5296
  let height = this.scrollContainer.clientHeight;
5266
- // We cannot get the height for absolut positioned parent, so look at first col
5297
+ // We cannot get the height for absolute positioned parent, so look at first col
5267
5298
  // let headerHeight = this.headerElement.clientHeight
5268
5299
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
5269
5300
  const headerHeight = this.options.headerHeightPx;
5270
5301
  const wantHeight = this.element.clientHeight - headerHeight;
5271
- const ofs = this.scrollContainer.scrollTop;
5272
5302
  if (Math.abs(height - wantHeight) > 1.0) {
5273
5303
  // this.log("resize", height, wantHeight);
5274
5304
  this.scrollContainer.style.height = wantHeight + "px";
5275
5305
  height = wantHeight;
5276
5306
  }
5277
- this.updateColumns({ render: false });
5278
- this.render({
5279
- startIdx: Math.max(0, ofs / ROW_HEIGHT - RENDER_MAX_PREFETCH),
5280
- endIdx: Math.max(0, (ofs + height) / ROW_HEIGHT + RENDER_MAX_PREFETCH),
5281
- newNodesOnly: newNodesOnly,
5282
- });
5307
+ this.updateColumns({ updateRows: false });
5308
+ this._updateRows({ newNodesOnly: newNodesOnly });
5283
5309
  this._callEvent("update");
5284
5310
  }
5285
- /** Call callback(node) for all nodes in hierarchical order (depth-first).
5311
+ /**
5312
+ * Assert that TR order matches the natural node order
5313
+ * @internal
5314
+ */
5315
+ _validateRows() {
5316
+ let trs = this.nodeListElement.childNodes;
5317
+ let i = 0;
5318
+ let prev = -1;
5319
+ let ok = true;
5320
+ trs.forEach((element) => {
5321
+ const tr = element;
5322
+ const top = Number.parseInt(tr.style.top);
5323
+ const n = tr._wb_node;
5324
+ // if (i < 4) {
5325
+ // console.info(
5326
+ // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
5327
+ // );
5328
+ // }
5329
+ if (top <= prev) {
5330
+ console.warn(`TR order mismatch at index ${i}: top=${top}px, node=${n}`);
5331
+ // throw new Error("fault");
5332
+ ok = false;
5333
+ }
5334
+ prev = top;
5335
+ i++;
5336
+ });
5337
+ return ok;
5338
+ }
5339
+ /*
5340
+ * - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
5341
+ * - Store count of rows to `tree.treeRowCount`.
5342
+ * - Renumber `node._rowIdx` for all visible nodes.
5343
+ * - Calculate the index range that must be rendered to fill the viewport
5344
+ * (including upper and lower prefetch)
5345
+ * -
5346
+ */
5347
+ _updateRows(opts) {
5348
+ const label = this.logTime("_updateRows");
5349
+ opts = Object.assign({ newNodesOnly: false }, opts);
5350
+ const newNodesOnly = !!opts.newNodesOnly;
5351
+ const row_height = ROW_HEIGHT;
5352
+ const vp_height = this.scrollContainer.clientHeight;
5353
+ const prefetch = RENDER_MAX_PREFETCH;
5354
+ const ofs = this.scrollContainer.scrollTop;
5355
+ let startIdx = Math.max(0, ofs / row_height - prefetch);
5356
+ startIdx = Math.floor(startIdx);
5357
+ // Make sure start is always even, so the alternating row colors don't
5358
+ // change when scrolling:
5359
+ if (startIdx % 2) {
5360
+ startIdx--;
5361
+ }
5362
+ let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
5363
+ endIdx = Math.ceil(endIdx);
5364
+ // const obsoleteViewNodes = this.viewNodes;
5365
+ // this.viewNodes = new Set();
5366
+ // const viewNodes = this.viewNodes;
5367
+ // this.debug("render", opts);
5368
+ const obsoleteNodes = new Set();
5369
+ this.nodeListElement.childNodes.forEach((elem) => {
5370
+ const tr = elem;
5371
+ obsoleteNodes.add(tr._wb_node);
5372
+ });
5373
+ let idx = 0;
5374
+ let top = 0;
5375
+ let modified = false;
5376
+ let prevElem = "first";
5377
+ this.visitRows(function (node) {
5378
+ // console.log("visit", node)
5379
+ const rowDiv = node._rowElem;
5380
+ // Renumber all expanded nodes
5381
+ if (node._rowIdx !== idx) {
5382
+ node._rowIdx = idx;
5383
+ modified = true;
5384
+ }
5385
+ if (idx < startIdx || idx > endIdx) {
5386
+ // row is outside viewport bounds
5387
+ if (rowDiv) {
5388
+ prevElem = rowDiv;
5389
+ }
5390
+ }
5391
+ else if (rowDiv && newNodesOnly) {
5392
+ obsoleteNodes.delete(node);
5393
+ // no need to update existing node markup
5394
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
5395
+ prevElem = rowDiv;
5396
+ }
5397
+ else {
5398
+ obsoleteNodes.delete(node);
5399
+ // Create new markup
5400
+ node.render({ top: top, after: prevElem });
5401
+ // console.log("render", top, prevElem, "=>", node._rowElem);
5402
+ prevElem = node._rowElem;
5403
+ }
5404
+ idx++;
5405
+ top += row_height;
5406
+ });
5407
+ this.treeRowCount = idx;
5408
+ for (const n of obsoleteNodes) {
5409
+ n._callEvent("discard");
5410
+ n.removeMarkup();
5411
+ }
5412
+ // Resize tree container
5413
+ this.nodeListElement.style.height = `${top}px`;
5414
+ // this.log(
5415
+ // `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
5416
+ // this.nodeListElement.style.height
5417
+ // );
5418
+ this.logTimeEnd(label);
5419
+ this._validateRows();
5420
+ return modified;
5421
+ }
5422
+ /**
5423
+ * Call callback(node) for all nodes in hierarchical order (depth-first).
5286
5424
  *
5287
5425
  * @param {function} callback the callback function.
5288
- * Return false to stop iteration, return "skip" to skip this node and children only.
5426
+ * Return false to stop iteration, return "skip" to skip this node and
5427
+ * children only.
5289
5428
  * @returns {boolean} false, if the iterator was stopped.
5290
5429
  */
5291
5430
  visit(callback) {
5292
5431
  return this.root.visit(callback, false);
5293
5432
  }
5294
- /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
5433
+ /**
5434
+ * Call fn(node) for all nodes in vertical order, top down (or bottom up).
5435
+ *
5436
+ * Note that this considers expansion state, i.e. children of collapsed nodes
5437
+ * are skipped.
5438
+ *
5295
5439
  * Stop iteration, if fn() returns false.<br>
5296
5440
  * Return false if iteration was stopped.
5297
5441
  *
@@ -5371,7 +5515,8 @@
5371
5515
  }
5372
5516
  return true;
5373
5517
  }
5374
- /** Call fn(node) for all nodes in vertical order, bottom up.
5518
+ /**
5519
+ * Call fn(node) for all nodes in vertical order, bottom up.
5375
5520
  * @internal
5376
5521
  */
5377
5522
  _visitRowsUp(callback, opts) {
@@ -5415,19 +5560,36 @@
5415
5560
  }
5416
5561
  return true;
5417
5562
  }
5418
- /** . */
5563
+ /**
5564
+ * Reload the tree with a new source.
5565
+ *
5566
+ * Previous data is cleared.
5567
+ * Pass `options.columns` to define a header (may also be part of `source.columns`).
5568
+ */
5419
5569
  load(source, options = {}) {
5420
5570
  this.clear();
5421
5571
  const columns = options.columns || source.columns;
5422
5572
  if (columns) {
5423
5573
  this.columns = options.columns;
5424
- this.renderHeader();
5425
- // this.updateColumns({ render: false });
5574
+ // this._renderHeaderMarkup();
5575
+ this.updateColumns({ calculateCols: false });
5426
5576
  }
5427
5577
  return this.root.load(source);
5428
5578
  }
5429
5579
  /**
5580
+ * Disable render requests during operations that would trigger many updates.
5430
5581
  *
5582
+ * ```js
5583
+ * try {
5584
+ * tree.enableUpdate(false);
5585
+ * // ... (long running operation that would trigger many updates)
5586
+ * foo();
5587
+ * // ... NOTE: make sure that async operations have finished
5588
+ * await foo();
5589
+ * } finally {
5590
+ * tree.enableUpdate(true);
5591
+ * }
5592
+ * ```
5431
5593
  */
5432
5594
  enableUpdate(flag) {
5433
5595
  /*
@@ -5435,20 +5597,22 @@
5435
5597
  1 >-------------------------------------<
5436
5598
  2 >--------------------<
5437
5599
  3 >--------------------------<
5438
-
5439
- 5
5440
-
5441
5600
  */
5442
- // this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
5443
5601
  if (flag) {
5444
- assert(this._disableUpdateCount > 0);
5602
+ assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often");
5445
5603
  this._disableUpdateCount--;
5604
+ // this.logDebug(
5605
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5606
+ // );
5446
5607
  if (this._disableUpdateCount === 0) {
5447
5608
  this.updateViewport();
5448
5609
  }
5449
5610
  }
5450
5611
  else {
5451
5612
  this._disableUpdateCount++;
5613
+ // this.logDebug(
5614
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5615
+ // );
5452
5616
  // this._disableUpdate = Date.now();
5453
5617
  }
5454
5618
  // return !flag; // return previous value
@@ -5481,8 +5645,10 @@
5481
5645
  return this.extensions.filter.updateFilter();
5482
5646
  }
5483
5647
  }
5484
- Wunderbaum.version = "v0.0.2"; // Set to semver by 'grunt release'
5485
5648
  Wunderbaum.sequence = 0;
5649
+ /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
5650
+ Wunderbaum.version = "v0.0.3"; // Set to semver by 'grunt release'
5651
+ /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
5486
5652
  Wunderbaum.util = util;
5487
5653
 
5488
5654
  exports.Wunderbaum = Wunderbaum;