wunderbaum 0.3.4 → 0.4.0

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,7 +7,7 @@
7
7
  /*!
8
8
  * Wunderbaum - util
9
9
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
10
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
10
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
11
11
  */
12
12
  /** @module util */
13
13
  /** Readable names for `MouseEvent.button` */
@@ -762,10 +762,10 @@
762
762
  /*!
763
763
  * Wunderbaum - types
764
764
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
765
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
765
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
766
766
  */
767
767
  /**
768
- * Possible values for {@link WunderbaumNode.setModified()} and {@link Wunderbaum.setModified()}.
768
+ * Possible values for {@link WunderbaumNode.update()} and {@link Wunderbaum.update()}.
769
769
  */
770
770
  var ChangeType;
771
771
  (function (ChangeType) {
@@ -826,7 +826,7 @@
826
826
  /*!
827
827
  * Wunderbaum - wb_extension_base
828
828
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
829
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
829
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
830
830
  */
831
831
  class WunderbaumExtension {
832
832
  constructor(tree, id, defaults) {
@@ -1113,11 +1113,75 @@
1113
1113
  debounced.pending = pending;
1114
1114
  return debounced;
1115
1115
  }
1116
+ /**
1117
+ * Creates a throttled function that only invokes `func` at most once per
1118
+ * every `wait` milliseconds (or once per browser frame). The throttled function
1119
+ * comes with a `cancel` method to cancel delayed `func` invocations and a
1120
+ * `flush` method to immediately invoke them. Provide `options` to indicate
1121
+ * whether `func` should be invoked on the leading and/or trailing edge of the
1122
+ * `wait` timeout. The `func` is invoked with the last arguments provided to the
1123
+ * throttled function. Subsequent calls to the throttled function return the
1124
+ * result of the last `func` invocation.
1125
+ *
1126
+ * **Note:** If `leading` and `trailing` options are `true`, `func` is
1127
+ * invoked on the trailing edge of the timeout only if the throttled function
1128
+ * is invoked more than once during the `wait` timeout.
1129
+ *
1130
+ * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
1131
+ * until the next tick, similar to `setTimeout` with a timeout of `0`.
1132
+ *
1133
+ * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
1134
+ * invocation will be deferred until the next frame is drawn (typically about
1135
+ * 16ms).
1136
+ *
1137
+ * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
1138
+ * for details over the differences between `throttle` and `debounce`.
1139
+ *
1140
+ * @since 0.1.0
1141
+ * @category Function
1142
+ * @param {Function} func The function to throttle.
1143
+ * @param {number} [wait=0]
1144
+ * The number of milliseconds to throttle invocations to; if omitted,
1145
+ * `requestAnimationFrame` is used (if available).
1146
+ * @param {Object} [options={}] The options object.
1147
+ * @param {boolean} [options.leading=true]
1148
+ * Specify invoking on the leading edge of the timeout.
1149
+ * @param {boolean} [options.trailing=true]
1150
+ * Specify invoking on the trailing edge of the timeout.
1151
+ * @returns {Function} Returns the new throttled function.
1152
+ * @example
1153
+ *
1154
+ * // Avoid excessively updating the position while scrolling.
1155
+ * jQuery(window).on('scroll', throttle(updatePosition, 100))
1156
+ *
1157
+ * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
1158
+ * const throttled = throttle(renewToken, 300000, { 'trailing': false })
1159
+ * jQuery(element).on('click', throttled)
1160
+ *
1161
+ * // Cancel the trailing throttled invocation.
1162
+ * jQuery(window).on('popstate', throttled.cancel)
1163
+ */
1164
+ function throttle(func, wait = 0, options = {}) {
1165
+ let leading = true;
1166
+ let trailing = true;
1167
+ if (typeof func !== "function") {
1168
+ throw new TypeError("Expected a function");
1169
+ }
1170
+ if (isObject(options)) {
1171
+ leading = "leading" in options ? !!options.leading : leading;
1172
+ trailing = "trailing" in options ? !!options.trailing : trailing;
1173
+ }
1174
+ return debounce(func, wait, {
1175
+ leading,
1176
+ trailing,
1177
+ maxWait: wait,
1178
+ });
1179
+ }
1116
1180
 
1117
1181
  /*!
1118
1182
  * Wunderbaum - ext-filter
1119
1183
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
1120
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
1184
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
1121
1185
  */
1122
1186
  const START_MARKER = "\uFFF7";
1123
1187
  const END_MARKER = "\uFFF8";
@@ -1162,7 +1226,7 @@
1162
1226
  }
1163
1227
  }
1164
1228
  _applyFilterNoUpdate(filter, branchMode, _opts) {
1165
- return this.tree.runWithoutUpdate(() => {
1229
+ return this.tree.runWithDeferredUpdate(() => {
1166
1230
  return this._applyFilterImpl(filter, branchMode, _opts);
1167
1231
  });
1168
1232
  }
@@ -1374,7 +1438,6 @@
1374
1438
  // "wb-ext-filter",
1375
1439
  "wb-ext-filter-dim", "wb-ext-filter-hide");
1376
1440
  // tree._callHook("treeStructureChanged", this, "clearFilter");
1377
- // tree.render();
1378
1441
  tree.enableUpdate(true);
1379
1442
  }
1380
1443
  }
@@ -1418,7 +1481,7 @@
1418
1481
  /*!
1419
1482
  * Wunderbaum - ext-keynav
1420
1483
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
1421
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
1484
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
1422
1485
  */
1423
1486
  const QUICKSEARCH_DELAY = 500;
1424
1487
  class KeynavExtension extends WunderbaumExtension {
@@ -1495,7 +1558,7 @@
1495
1558
  tree.setFocus();
1496
1559
  break;
1497
1560
  case "Escape":
1498
- node.render();
1561
+ node._render();
1499
1562
  tree.setFocus();
1500
1563
  break;
1501
1564
  }
@@ -1565,7 +1628,7 @@
1565
1628
  // tree._triggerNodeEvent("clickPaging", ctx, event);
1566
1629
  // } else
1567
1630
  if (node.getOption("checkbox")) {
1568
- node.setSelected(!node.isSelected());
1631
+ node.toggleSelected();
1569
1632
  }
1570
1633
  else {
1571
1634
  node.setActive(true, { event: event });
@@ -1601,7 +1664,7 @@
1601
1664
  if (inputHasFocus) {
1602
1665
  if (eventName === "Escape") {
1603
1666
  // Discard changes
1604
- node.render();
1667
+ node._render();
1605
1668
  // Keep cell-nav mode
1606
1669
  node.logDebug(`Reset focused input`);
1607
1670
  tree.setFocus();
@@ -1651,7 +1714,7 @@
1651
1714
  break;
1652
1715
  case " ": // Space
1653
1716
  if (tree.activeColIdx === 0 && node.getOption("checkbox")) {
1654
- node.setSelected(!node.isSelected());
1717
+ node.toggleSelected();
1655
1718
  handled = true;
1656
1719
  }
1657
1720
  else if (curInput && curInputType === "checkbox") {
@@ -1758,7 +1821,7 @@
1758
1821
  /*!
1759
1822
  * Wunderbaum - ext-logger
1760
1823
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
1761
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
1824
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
1762
1825
  */
1763
1826
  class LoggerExtension extends WunderbaumExtension {
1764
1827
  constructor(tree) {
@@ -1798,9 +1861,9 @@
1798
1861
  /*!
1799
1862
  * Wunderbaum - common
1800
1863
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
1801
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
1864
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
1802
1865
  */
1803
- const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
1866
+ const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
1804
1867
  /**
1805
1868
  * Fixed height of a row in pixel. Must match the SCSS variable `$row-outer-height`.
1806
1869
  */
@@ -1842,10 +1905,10 @@
1842
1905
  // expanderLazy: "bi bi-chevron-bar-right",
1843
1906
  checkChecked: "bi bi-check-square",
1844
1907
  checkUnchecked: "bi bi-square",
1845
- checkUnknown: "bi dash-square-dotted",
1908
+ checkUnknown: "bi bi-dash-square-dotted",
1846
1909
  radioChecked: "bi bi-circle-fill",
1847
1910
  radioUnchecked: "bi bi-circle",
1848
- radioUnknown: "bi bi-circle-dotted",
1911
+ radioUnknown: "bi bi-record-circle",
1849
1912
  folder: "bi bi-folder2",
1850
1913
  folderOpen: "bi bi-folder2-open",
1851
1914
  folderLazy: "bi bi-folder-symlink",
@@ -2044,7 +2107,7 @@
2044
2107
  /*!
2045
2108
  * Wunderbaum - ext-dnd
2046
2109
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
2047
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
2110
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
2048
2111
  */
2049
2112
  const nodeMimeType = "application/x-wunderbaum-node";
2050
2113
  class DndExtension extends WunderbaumExtension {
@@ -2067,6 +2130,7 @@
2067
2130
  preventVoidMoves: true,
2068
2131
  scroll: true,
2069
2132
  scrollSensitivity: 20,
2133
+ // scrollnterval: 50, // Generste event every 50 ms
2070
2134
  scrollSpeed: 5,
2071
2135
  // setTextTypeJson: false, // Allow dragging of nodes to different IE windows
2072
2136
  sourceCopyHook: null,
@@ -2088,6 +2152,8 @@
2088
2152
  this.lastAllowedDropRegions = null;
2089
2153
  this.lastDropEffect = null;
2090
2154
  this.lastDropRegion = false;
2155
+ this.currentScrollDir = 0;
2156
+ this.applyScrollDirThrottled = throttle(this.applyScrollDir, 50);
2091
2157
  }
2092
2158
  init() {
2093
2159
  super.init();
@@ -2158,22 +2224,55 @@
2158
2224
  // return "over";
2159
2225
  }
2160
2226
  /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
2161
- autoScroll(event) {
2162
- let tree = this.tree, dndOpts = tree.options.dnd, sp = tree.listContainerElement, sensitivity = dndOpts.scrollSensitivity, speed = dndOpts.scrollSpeed, scrolled = 0;
2163
- const scrollTop = sp.offsetTop;
2164
- if (scrollTop + sp.offsetHeight - event.pageY < sensitivity) {
2165
- const delta = sp.scrollHeight - sp.clientHeight - scrollTop;
2166
- if (delta > 0) {
2167
- sp.scrollTop = scrolled = scrollTop + speed;
2227
+ applyScrollDir() {
2228
+ if (this.isDragging() && this.currentScrollDir) {
2229
+ const dndOpts = this.tree.options.dnd;
2230
+ const sp = this.tree.element; // scroll parent
2231
+ const scrollTop = sp.scrollTop;
2232
+ if (this.currentScrollDir < 0) {
2233
+ sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed);
2234
+ }
2235
+ else if (this.currentScrollDir > 0) {
2236
+ sp.scrollTop = scrollTop + dndOpts.scrollSpeed;
2168
2237
  }
2169
2238
  }
2170
- else if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) {
2171
- sp.scrollTop = scrolled = scrollTop - speed;
2172
- }
2173
- // if (scrolled) {
2174
- // tree.logDebug("autoScroll: " + scrolled + "px");
2175
- // }
2176
- return scrolled;
2239
+ }
2240
+ /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
2241
+ autoScroll(viewportY) {
2242
+ const tree = this.tree;
2243
+ const dndOpts = tree.options.dnd;
2244
+ const sensitivity = dndOpts.scrollSensitivity;
2245
+ const sp = tree.element; // scroll parent
2246
+ const headerHeight = tree.headerElement.clientHeight; // May be 0
2247
+ // const height = sp.clientHeight - headerHeight;
2248
+ // const height = sp.offsetHeight + headerHeight;
2249
+ const height = sp.offsetHeight;
2250
+ const scrollTop = sp.scrollTop;
2251
+ // tree.logDebug(
2252
+ // `autoScroll: height=${height}, scrollTop=${scrollTop}, viewportY=${viewportY}`
2253
+ // );
2254
+ this.currentScrollDir = 0;
2255
+ if (scrollTop > 0 &&
2256
+ viewportY > 0 &&
2257
+ viewportY <= sensitivity + headerHeight) {
2258
+ // Mouse in top 20px area: scroll up
2259
+ // sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed);
2260
+ this.currentScrollDir = -1;
2261
+ }
2262
+ else if (scrollTop < sp.scrollHeight - height &&
2263
+ viewportY >= height - sensitivity) {
2264
+ // Mouse in bottom 20px area: scroll down
2265
+ // sp.scrollTop = scrollTop + dndOpts.scrollSpeed;
2266
+ this.currentScrollDir = +1;
2267
+ }
2268
+ if (this.currentScrollDir) {
2269
+ this.applyScrollDirThrottled();
2270
+ }
2271
+ return sp.scrollTop - scrollTop;
2272
+ }
2273
+ /** Return true if a drag operation currently in progress. */
2274
+ isDragging() {
2275
+ return !!this.srcNode;
2177
2276
  }
2178
2277
  onDragEvent(e) {
2179
2278
  // const tree = this.tree;
@@ -2297,7 +2396,8 @@
2297
2396
  // --- dragover ---
2298
2397
  }
2299
2398
  else if (e.type === "dragover") {
2300
- this.autoScroll(e);
2399
+ const viewportY = e.clientY - this.tree.element.offsetTop;
2400
+ this.autoScroll(viewportY);
2301
2401
  const region = this._calcDropRegion(e, this.lastAllowedDropRegions);
2302
2402
  this.lastDropRegion = region;
2303
2403
  if (dndOpts.autoExpandMS > 0 &&
@@ -2337,7 +2437,7 @@
2337
2437
  /*!
2338
2438
  * Wunderbaum - drag_observer
2339
2439
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
2340
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
2440
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
2341
2441
  */
2342
2442
  /**
2343
2443
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2473,7 +2573,7 @@
2473
2573
  /*!
2474
2574
  * Wunderbaum - ext-grid
2475
2575
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
2476
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
2576
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
2477
2577
  */
2478
2578
  class GridExtension extends WunderbaumExtension {
2479
2579
  constructor(tree) {
@@ -2510,7 +2610,7 @@
2510
2610
  /*!
2511
2611
  * Wunderbaum - deferred
2512
2612
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
2513
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
2613
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
2514
2614
  */
2515
2615
  /**
2516
2616
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -2563,32 +2663,20 @@
2563
2663
  /*!
2564
2664
  * Wunderbaum - wunderbaum_node
2565
2665
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
2566
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
2666
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
2667
+ */
2668
+ /** WunderbaumNode properties that can be passed with source data.
2669
+ * (Any other source properties will be stored as `node.data.PROP`.)
2567
2670
  */
2568
- /** Top-level properties that can be passed with `data`. */
2569
2671
  const NODE_PROPS = new Set([
2570
- // TODO: use NODE_ATTRS instead?
2571
- "classes",
2572
- "expanded",
2573
- "icon",
2574
- "key",
2575
- "lazy",
2576
- "refKey",
2577
- "selected",
2578
- "title",
2579
- "tooltip",
2580
- "type",
2581
- ]);
2582
- const NODE_ATTRS = new Set([
2583
2672
  "checkbox",
2584
- "expanded",
2585
2673
  "classes",
2586
- "folder",
2674
+ "expanded",
2587
2675
  "icon",
2588
2676
  "iconTooltip",
2589
2677
  "key",
2590
2678
  "lazy",
2591
- "partsel",
2679
+ "_partsel",
2592
2680
  "radiogroup",
2593
2681
  "refKey",
2594
2682
  "selected",
@@ -2597,9 +2685,16 @@
2597
2685
  "tooltip",
2598
2686
  "type",
2599
2687
  "unselectable",
2600
- "unselectableIgnore",
2601
- "unselectableStatus",
2688
+ // "unselectableIgnore",
2689
+ // "unselectableStatus",
2602
2690
  ]);
2691
+ /** WunderbaumNode properties that will be returned by `node.toDict()`.)
2692
+ */
2693
+ const NODE_DICT_PROPS = new Set(NODE_PROPS);
2694
+ NODE_DICT_PROPS.delete("_partsel");
2695
+ NODE_DICT_PROPS.delete("unselectable");
2696
+ // NODE_DICT_PROPS.delete("unselectableIgnore");
2697
+ // NODE_DICT_PROPS.delete("unselectableStatus");
2603
2698
  /**
2604
2699
  * A single tree node.
2605
2700
  *
@@ -2621,7 +2716,7 @@
2621
2716
  * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */
2622
2717
  this.expanded = false;
2623
2718
  /** Selection state.
2624
- * @see {@link isSelected}, {@link setSelected}. */
2719
+ * @see {@link isSelected}, {@link setSelected}, {@link toggleSelected}. */
2625
2720
  this.selected = false;
2626
2721
  /** Additional classes added to `div.wb-row`.
2627
2722
  * @see {@link hasClass}, {@link setClass}. */
@@ -2643,16 +2738,23 @@
2643
2738
  this.key = "" + ((_a = data.key) !== null && _a !== void 0 ? _a : ++WunderbaumNode.sequence);
2644
2739
  this.title = "" + ((_b = data.title) !== null && _b !== void 0 ? _b : "<" + this.key + ">");
2645
2740
  data.refKey != null ? (this.refKey = "" + data.refKey) : 0;
2646
- data.statusNodeType != null
2647
- ? (this.statusNodeType = "" + data.statusNodeType)
2648
- : 0;
2649
2741
  data.type != null ? (this.type = "" + data.type) : 0;
2650
- data.checkbox != null ? (this.checkbox = !!data.checkbox) : 0;
2651
- data.colspan != null ? (this.colspan = !!data.colspan) : 0;
2652
2742
  this.expanded = data.expanded === true;
2653
2743
  data.icon != null ? (this.icon = data.icon) : 0;
2654
2744
  this.lazy = data.lazy === true;
2745
+ data.statusNodeType != null
2746
+ ? (this.statusNodeType = "" + data.statusNodeType)
2747
+ : 0;
2748
+ data.colspan != null ? (this.colspan = !!data.colspan) : 0;
2749
+ // Selection
2750
+ data.checkbox != null ? (this.checkbox = !!data.checkbox) : 0;
2751
+ data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0;
2655
2752
  this.selected = data.selected === true;
2753
+ data.unselectable === true ? (this.unselectable = true) : 0;
2754
+ // data.unselectableStatus != null
2755
+ // ? (this.unselectableStatus = !!data.unselectableStatus)
2756
+ // : 0;
2757
+ // data.unselectableIgnore === true ? (this.unselectableIgnore = true) : 0;
2656
2758
  if (data.classes) {
2657
2759
  this.setClass(data.classes);
2658
2760
  }
@@ -2776,14 +2878,17 @@
2776
2878
  // insert nodeList after children[pos]
2777
2879
  this.children.splice(pos, 0, ...nodeList);
2778
2880
  }
2779
- // TODO:
2780
- // if (tree.options.selectMode === 3) {
2781
- // this.fixSelection3FromEndNodes();
2782
- // }
2783
2881
  // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
2784
- tree.setModified(ChangeType.structure);
2882
+ tree.update(ChangeType.structure);
2785
2883
  }
2786
2884
  finally {
2885
+ // if (tree.options.selectMode === "hier") {
2886
+ // if (this.parent && this.parent.children) {
2887
+ // this.fixSelection3FromEndNodes();
2888
+ // } else {
2889
+ // // my happen when loading __root__;
2890
+ // }
2891
+ // }
2787
2892
  tree.enableUpdate(true);
2788
2893
  }
2789
2894
  // if(isTopCall && loadLazy){
@@ -2829,6 +2934,17 @@
2829
2934
  applyCommand(cmd, options) {
2830
2935
  return this.tree.applyCommand(cmd, this, options);
2831
2936
  }
2937
+ /**
2938
+ * Collapse all expanded sibling nodes if any.
2939
+ * (Automatically called when `autoCollapse` is true.)
2940
+ */
2941
+ collapseSiblings(options) {
2942
+ for (let node of this.parent.children) {
2943
+ if (node !== this && node.expanded) {
2944
+ node.setExpanded(false, options);
2945
+ }
2946
+ }
2947
+ }
2832
2948
  /**
2833
2949
  * Add/remove one or more classes to `<div class='wb-row'>`.
2834
2950
  *
@@ -2868,7 +2984,7 @@
2868
2984
  const tree = this.tree;
2869
2985
  const minExpandLevel = this.tree.options.minExpandLevel;
2870
2986
  let { depth = 99, loadLazy, force } = options !== null && options !== void 0 ? options : {};
2871
- const expand_opts = {
2987
+ const expandOpts = {
2872
2988
  scrollIntoView: false,
2873
2989
  force: force,
2874
2990
  loadLazy: loadLazy,
@@ -2892,7 +3008,7 @@
2892
3008
  // Node is collapsed and may be expanded (i.e. has children or is lazy)
2893
3009
  // Expanding may be async, so we store the promise.
2894
3010
  // Also the recursion is delayed until expansion finished.
2895
- const p = cn.setExpanded(true, expand_opts);
3011
+ const p = cn.setExpanded(true, expandOpts);
2896
3012
  promises.push(p);
2897
3013
  p.then(async () => {
2898
3014
  await _iter(cn, level_1);
@@ -2908,7 +3024,7 @@
2908
3024
  // Collapsing is always synchronous, so no promises required
2909
3025
  if (!minExpandLevel || force || cn.getLevel() > minExpandLevel) {
2910
3026
  // Do not collapse until minExpandLevel
2911
- cn.setExpanded(false, expand_opts);
3027
+ cn.setExpanded(false, expandOpts);
2912
3028
  }
2913
3029
  _iter(cn, level_1); // recursion, even if cn was already collapsed
2914
3030
  }
@@ -3280,9 +3396,11 @@
3280
3396
  isRootNode() {
3281
3397
  return this.tree.root === this;
3282
3398
  }
3283
- /** Return true if this node is selected, i.e. the checkbox is set. */
3399
+ /** Return true if this node is selected, i.e. the checkbox is set.
3400
+ * `undefined` if partly selected (tri-state), false otherwise.
3401
+ */
3284
3402
  isSelected() {
3285
- return !!this.selected;
3403
+ return this.selected ? true : this._partsel ? undefined : false;
3286
3404
  }
3287
3405
  /** Return true if this node is a temporarily generated system node like
3288
3406
  * 'loading', 'paging', or 'error' (node.statusNodeType contains the type).
@@ -3354,7 +3472,7 @@
3354
3472
  tree.logInfo("Redefine columns", source.columns);
3355
3473
  tree.columns = source.columns;
3356
3474
  delete source.columns;
3357
- tree.setModified(ChangeType.colStructure);
3475
+ tree.update(ChangeType.colStructure);
3358
3476
  }
3359
3477
  this.addChildren(source.children);
3360
3478
  // Add extra data to `tree.data`
@@ -3364,6 +3482,9 @@
3364
3482
  tree.logDebug(`Add source.${key} to tree.data.${key}`);
3365
3483
  }
3366
3484
  }
3485
+ if (tree.options.selectMode === "hier") {
3486
+ this.fixSelection3FromEndNodes();
3487
+ }
3367
3488
  this._callEvent("load");
3368
3489
  }
3369
3490
  async _fetchWithOptions(source) {
@@ -3493,10 +3614,10 @@
3493
3614
  await this.load(source); // also calls setStatus('ok')
3494
3615
  if (wasExpanded) {
3495
3616
  this.expanded = true;
3496
- this.tree.setModified(ChangeType.structure);
3617
+ this.tree.update(ChangeType.structure);
3497
3618
  }
3498
3619
  else {
3499
- this.setModified(); // Fix expander icon to 'loaded'
3620
+ this.update(); // Fix expander icon to 'loaded'
3500
3621
  }
3501
3622
  }
3502
3623
  catch (e) {
@@ -3653,7 +3774,7 @@
3653
3774
  // Fix node.tree for all source nodes
3654
3775
  // util.assert(false, "Cross-tree move is not yet implemented.");
3655
3776
  this.logWarn("Cross-tree moveTo is experimental!");
3656
- this.visit(function (n) {
3777
+ this.visit((n) => {
3657
3778
  // TODO: fix selection state and activation, ...
3658
3779
  n.tree = targetNode.tree;
3659
3780
  }, true);
@@ -3662,7 +3783,7 @@
3662
3783
  // DragAndDrop to generate a dragend event on the source node
3663
3784
  setTimeout(() => {
3664
3785
  // Even indentation may have changed:
3665
- tree.setModified(ChangeType.any);
3786
+ tree.update(ChangeType.any);
3666
3787
  }, 0);
3667
3788
  // TODO: fix selection state
3668
3789
  // TODO: fix active state
@@ -3709,7 +3830,7 @@
3709
3830
  n.removeMarkup();
3710
3831
  tree._unregisterNode(n);
3711
3832
  }, true);
3712
- tree.setModified(ChangeType.structure);
3833
+ tree.update(ChangeType.structure);
3713
3834
  }
3714
3835
  /** Remove all descendants of this node. */
3715
3836
  removeChildren() {
@@ -3741,7 +3862,7 @@
3741
3862
  if (!this.isRootNode()) {
3742
3863
  this.expanded = false;
3743
3864
  }
3744
- this.tree.setModified(ChangeType.structure);
3865
+ this.tree.update(ChangeType.structure);
3745
3866
  }
3746
3867
  /** Remove all HTML markup from the DOM. */
3747
3868
  removeMarkup() {
@@ -3836,12 +3957,13 @@
3836
3957
  }
3837
3958
  /**
3838
3959
  * Create a whole new `<div class="wb-row">` element.
3839
- * @see {@link WunderbaumNode.render}
3960
+ * @see {@link WunderbaumNode._render}
3840
3961
  */
3841
3962
  _render_markup(opts) {
3842
3963
  const tree = this.tree;
3843
3964
  const treeOptions = tree.options;
3844
- const checkbox = this.getOption("checkbox") !== false;
3965
+ const checkbox = this.getOption("checkbox");
3966
+ // const checkbox = this.getOption("checkbox") !== false;
3845
3967
  const columns = tree.columns;
3846
3968
  const level = this.getLevel();
3847
3969
  let elem;
@@ -3869,6 +3991,9 @@
3869
3991
  if (checkbox) {
3870
3992
  checkboxSpan = document.createElement("i");
3871
3993
  checkboxSpan.classList.add("wb-checkbox");
3994
+ if (checkbox === "radio" || this.parent.radiogroup) {
3995
+ checkboxSpan.classList.add("wb-radio");
3996
+ }
3872
3997
  nodeElem.appendChild(checkboxSpan);
3873
3998
  ofsTitlePx += ICON_WIDTH;
3874
3999
  }
@@ -3949,7 +4074,7 @@
3949
4074
  /**
3950
4075
  * Render `node.title`, `.icon` into an existing row.
3951
4076
  *
3952
- * @see {@link WunderbaumNode.render}
4077
+ * @see {@link WunderbaumNode._render}
3953
4078
  */
3954
4079
  _render_data(opts) {
3955
4080
  assert(this._rowElem);
@@ -4015,7 +4140,7 @@
4015
4140
  }
4016
4141
  /**
4017
4142
  * Update row classes to reflect active, focuses, etc.
4018
- * @see {@link WunderbaumNode.render}
4143
+ * @see {@link WunderbaumNode._render}
4019
4144
  */
4020
4145
  _render_status(opts) {
4021
4146
  // this.log("_render_status", opts);
@@ -4031,6 +4156,7 @@
4031
4156
  this.expanded ? rowClasses.push("wb-expanded") : 0;
4032
4157
  this.lazy ? rowClasses.push("wb-lazy") : 0;
4033
4158
  this.selected ? rowClasses.push("wb-selected") : 0;
4159
+ this._partsel ? rowClasses.push("wb-partsel") : 0;
4034
4160
  this === tree.activeNode ? rowClasses.push("wb-active") : 0;
4035
4161
  this === tree.focusNode ? rowClasses.push("wb-focus") : 0;
4036
4162
  this._errorInfo ? rowClasses.push("wb-error") : 0;
@@ -4070,12 +4196,30 @@
4070
4196
  }
4071
4197
  }
4072
4198
  if (checkboxSpan) {
4073
- if (this.selected) {
4074
- checkboxSpan.className = "wb-checkbox " + iconMap.checkChecked;
4199
+ let cbclass = "wb-checkbox ";
4200
+ if (this.parent.radiogroup) {
4201
+ cbclass += "wb-radio ";
4202
+ if (this.selected) {
4203
+ cbclass += iconMap.radioChecked;
4204
+ // } else if (this._partsel) {
4205
+ // cbclass += iconMap.radioUnknown;
4206
+ }
4207
+ else {
4208
+ cbclass += iconMap.radioUnchecked;
4209
+ }
4075
4210
  }
4076
4211
  else {
4077
- checkboxSpan.className = "wb-checkbox " + iconMap.checkUnchecked;
4212
+ if (this.selected) {
4213
+ cbclass += iconMap.checkChecked;
4214
+ }
4215
+ else if (this._partsel) {
4216
+ cbclass += iconMap.checkUnknown;
4217
+ }
4218
+ else {
4219
+ cbclass += iconMap.checkUnchecked;
4220
+ }
4078
4221
  }
4222
+ checkboxSpan.className = cbclass;
4079
4223
  }
4080
4224
  // Fix active cell in cell-nav mode
4081
4225
  if (!opts.isNew) {
@@ -4103,7 +4247,7 @@
4103
4247
  }
4104
4248
  }
4105
4249
  }
4106
- /**
4250
+ /*
4107
4251
  * Create or update node's markup.
4108
4252
  *
4109
4253
  * `options.change` defaults to ChangeType.data, which updates the title,
@@ -4114,10 +4258,10 @@
4114
4258
  * `options.change` should be set to ChangeType.status instead for best
4115
4259
  * efficiency.
4116
4260
  *
4117
- * Calling `setModified` instead may be a better alternative.
4118
- * @see {@link WunderbaumNode.setModified}
4261
+ * Calling `update()` is almost always a better alternative.
4262
+ * @see {@link WunderbaumNode.update}
4119
4263
  */
4120
- render(options) {
4264
+ _render(options) {
4121
4265
  // this.log("render", options);
4122
4266
  const opts = Object.assign({ change: ChangeType.data }, options);
4123
4267
  if (!this._rowElem) {
@@ -4147,7 +4291,7 @@
4147
4291
  this.expanded = false;
4148
4292
  this.lazy = true;
4149
4293
  this.children = null;
4150
- this.tree.setModified(ChangeType.structure);
4294
+ this.tree.update(ChangeType.structure);
4151
4295
  }
4152
4296
  /** Convert node (or whole branch) into a plain object.
4153
4297
  *
@@ -4162,7 +4306,7 @@
4162
4306
  */
4163
4307
  toDict(recursive = false, callback) {
4164
4308
  const dict = {};
4165
- NODE_ATTRS.forEach((propName) => {
4309
+ NODE_DICT_PROPS.forEach((propName) => {
4166
4310
  const val = this[propName];
4167
4311
  if (val instanceof Set) {
4168
4312
  // Convert Set to string (or skip if set is empty)
@@ -4287,7 +4431,7 @@
4287
4431
  return;
4288
4432
  }
4289
4433
  tree.activeNode = null;
4290
- prev === null || prev === void 0 ? void 0 : prev.setModified(ChangeType.status);
4434
+ prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status);
4291
4435
  }
4292
4436
  }
4293
4437
  else if (prev === this || retrigger) {
@@ -4302,8 +4446,8 @@
4302
4446
  if (focusTree)
4303
4447
  tree.setFocus();
4304
4448
  }
4305
- prev === null || prev === void 0 ? void 0 : prev.setModified(ChangeType.status);
4306
- this.setModified(ChangeType.status);
4449
+ prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status);
4450
+ this.update(ChangeType.status);
4307
4451
  }
4308
4452
  if (options &&
4309
4453
  options.colIdx != null &&
@@ -4332,13 +4476,16 @@
4332
4476
  return; // Nothing to do
4333
4477
  }
4334
4478
  // this.log("setExpanded()");
4479
+ if (flag && this.getOption("autoCollapse")) {
4480
+ this.collapseSiblings(options);
4481
+ }
4335
4482
  if (flag && this.lazy && this.children == null) {
4336
4483
  await this.loadLazy();
4337
4484
  }
4338
4485
  this.expanded = flag;
4339
4486
  const updateOpts = { immediate: immediate };
4340
4487
  // const updateOpts = { immediate: !!util.getOption(options, "immediate") };
4341
- this.tree.setModified(ChangeType.structure, updateOpts);
4488
+ this.tree.update(ChangeType.structure, updateOpts);
4342
4489
  if (flag && scrollIntoView !== false) {
4343
4490
  const lastChild = this.getLastChild();
4344
4491
  if (lastChild) {
@@ -4355,18 +4502,25 @@
4355
4502
  assert(!!flag, "blur is not yet implemented");
4356
4503
  const prev = this.tree.focusNode;
4357
4504
  this.tree.focusNode = this;
4358
- prev === null || prev === void 0 ? void 0 : prev.setModified();
4359
- this.setModified();
4505
+ prev === null || prev === void 0 ? void 0 : prev.update();
4506
+ this.update();
4360
4507
  }
4361
4508
  /** Set a new icon path or class. */
4362
4509
  setIcon(icon) {
4363
4510
  this.icon = icon;
4364
- this.setModified();
4511
+ this.update();
4365
4512
  }
4366
4513
  /** Change node's {@link key} and/or {@link refKey}. */
4367
4514
  setKey(key, refKey) {
4368
4515
  throw new Error("Not yet implemented");
4369
4516
  }
4517
+ /**
4518
+ * @deprecated since v0.3.6: use `update()` instead.
4519
+ */
4520
+ setModified(change = ChangeType.data) {
4521
+ this.logWarn("setModified() is deprecated: use update() instead.");
4522
+ return this.update(change);
4523
+ }
4370
4524
  /**
4371
4525
  * Trigger a repaint, typically after a status or data change.
4372
4526
  *
@@ -4374,22 +4528,219 @@
4374
4528
  * and column content. It can be reduced to 'ChangeType.status' if only
4375
4529
  * active/focus/selected state has changed.
4376
4530
  *
4377
- * This method will eventually call {@link WunderbaumNode.render()} with
4531
+ * This method will eventually call {@link WunderbaumNode._render()} with
4378
4532
  * default options, but may be more consistent with the tree's
4379
- * {@link Wunderbaum.setModified()} API.
4533
+ * {@link Wunderbaum.update()} API.
4380
4534
  */
4381
- setModified(change = ChangeType.data) {
4535
+ update(change = ChangeType.data) {
4382
4536
  assert(change === ChangeType.status || change === ChangeType.data);
4383
- this.tree.setModified(change, this);
4537
+ this.tree.update(change, this);
4538
+ }
4539
+ /**
4540
+ * Return an array of selected nodes.
4541
+ * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier')
4542
+ */
4543
+ getSelectedNodes(stopOnParents = false) {
4544
+ let nodeList = [];
4545
+ this.visit((node) => {
4546
+ if (node.selected) {
4547
+ nodeList.push(node);
4548
+ if (stopOnParents === true) {
4549
+ return "skip"; // stop processing this branch
4550
+ }
4551
+ }
4552
+ });
4553
+ return nodeList;
4554
+ }
4555
+ /** Toggle the check/uncheck state. */
4556
+ toggleSelected(options) {
4557
+ let flag = this.isSelected();
4558
+ if (flag === undefined) {
4559
+ flag = this._anySelectable();
4560
+ }
4561
+ else {
4562
+ flag = !flag;
4563
+ }
4564
+ return this.setSelected(flag, options);
4565
+ }
4566
+ /** Return true if at least on selectable descendant end-node is unselected. @internal */
4567
+ _anySelectable() {
4568
+ let found = false;
4569
+ this.visit((node) => {
4570
+ if (node.selected === false &&
4571
+ !node.unselectable &&
4572
+ !node.hasChildren() &&
4573
+ !node.parent.radiogroup) {
4574
+ found = true;
4575
+ return false; // Stop iteration
4576
+ }
4577
+ });
4578
+ return found;
4579
+ }
4580
+ /* Apply selection state to a single node. */
4581
+ _changeSelectStatusProps(state) {
4582
+ let changed = false;
4583
+ switch (state) {
4584
+ case false:
4585
+ changed = this.selected || this._partsel;
4586
+ this.selected = false;
4587
+ this._partsel = false;
4588
+ break;
4589
+ case true:
4590
+ changed = !this.selected || !this._partsel;
4591
+ this.selected = true;
4592
+ this._partsel = true;
4593
+ break;
4594
+ case undefined:
4595
+ changed = this.selected || !this._partsel;
4596
+ this.selected = false;
4597
+ this._partsel = true;
4598
+ break;
4599
+ default:
4600
+ error(`Invalid state: ${state}`);
4601
+ }
4602
+ if (changed) {
4603
+ this.update();
4604
+ }
4605
+ return changed;
4606
+ }
4607
+ /**
4608
+ * Fix selection status, after this node was (de)selected in `selectMode: 'hier'`.
4609
+ * This includes (de)selecting all descendants.
4610
+ */
4611
+ fixSelection3AfterClick(opts) {
4612
+ const force = !!(opts === null || opts === void 0 ? void 0 : opts.force);
4613
+ let flag = this.isSelected();
4614
+ this.visit((node) => {
4615
+ if (node.radiogroup) {
4616
+ return "skip"; // Don't (de)select this branch
4617
+ }
4618
+ if (force || !node.getOption("unselectable")) {
4619
+ node._changeSelectStatusProps(flag);
4620
+ }
4621
+ });
4622
+ this.fixSelection3FromEndNodes();
4623
+ }
4624
+ /**
4625
+ * Fix selection status for multi-hier mode.
4626
+ * Only end-nodes are considered to update the descendants branch and parents.
4627
+ * Should be called after this node has loaded new children or after
4628
+ * children have been modified using the API.
4629
+ */
4630
+ fixSelection3FromEndNodes(opts) {
4631
+ const force = !!(opts === null || opts === void 0 ? void 0 : opts.force);
4632
+ assert(this.tree.options.selectMode === "hier", "expected selectMode 'hier'");
4633
+ // Visit all end nodes and adjust their parent's `selected` and `_partsel`
4634
+ // attributes. Return selection state true, false, or undefined.
4635
+ const _walk = (node) => {
4636
+ let state;
4637
+ const children = node.children;
4638
+ if (children && children.length) {
4639
+ // check all children recursively
4640
+ let allSelected = true;
4641
+ let someSelected = false;
4642
+ for (let i = 0, l = children.length; i < l; i++) {
4643
+ const child = children[i];
4644
+ // the selection state of a node is not relevant; we need the end-nodes
4645
+ const s = _walk(child);
4646
+ if (s !== false) {
4647
+ someSelected = true;
4648
+ }
4649
+ if (s !== true) {
4650
+ allSelected = false;
4651
+ }
4652
+ }
4653
+ state = allSelected ? true : someSelected ? undefined : false;
4654
+ }
4655
+ else {
4656
+ // This is an end-node: simply report the status
4657
+ state = !!node.selected;
4658
+ }
4659
+ // #939: Keep a `_partsel` flag that was explicitly set on a lazy node
4660
+ if (node._partsel &&
4661
+ !node.selected &&
4662
+ node.lazy &&
4663
+ node.children == null) {
4664
+ state = undefined;
4665
+ }
4666
+ if (force || !node.getOption("unselectable")) {
4667
+ node._changeSelectStatusProps(state);
4668
+ }
4669
+ return state;
4670
+ };
4671
+ _walk(this);
4672
+ // Update parent's state
4673
+ this.visitParents((node) => {
4674
+ let state;
4675
+ const children = node.children;
4676
+ let allSelected = true;
4677
+ let someSelected = false;
4678
+ for (let i = 0, l = children.length; i < l; i++) {
4679
+ const child = children[i];
4680
+ state = !!child.selected;
4681
+ // When fixing the parents, we trust the sibling status (i.e. we don't recurse)
4682
+ if (state || child._partsel) {
4683
+ someSelected = true;
4684
+ }
4685
+ if (!state) {
4686
+ allSelected = false;
4687
+ }
4688
+ }
4689
+ state = allSelected ? true : someSelected ? undefined : false;
4690
+ node._changeSelectStatusProps(state);
4691
+ });
4384
4692
  }
4385
4693
  /** Modify the check/uncheck state. */
4386
4694
  setSelected(flag = true, options) {
4387
- const prev = this.selected;
4388
- if (!!flag !== prev) {
4695
+ const tree = this.tree;
4696
+ const sendEvents = !(options === null || options === void 0 ? void 0 : options.noEvents); // Default: send events
4697
+ const prev = this.isSelected();
4698
+ const isRadio = this.parent && this.parent.radiogroup;
4699
+ const selectMode = tree.options.selectMode;
4700
+ const canSelect = (options === null || options === void 0 ? void 0 : options.force) || !this.getOption("unselectable");
4701
+ flag = !!flag;
4702
+ // this.logDebug(`setSelected(${flag})`, this);
4703
+ if (!canSelect) {
4704
+ return prev;
4705
+ }
4706
+ if ((options === null || options === void 0 ? void 0 : options.propagateDown) && selectMode === "multi") {
4707
+ tree.runWithDeferredUpdate(() => {
4708
+ this.visit((node) => {
4709
+ node.setSelected(flag);
4710
+ });
4711
+ });
4712
+ return prev;
4713
+ }
4714
+ if (flag === prev ||
4715
+ (sendEvents && this._callEvent("beforeSelect", { flag: flag }) === false)) {
4716
+ return prev;
4717
+ }
4718
+ tree.runWithDeferredUpdate(() => {
4719
+ if (isRadio) {
4720
+ // Radiobutton Group
4721
+ if (!flag && !(options === null || options === void 0 ? void 0 : options.force)) {
4722
+ return prev; // don't uncheck radio buttons
4723
+ }
4724
+ for (let sibling of this.parent.children) {
4725
+ sibling.selected = sibling === this;
4726
+ }
4727
+ }
4728
+ else {
4729
+ this.selected = flag;
4730
+ if (selectMode === "hier") {
4731
+ this.fixSelection3AfterClick();
4732
+ }
4733
+ else if (selectMode === "single") {
4734
+ tree.visit((n) => {
4735
+ n.selected = false;
4736
+ });
4737
+ }
4738
+ }
4739
+ });
4740
+ if (sendEvents) {
4389
4741
  this._callEvent("select", { flag: flag });
4390
4742
  }
4391
- this.selected = !!flag;
4392
- this.setModified();
4743
+ return prev;
4393
4744
  }
4394
4745
  /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
4395
4746
  setStatus(status, options) {
@@ -4414,7 +4765,7 @@
4414
4765
  assert(!firstChild || !firstChild.isStatusNode());
4415
4766
  statusNode = this.addNode(data, "prependChild");
4416
4767
  statusNode.match = true;
4417
- tree.setModified(ChangeType.structure);
4768
+ tree.update(ChangeType.structure);
4418
4769
  return statusNode;
4419
4770
  };
4420
4771
  _clearStatusNode();
@@ -4427,7 +4778,7 @@
4427
4778
  this._isLoading = true;
4428
4779
  this._errorInfo = null;
4429
4780
  if (this.parent) {
4430
- this.setModified(ChangeType.status);
4781
+ this.update(ChangeType.status);
4431
4782
  }
4432
4783
  else {
4433
4784
  // If this is the invisible root, add a visible top-level node
@@ -4440,7 +4791,7 @@
4440
4791
  tooltip: details,
4441
4792
  });
4442
4793
  }
4443
- // this.render();
4794
+ // this.update();
4444
4795
  break;
4445
4796
  case "error":
4446
4797
  _setStatusNode({
@@ -4469,13 +4820,13 @@
4469
4820
  default:
4470
4821
  error("invalid node status " + status);
4471
4822
  }
4472
- tree.setModified(ChangeType.structure);
4823
+ tree.update(ChangeType.structure);
4473
4824
  return statusNode;
4474
4825
  }
4475
4826
  /** Rename this node. */
4476
4827
  setTitle(title) {
4477
4828
  this.title = title;
4478
- this.setModified();
4829
+ this.update();
4479
4830
  // this.triggerModify("rename"); // TODO
4480
4831
  }
4481
4832
  _sortChildren(cmp, deep) {
@@ -4500,7 +4851,7 @@
4500
4851
  */
4501
4852
  sortChildren(cmp = nodeTitleSorter, deep = false) {
4502
4853
  this._sortChildren(cmp || nodeTitleSorter, deep);
4503
- this.tree.setModified(ChangeType.structure);
4854
+ this.tree.update(ChangeType.structure);
4504
4855
  // this.triggerModify("sort"); // TODO
4505
4856
  }
4506
4857
  /**
@@ -4528,7 +4879,7 @@
4528
4879
  this.parent.triggerModifyChild(operation, this, extra);
4529
4880
  }
4530
4881
  /**
4531
- * Call `callback(node)` for all child nodes in hierarchical order (depth-first, pre-order).
4882
+ * Call `callback(node)` for all descendant nodes in hierarchical order (depth-first, pre-order).
4532
4883
  *
4533
4884
  * Stop iteration, if fn() returns false. Skip current branch, if fn()
4534
4885
  * returns "skip".<br>
@@ -4608,7 +4959,7 @@
4608
4959
  /*!
4609
4960
  * Wunderbaum - ext-edit
4610
4961
  * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
4611
- * v0.3.4-0, Sun, 18 Jun 2023 16:04:04 GMT (https://github.com/mar10/wunderbaum)
4962
+ * v0.4.0, Wed, 06 Sep 2023 08:21:56 GMT (https://github.com/mar10/wunderbaum)
4612
4963
  */
4613
4964
  // const START_MARKER = "\uFFF7";
4614
4965
  class EditExtension extends WunderbaumExtension {
@@ -4836,7 +5187,7 @@
4836
5187
  node === null || node === void 0 ? void 0 : node.setTitle(newValue);
4837
5188
  // NOTE: At least on Safari, this render call triggers a scroll event
4838
5189
  // probably because the focused input is replaced.
4839
- this.curEditNode.render({ preventScroll: true });
5190
+ this.curEditNode._render({ preventScroll: true });
4840
5191
  this.curEditNode = null;
4841
5192
  this.relatedNode = null;
4842
5193
  this.tree.setFocus(); // restore focus that was in the input element
@@ -4851,7 +5202,7 @@
4851
5202
  // Discard the embedded `<input>`
4852
5203
  // NOTE: At least on Safari, this render call triggers a scroll event
4853
5204
  // probably because the focused input is replaced.
4854
- this.curEditNode.render({ preventScroll: true });
5205
+ this.curEditNode._render({ preventScroll: true });
4855
5206
  this.curEditNode = null;
4856
5207
  this.relatedNode = null;
4857
5208
  // We discarded the <input>, so we have to acquire keyboard focus again
@@ -4904,9 +5255,10 @@
4904
5255
  * https://github.com/mar10/wunderbaum
4905
5256
  *
4906
5257
  * Released under the MIT license.
4907
- * @version v0.3.4-0
4908
- * @date Sun, 18 Jun 2023 16:04:04 GMT
5258
+ * @version v0.4.0
5259
+ * @date Wed, 06 Sep 2023 08:21:56 GMT
4909
5260
  */
5261
+ // import "./wunderbaum.scss";
4910
5262
  class WbSystemRoot extends WunderbaumNode {
4911
5263
  constructor(tree) {
4912
5264
  super(tree, null, {
@@ -4948,6 +5300,9 @@
4948
5300
  this.pendingChangeTypes = new Set();
4949
5301
  /** Expose some useful methods of the util.ts module as `tree._util`. */
4950
5302
  this._util = util;
5303
+ // --- SELECT ---
5304
+ // /** @internal */
5305
+ // public selectRangeAnchor: WunderbaumNode | null = null;
4951
5306
  // --- FILTER ---
4952
5307
  this.filterMode = null;
4953
5308
  // --- KEYNAV ---
@@ -4982,9 +5337,10 @@
4982
5337
  checkbox: false,
4983
5338
  minExpandLevel: 0,
4984
5339
  emptyChildListExpandable: false,
4985
- updateThrottleWait: 200,
5340
+ // updateThrottleWait: 200,
4986
5341
  skeleton: false,
4987
5342
  connectTopBreadcrumb: null,
5343
+ selectMode: "multi",
4988
5344
  // --- KeyNav ---
4989
5345
  navigationModeOption: null,
4990
5346
  quicksearch: true,
@@ -5140,26 +5496,34 @@
5140
5496
  }
5141
5497
  // Async mode is sometimes required, because this.element.clientWidth
5142
5498
  // has a wrong value at start???
5143
- this.setModified(ChangeType.any);
5499
+ this.update(ChangeType.any);
5144
5500
  // --- Bind listeners
5145
5501
  this.element.addEventListener("scroll", (e) => {
5146
5502
  // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
5147
- this.setModified(ChangeType.scroll);
5503
+ this.update(ChangeType.scroll);
5148
5504
  });
5149
5505
  this.resizeObserver = new ResizeObserver((entries) => {
5150
5506
  // this.log("ResizeObserver: Size changed", entries);
5151
- this.setModified(ChangeType.resize);
5507
+ this.update(ChangeType.resize);
5152
5508
  });
5153
5509
  this.resizeObserver.observe(this.element);
5154
5510
  onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
5155
5511
  const info = Wunderbaum.getEventInfo(e);
5156
5512
  const node = info.node;
5157
- // this.log("click", info, e);
5513
+ const mouseEvent = e;
5514
+ // this.log("click", info);
5515
+ // if (this._selectRange(info) === false) {
5516
+ // return;
5517
+ // }
5158
5518
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
5159
5519
  this.lastClickTime = Date.now();
5160
5520
  return false;
5161
5521
  }
5162
5522
  if (node) {
5523
+ if (mouseEvent.ctrlKey) {
5524
+ node.toggleSelected();
5525
+ return;
5526
+ }
5163
5527
  // Edit title if 'clickActive' is triggered:
5164
5528
  const trigger = this.getOption("edit.trigger");
5165
5529
  const slowClickDelay = this.getOption("edit.slowClickDelay");
@@ -5179,7 +5543,7 @@
5179
5543
  node.setExpanded(!node.isExpanded());
5180
5544
  }
5181
5545
  else if (info.region === NodeRegion.checkbox) {
5182
- node.setSelected(!node.isSelected());
5546
+ node.toggleSelected();
5183
5547
  }
5184
5548
  }
5185
5549
  this.lastClickTime = Date.now();
@@ -5626,7 +5990,7 @@
5626
5990
  // public cellNavMode = false;
5627
5991
  // public lastQuicksearchTime = 0;
5628
5992
  // public lastQuicksearchTerm = "";
5629
- this.setModified(ChangeType.structure);
5993
+ this.update(ChangeType.structure);
5630
5994
  }
5631
5995
  /**
5632
5996
  * Clear nodes and markup and detach events and observers.
@@ -5684,7 +6048,7 @@
5684
6048
  this.options[name] = value;
5685
6049
  switch (name) {
5686
6050
  case "checkbox":
5687
- this.setModified(ChangeType.any);
6051
+ this.update(ChangeType.any);
5688
6052
  break;
5689
6053
  case "enabled":
5690
6054
  this.setEnabled(!!value);
@@ -5707,8 +6071,15 @@
5707
6071
  const header = this.options.header;
5708
6072
  return this.isGrid() ? header !== false : !!header;
5709
6073
  }
5710
- /** Run code, but defer rendering of viewport until done. */
5711
- runWithoutUpdate(func, hint = null) {
6074
+ /** Run code, but defer rendering of viewport until done.
6075
+ *
6076
+ * ```
6077
+ * tree.runWithDeferredUpdate(() => {
6078
+ * return someFuncThatWouldUpdateManyNodes();
6079
+ * });
6080
+ * ```
6081
+ */
6082
+ runWithDeferredUpdate(func, hint = null) {
5712
6083
  try {
5713
6084
  this.enableUpdate(false);
5714
6085
  const res = func();
@@ -5719,29 +6090,66 @@
5719
6090
  this.enableUpdate(true);
5720
6091
  }
5721
6092
  }
5722
- /** Recursively expand all expandable nodes (triggers lazy load id needed). */
6093
+ /** Recursively expand all expandable nodes (triggers lazy load if needed). */
5723
6094
  async expandAll(flag = true, options) {
5724
6095
  await this.root.expandAll(flag, options);
5725
6096
  }
5726
6097
  /** Recursively select all nodes. */
5727
6098
  selectAll(flag = true) {
5728
- try {
5729
- this.enableUpdate(false);
5730
- this.visit((node) => {
5731
- node.setSelected(flag);
5732
- });
5733
- }
5734
- finally {
5735
- this.enableUpdate(true);
5736
- }
6099
+ return this.root.setSelected(flag, { propagateDown: true });
6100
+ }
6101
+ /** Toggle select all nodes. */
6102
+ toggleSelect() {
6103
+ this.selectAll(this.root._anySelectable());
6104
+ }
6105
+ /**
6106
+ * Return an array of selected nodes.
6107
+ * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier')
6108
+ */
6109
+ getSelectedNodes(stopOnParents = false) {
6110
+ return this.root.getSelectedNodes(stopOnParents);
6111
+ }
6112
+ /*
6113
+ * Return an array of selected nodes.
6114
+ */
6115
+ _selectRange(eventInfo) {
6116
+ this.logDebug("_selectRange", eventInfo);
6117
+ error("Not yet implemented.");
6118
+ // const mode = this.options.selectMode!;
6119
+ // if (mode !== "multi") {
6120
+ // this.logDebug(`Range selection only available for selectMode 'multi'`);
6121
+ // return;
6122
+ // }
6123
+ // if (eventInfo.canonicalName === "Meta+click") {
6124
+ // eventInfo.node?.toggleSelected();
6125
+ // return false; // don't
6126
+ // } else if (eventInfo.canonicalName === "Shift+click") {
6127
+ // let from = this.activeNode;
6128
+ // let to = eventInfo.node;
6129
+ // if (!from || !to || from === to) {
6130
+ // return;
6131
+ // }
6132
+ // this.runWithDeferredUpdate(() => {
6133
+ // this.visitRows(
6134
+ // (node) => {
6135
+ // node.setSelected();
6136
+ // },
6137
+ // {
6138
+ // includeHidden: true,
6139
+ // includeSelf: false,
6140
+ // start: from,
6141
+ // reverse: from!._rowIdx! > to!._rowIdx!,
6142
+ // }
6143
+ // );
6144
+ // });
6145
+ // return false;
6146
+ // }
5737
6147
  }
5738
- /** Return the number of nodes in the data model.*/
6148
+ /** Return the number of nodes in the data model.
6149
+ * @param visible if true, nodes that are hidden due to collapsed parents are ignored.
6150
+ */
5739
6151
  count(visible = false) {
5740
- if (visible) {
5741
- return this.treeRowCount;
5742
- // return this.viewNodes.size;
5743
- }
5744
- return this.keyMap.size;
6152
+ return visible ? this.treeRowCount : this.keyMap.size;
5745
6153
  }
5746
6154
  /** @internal sanity check. */
5747
6155
  _check() {
@@ -5832,7 +6240,7 @@
5832
6240
  break;
5833
6241
  case "first":
5834
6242
  // First visible node
5835
- this.visit(function (n) {
6243
+ this.visit((n) => {
5836
6244
  if (n.isVisible()) {
5837
6245
  res = n;
5838
6246
  return false;
@@ -5840,7 +6248,7 @@
5840
6248
  });
5841
6249
  break;
5842
6250
  case "last":
5843
- this.visit(function (n) {
6251
+ this.visit((n) => {
5844
6252
  // last visible node
5845
6253
  if (n.isVisible()) {
5846
6254
  res = n;
@@ -5972,6 +6380,8 @@
5972
6380
  */
5973
6381
  static getEventInfo(event) {
5974
6382
  let target = event.target, cl = target.classList, parentCol = target.closest("span.wb-col"), node = Wunderbaum.getNode(target), tree = node ? node.tree : Wunderbaum.getTree(event), res = {
6383
+ event: event,
6384
+ canonicalName: eventToString(event),
5975
6385
  tree: tree,
5976
6386
  node: node,
5977
6387
  region: NodeRegion.unknown,
@@ -6137,7 +6547,7 @@
6137
6547
  // Make sure the topNode is always visible
6138
6548
  this.scrollTo(topNode);
6139
6549
  }
6140
- // this.setModified(ChangeType.scroll);
6550
+ // this.update(ChangeType.scroll);
6141
6551
  }
6142
6552
  }
6143
6553
  /**
@@ -6165,7 +6575,7 @@
6165
6575
  // util.assert(node._rowIdx != null);
6166
6576
  this.log(`scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`);
6167
6577
  this.element.scrollLeft = newLeft;
6168
- // this.setModified(ChangeType.scroll);
6578
+ // this.update(ChangeType.scroll);
6169
6579
  }
6170
6580
  /**
6171
6581
  * Set column #colIdx to 'active'.
@@ -6187,7 +6597,7 @@
6187
6597
  }
6188
6598
  }
6189
6599
  }
6190
- (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.setModified(ChangeType.status);
6600
+ (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status);
6191
6601
  // Update `wb-active` class for all cell spans
6192
6602
  for (let rowDiv of this.nodeListElement.children) {
6193
6603
  let i = 0;
@@ -6214,16 +6624,25 @@
6214
6624
  this.element.blur();
6215
6625
  }
6216
6626
  }
6217
- setModified(change, node, options) {
6627
+ /**
6628
+ * @deprecated since v0.3.6: use `update()` instead.
6629
+ */
6630
+ setModified(change, ...args) {
6631
+ this.logWarn("setModified() is deprecated: use update() instead.");
6632
+ // @ts-ignore
6633
+ // (!) TS2556: A spread argument must either have a tuple type or be passed to a rest parameter.
6634
+ return this.update.call(this, change, ...args);
6635
+ }
6636
+ update(change, node, options) {
6218
6637
  if (this._disableUpdateCount) {
6219
6638
  // Assuming that we redraw all when enableUpdate() is re-enabled.
6220
6639
  // this.log(
6221
- // `IGNORED setModified(${change}) node=${node} (disable level ${this._disableUpdateCount})`
6640
+ // `IGNORED update(${change}) node=${node} (disable level ${this._disableUpdateCount})`
6222
6641
  // );
6223
6642
  this._disableUpdateIgnoreCount++;
6224
6643
  return;
6225
6644
  }
6226
- // this.log(`setModified(${change}) node=${node}`);
6645
+ // this.log(`update(${change}) node=${node}`);
6227
6646
  if (!(node instanceof WunderbaumNode)) {
6228
6647
  options = node;
6229
6648
  node = null;
@@ -6257,7 +6676,7 @@
6257
6676
  // Single nodes are immediately updated if already inside the viewport
6258
6677
  // (otherwise we can ignore)
6259
6678
  if (node._rowElem) {
6260
- node.render({ change: change });
6679
+ node._render({ change: change });
6261
6680
  }
6262
6681
  break;
6263
6682
  default:
@@ -6315,7 +6734,7 @@
6315
6734
  this.setColumn(0);
6316
6735
  }
6317
6736
  this.element.classList.toggle("wb-cell-mode", flag);
6318
- (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.setModified(ChangeType.status);
6737
+ (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status);
6319
6738
  }
6320
6739
  /** Set the tree's navigation mode option. */
6321
6740
  setNavigationOption(mode, reset = false) {
@@ -6479,7 +6898,7 @@
6479
6898
  // if (modified) {
6480
6899
  // this._renderHeaderMarkup();
6481
6900
  // if (options.renderMarkup) {
6482
- // this.setModified(ChangeType.header, { removeMarkup: true });
6901
+ // this.update(ChangeType.header, { removeMarkup: true });
6483
6902
  // } else if (options.updateRows) {
6484
6903
  // this._updateRows();
6485
6904
  // }
@@ -6506,7 +6925,14 @@
6506
6925
  colElem.style.left = col._ofsPx + "px";
6507
6926
  colElem.style.width = col._widthPx + "px";
6508
6927
  // Add classes from `columns` definition to `<div.wb-col>` cells
6509
- col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
6928
+ if (typeof col.headerClasses === "string") {
6929
+ col.headerClasses
6930
+ ? colElem.classList.add(...col.headerClasses.split(" "))
6931
+ : 0;
6932
+ }
6933
+ else {
6934
+ col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
6935
+ }
6510
6936
  const title = escapeHtml(col.title || col.id);
6511
6937
  let tooltip = "";
6512
6938
  if (col.tooltip) {
@@ -6524,11 +6950,11 @@
6524
6950
  }
6525
6951
  }
6526
6952
  /**
6527
- * Render pending changes that were scheduled using {@link WunderbaumNode.setModified} if any.
6953
+ * Render pending changes that were scheduled using {@link WunderbaumNode.update} if any.
6528
6954
  *
6529
6955
  * This is hardly ever neccessary, since we normally either
6530
- * - call `setModified(ChangeType.TYPE)` (async, throttled), or
6531
- * - call `setModified(ChangeType.TYPE, {immediate: true})` (synchronous)
6956
+ * - call `update(ChangeType.TYPE)` (async, throttled), or
6957
+ * - call `update(ChangeType.TYPE, {immediate: true})` (synchronous)
6532
6958
  *
6533
6959
  * `updatePendingModifications()` will only force immediate execution of
6534
6960
  * pending async changes if any.
@@ -6543,7 +6969,7 @@
6543
6969
  * It calls `updateColumns()` and `_updateRows()`.
6544
6970
  *
6545
6971
  * This protected method should not be called directly but via
6546
- * {@link WunderbaumNode.setModified}`, {@link Wunderbaum.setModified},
6972
+ * {@link WunderbaumNode.update}`, {@link Wunderbaum.update},
6547
6973
  * or {@link Wunderbaum.updatePendingModifications}.
6548
6974
  * @internal
6549
6975
  */
@@ -6698,7 +7124,7 @@
6698
7124
  if (rowDiv) {
6699
7125
  rowDiv.style.top = idx * ROW_HEIGHT + "px";
6700
7126
  }
6701
- node.render({ top: top, after: prevElem });
7127
+ node._render({ top: top, after: prevElem });
6702
7128
  // node.log("render", top, prevElem, "=>", node._rowElem);
6703
7129
  prevElem = node._rowElem;
6704
7130
  }
@@ -6778,7 +7204,7 @@
6778
7204
  if (node.children &&
6779
7205
  node.children.length &&
6780
7206
  (includeHidden || node.expanded)) {
6781
- res = node.visit(function (n) {
7207
+ res = node.visit((n) => {
6782
7208
  if (n === stopNode) {
6783
7209
  return false;
6784
7210
  }
@@ -6898,7 +7324,7 @@
6898
7324
  if (this._disableUpdateCount === 0) {
6899
7325
  this.logDebug(`enableUpdate(): active again. Re-painting to catch up with ${this._disableUpdateIgnoreCount} ignored update requests...`);
6900
7326
  this._disableUpdateIgnoreCount = 0;
6901
- this.setModified(ChangeType.any, { immediate: true });
7327
+ this.update(ChangeType.any, { immediate: true });
6902
7328
  }
6903
7329
  }
6904
7330
  else {
@@ -6952,7 +7378,7 @@
6952
7378
  }
6953
7379
  Wunderbaum.sequence = 0;
6954
7380
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
6955
- Wunderbaum.version = "v0.3.4-0"; // Set to semver by 'grunt release'
7381
+ Wunderbaum.version = "v0.4.0"; // Set to semver by 'grunt release'
6956
7382
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
6957
7383
  Wunderbaum.util = util;
6958
7384