wunderbaum 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Wunderbaum - debounce.ts
3
3
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
4
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
4
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
  /*
7
7
  * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21
@@ -293,7 +293,7 @@ function throttle(func, wait = 0, options = {}) {
293
293
  /*!
294
294
  * Wunderbaum - util
295
295
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
296
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
296
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
297
297
  */
298
298
  /** @module util */
299
299
  /** Readable names for `MouseEvent.button` */
@@ -443,7 +443,7 @@ function each(obj, callback) {
443
443
  }
444
444
  return obj;
445
445
  }
446
- /** Shortcut for `throw new Error(msg)`.*/
446
+ /** Shortcut for `throw new Error(msg)`. */
447
447
  function error(msg) {
448
448
  throw new Error(msg);
449
449
  }
@@ -958,6 +958,10 @@ function toPixel(...defaults) {
958
958
  }
959
959
  throw new Error(`Expected a string like '123px': ${defaults}`);
960
960
  }
961
+ /** Cast any value to <T>. */
962
+ function unsafeCast(value) {
963
+ return value;
964
+ }
961
965
  /** Return the the boolean value of the first non-null element.
962
966
  * Example:
963
967
  * ```js
@@ -1031,7 +1035,7 @@ function adaptiveThrottle(callback, options) {
1031
1035
  const throttledFn = (...args) => {
1032
1036
  if (waiting) {
1033
1037
  pendingArgs = args;
1034
- // console.log(`adaptiveThrottle() queing request #${waiting}...`, args);
1038
+ // console.log(`adaptiveThrottle() queueing request #${waiting}...`, args);
1035
1039
  waiting += 1;
1036
1040
  }
1037
1041
  else {
@@ -1086,6 +1090,60 @@ function adaptiveThrottle(callback, options) {
1086
1090
  };
1087
1091
  return throttledFn;
1088
1092
  }
1093
+ /**
1094
+ * MurmurHash3 implementation for strings.
1095
+ * @param key The input string to hash.
1096
+ * @param asString Optional convert result to zero-padded string of 8 characters.
1097
+ * @param seed Optional seed value.
1098
+ * @returns A 32-bit hash as a number or string.
1099
+ */
1100
+ function murmurHash3(key, asString = true, seed = 0) {
1101
+ let h1 = seed;
1102
+ const remainder = key.length & 3; // key.length % 4
1103
+ const bytes = key.length - remainder;
1104
+ const c1 = 0xcc9e2d51;
1105
+ const c2 = 0x1b873593;
1106
+ let i = 0;
1107
+ while (i < bytes) {
1108
+ let k1 = (key.charCodeAt(i) & 0xff) |
1109
+ ((key.charCodeAt(++i) & 0xff) << 8) |
1110
+ ((key.charCodeAt(++i) & 0xff) << 16) |
1111
+ ((key.charCodeAt(++i) & 0xff) << 24);
1112
+ ++i;
1113
+ k1 = Math.imul(k1, c1);
1114
+ k1 = (k1 << 15) | (k1 >>> 17);
1115
+ k1 = Math.imul(k1, c2);
1116
+ h1 ^= k1;
1117
+ h1 = (h1 << 13) | (h1 >>> 19);
1118
+ h1 = Math.imul(h1, 5) + 0xe6546b64;
1119
+ }
1120
+ let k1 = 0;
1121
+ switch (remainder) {
1122
+ case 3:
1123
+ k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
1124
+ // fall through
1125
+ case 2:
1126
+ k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
1127
+ // fall through
1128
+ case 1:
1129
+ k1 ^= key.charCodeAt(i) & 0xff;
1130
+ k1 = Math.imul(k1, c1);
1131
+ k1 = (k1 << 15) | (k1 >>> 17);
1132
+ k1 = Math.imul(k1, c2);
1133
+ h1 ^= k1;
1134
+ }
1135
+ h1 ^= key.length;
1136
+ h1 ^= h1 >>> 16;
1137
+ h1 = Math.imul(h1, 0x85ebca6b);
1138
+ h1 ^= h1 >>> 13;
1139
+ h1 = Math.imul(h1, 0xc2b2ae35);
1140
+ h1 ^= h1 >>> 16;
1141
+ if (asString) {
1142
+ // Convert to 8 digit hex string
1143
+ return (h1 >>> 0).toString(16).padStart(8, "0");
1144
+ }
1145
+ return h1 >>> 0; // Convert to unsigned 32-bit integer
1146
+ }
1089
1147
 
1090
1148
  var util = /*#__PURE__*/Object.freeze({
1091
1149
  __proto__: null,
@@ -1116,6 +1174,7 @@ var util = /*#__PURE__*/Object.freeze({
1116
1174
  isFunction: isFunction,
1117
1175
  isMac: isMac,
1118
1176
  isPlainObject: isPlainObject,
1177
+ murmurHash3: murmurHash3,
1119
1178
  noop: noop,
1120
1179
  onEvent: onEvent,
1121
1180
  overrideMethod: overrideMethod,
@@ -1129,13 +1188,14 @@ var util = /*#__PURE__*/Object.freeze({
1129
1188
  toPixel: toPixel,
1130
1189
  toSet: toSet,
1131
1190
  toggleCheckbox: toggleCheckbox,
1132
- type: type
1191
+ type: type,
1192
+ unsafeCast: unsafeCast
1133
1193
  });
1134
1194
 
1135
1195
  /*!
1136
1196
  * Wunderbaum - types
1137
1197
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1138
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1198
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
1139
1199
  */
1140
1200
  /**
1141
1201
  * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
@@ -1203,7 +1263,7 @@ var NavModeEnum;
1203
1263
  /*!
1204
1264
  * Wunderbaum - wb_extension_base
1205
1265
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1206
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1266
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
1207
1267
  */
1208
1268
  class WunderbaumExtension {
1209
1269
  constructor(tree, id, defaults) {
@@ -1262,7 +1322,7 @@ class WunderbaumExtension {
1262
1322
  /*!
1263
1323
  * Wunderbaum - ext-filter
1264
1324
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1265
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1325
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
1266
1326
  */
1267
1327
  const START_MARKER = "\uFFF7";
1268
1328
  const END_MARKER = "\uFFF8";
@@ -1550,6 +1610,10 @@ class FilterExtension extends WunderbaumExtension {
1550
1610
  */
1551
1611
  filterBranches(filter, options) {
1552
1612
  assert(options.matchBranch === undefined, "filterBranches() is deprecated.");
1613
+ this.tree.logDeprecate("filterBranches()", {
1614
+ since: "0.9.0",
1615
+ hint: "Use `filterNodes` instead and set `options.matchBranch: true`",
1616
+ });
1553
1617
  options.matchBranch = true;
1554
1618
  return this._applyFilterNoUpdate(filter, options);
1555
1619
  }
@@ -1614,7 +1678,7 @@ class FilterExtension extends WunderbaumExtension {
1614
1678
  }
1615
1679
  }
1616
1680
  /**
1617
- * @description Marks the matching charecters of `text` either by `mark` or
1681
+ * @description Marks the matching characters of `text` either by `mark` or
1618
1682
  * by exotic*Chars (if `escapeTitles` is `true`) based on `matches`
1619
1683
  * which is an array of matching groups.
1620
1684
  * @param {string} text
@@ -1653,7 +1717,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1653
1717
  /*!
1654
1718
  * Wunderbaum - ext-keynav
1655
1719
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1656
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1720
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
1657
1721
  */
1658
1722
  const QUICKSEARCH_DELAY = 500;
1659
1723
  class KeynavExtension extends WunderbaumExtension {
@@ -2017,7 +2081,7 @@ class KeynavExtension extends WunderbaumExtension {
2017
2081
  /*!
2018
2082
  * Wunderbaum - ext-logger
2019
2083
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2020
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2084
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
2021
2085
  */
2022
2086
  class LoggerExtension extends WunderbaumExtension {
2023
2087
  constructor(tree) {
@@ -2059,7 +2123,7 @@ class LoggerExtension extends WunderbaumExtension {
2059
2123
  /*!
2060
2124
  * Wunderbaum - ext-dnd
2061
2125
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2062
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2126
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
2063
2127
  */
2064
2128
  const nodeMimeType = "application/x-wunderbaum-node";
2065
2129
  class DndExtension extends WunderbaumExtension {
@@ -2295,7 +2359,6 @@ class DndExtension extends WunderbaumExtension {
2295
2359
  */
2296
2360
  onDragEvent(e) {
2297
2361
  var _a;
2298
- // const tree = this.tree;
2299
2362
  const dndOpts = this.treeOpts.dnd;
2300
2363
  const srcNode = Wunderbaum.getNode(e);
2301
2364
  if (!srcNode) {
@@ -2321,7 +2384,7 @@ class DndExtension extends WunderbaumExtension {
2321
2384
  return false;
2322
2385
  }
2323
2386
  const nodeData = srcNode.toDict(true, (n) => {
2324
- // We don't want to re-use the key on drop:
2387
+ // We don't want to reuse the key on drop:
2325
2388
  n._orgKey = n.key;
2326
2389
  delete n.key;
2327
2390
  });
@@ -2383,6 +2446,7 @@ class DndExtension extends WunderbaumExtension {
2383
2446
  };
2384
2447
  if (!targetNode) {
2385
2448
  this._leaveNode();
2449
+ e.preventDefault(); // Don't open file in browser when dropped in empty area
2386
2450
  return;
2387
2451
  }
2388
2452
  if (["drop"].includes(e.type)) {
@@ -2488,19 +2552,20 @@ class DndExtension extends WunderbaumExtension {
2488
2552
  nodeData = nodeData ? JSON.parse(nodeData) : null;
2489
2553
  const srcNode = this.srcNode;
2490
2554
  const lastDropEffect = this.lastDropEffect;
2491
- setTimeout(() => {
2492
- // Decouple this call, because drop actions may prevent the dragend event
2493
- // from being fired on some browsers
2494
- targetNode._callEvent("dnd.drop", {
2495
- event: e,
2496
- region: region,
2497
- suggestedDropMode: region === "over" ? "appendChild" : region,
2498
- suggestedDropEffect: lastDropEffect,
2499
- // suggestedDropEffect: e.dataTransfer?.dropEffect,
2500
- sourceNode: srcNode,
2501
- sourceNodeData: nodeData,
2502
- });
2503
- }, 10);
2555
+ /* Before v0.14.0, we decoupled `_callEvent` like so:
2556
+ Decouple this call, because drop actions may prevent the dragend
2557
+ event from being fired on some browsers.
2558
+ setTimeout(() => {...}, 10);
2559
+ however this made e.dataTransfer.items inaccessible */
2560
+ targetNode._callEvent("dnd.drop", {
2561
+ event: e,
2562
+ region: region,
2563
+ suggestedDropMode: region === "over" ? "appendChild" : region,
2564
+ suggestedDropEffect: lastDropEffect,
2565
+ sourceNode: srcNode,
2566
+ sourceNodeData: nodeData,
2567
+ dataTransfer: e.dataTransfer,
2568
+ });
2504
2569
  }
2505
2570
  return false;
2506
2571
  }
@@ -2509,7 +2574,7 @@ class DndExtension extends WunderbaumExtension {
2509
2574
  /*!
2510
2575
  * Wunderbaum - drag_observer
2511
2576
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2512
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2577
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
2513
2578
  */
2514
2579
  /**
2515
2580
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2658,7 +2723,7 @@ class DragObserver {
2658
2723
  /*!
2659
2724
  * Wunderbaum - common
2660
2725
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2661
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2726
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
2662
2727
  */
2663
2728
  const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
2664
2729
  /**
@@ -2678,12 +2743,18 @@ const TITLE_SPAN_PAD_Y = 7;
2678
2743
  const RENDER_MAX_PREFETCH = 5;
2679
2744
  /** Minimum column width if not set otherwise. */
2680
2745
  const DEFAULT_MIN_COL_WIDTH = 4;
2746
+ /**
2747
+ * A value for `node.type` that by convention may be used to mark a node as directory.
2748
+ * It may be used to sort 'directories' to the top.
2749
+ */
2750
+ const NODE_TYPE_FOLDER = "folder";
2681
2751
  /** Regular expression to detect if a string describes an image URL (in contrast
2682
2752
  * to a class name). Strings are considered image urls if they contain '.' or '/'.
2753
+ * `<` is ignored, because it is probably an html tag.
2683
2754
  */
2684
- const TEST_IMG = new RegExp(/\.|\//);
2685
- // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
2686
- // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
2755
+ const TEST_FILE_PATH = /^(?!.*<).*[/.]/;
2756
+ /** Regular expression to detect if a string describes an HTML element. */
2757
+ const TEST_HTML = /</;
2687
2758
  /**
2688
2759
  * Default node icons for icon libraries
2689
2760
  *
@@ -2691,7 +2762,7 @@ const TEST_IMG = new RegExp(/\.|\//);
2691
2762
  * - 'fontawesome6' {@link https://fontawesome.com/icons}
2692
2763
  *
2693
2764
  */
2694
- const iconMaps = {
2765
+ const defaultIconMaps = {
2695
2766
  bootstrap: {
2696
2767
  error: "bi bi-exclamation-triangle",
2697
2768
  // loading: "bi bi-hourglass-split wb-busy",
@@ -2739,7 +2810,7 @@ const iconMaps = {
2739
2810
  radioChecked: "fa-solid fa-circle",
2740
2811
  radioUnchecked: "fa-regular fa-circle",
2741
2812
  radioUnknown: "fa-regular fa-circle-question",
2742
- folder: "fa-solid fa-folder-closed",
2813
+ folder: "fa-regular fa-folder-closed",
2743
2814
  folderOpen: "fa-regular fa-folder-open",
2744
2815
  folderLazy: "fa-solid fa-folder-plus",
2745
2816
  doc: "fa-regular fa-file",
@@ -2812,12 +2883,20 @@ function makeNodeTitleStartMatcher(s) {
2812
2883
  return reMatch.test(node.title);
2813
2884
  };
2814
2885
  }
2815
- /** Compare two nodes by title (case-insensitive). */
2886
+ /** Compare two nodes by title (case-insensitive).
2887
+ * @deprecated Use `key` option instead of `cmp` in sort methods.
2888
+ */
2816
2889
  function nodeTitleSorter(a, b) {
2817
2890
  const x = a.title.toLowerCase();
2818
2891
  const y = b.title.toLowerCase();
2819
2892
  return x === y ? 0 : x > y ? 1 : -1;
2820
2893
  }
2894
+ // /** Compare nodes by title (case-insensitive). */
2895
+ // export function nodeTitleKeyGetter(
2896
+ // node: WunderbaumNode
2897
+ // ): string | number | Array<any> {
2898
+ // return node.title.toLowerCase();
2899
+ // }
2821
2900
  /**
2822
2901
  * Convert 'flat' to 'nested' format.
2823
2902
  *
@@ -3008,7 +3087,7 @@ function decompressSourceData(source) {
3008
3087
  /*!
3009
3088
  * Wunderbaum - ext-grid
3010
3089
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3011
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3090
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
3012
3091
  */
3013
3092
  class GridExtension extends WunderbaumExtension {
3014
3093
  constructor(tree) {
@@ -3068,7 +3147,7 @@ class GridExtension extends WunderbaumExtension {
3068
3147
  super.init();
3069
3148
  }
3070
3149
  /**
3071
- * Hanldes drag and sragstop events for column resizing.
3150
+ * Handles drag and sragstop events for column resizing.
3072
3151
  */
3073
3152
  handleDrag(e) {
3074
3153
  const custom = e.customData;
@@ -3099,7 +3178,7 @@ class GridExtension extends WunderbaumExtension {
3099
3178
  /*!
3100
3179
  * Wunderbaum - deferred
3101
3180
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3102
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3181
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
3103
3182
  */
3104
3183
  /**
3105
3184
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -3152,7 +3231,7 @@ class Deferred {
3152
3231
  /*!
3153
3232
  * Wunderbaum - wunderbaum_node
3154
3233
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3155
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3234
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
3156
3235
  */
3157
3236
  /** WunderbaumNode properties that can be passed with source data.
3158
3237
  * (Any other source properties will be stored as `node.data.PROP`.)
@@ -3203,7 +3282,7 @@ NODE_DICT_PROPS.delete("unselectable");
3203
3282
  */
3204
3283
  class WunderbaumNode {
3205
3284
  constructor(tree, parent, data) {
3206
- var _a, _b;
3285
+ var _a;
3207
3286
  /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
3208
3287
  * times within a tree (in this case we have 'clone nodes').
3209
3288
  * @see Use {@link setKey} to modify.
@@ -3233,8 +3312,8 @@ class WunderbaumNode {
3233
3312
  assert(!data.children, "'children' not allowed here");
3234
3313
  this.tree = tree;
3235
3314
  this.parent = parent;
3236
- this.key = "" + ((_a = data.key) !== null && _a !== void 0 ? _a : ++WunderbaumNode.sequence);
3237
- this.title = "" + ((_b = data.title) !== null && _b !== void 0 ? _b : "<" + this.key + ">");
3315
+ this.key = tree._calculateKey(data, parent);
3316
+ this.title = "" + ((_a = data.title) !== null && _a !== void 0 ? _a : "<" + this.key + ">");
3238
3317
  this.expanded = !!data.expanded;
3239
3318
  this.lazy = !!data.lazy;
3240
3319
  // We set the following node properties only if a matching data value is
@@ -3251,7 +3330,9 @@ class WunderbaumNode {
3251
3330
  : 0;
3252
3331
  data.colspan != null ? (this.colspan = !!data.colspan) : 0;
3253
3332
  // Selection
3254
- data.checkbox != null ? intToBool(data.checkbox) : 0;
3333
+ data.checkbox != null
3334
+ ? (this.checkbox = intToBool(data.checkbox))
3335
+ : 0;
3255
3336
  data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0;
3256
3337
  data.selected != null ? (this.selected = !!data.selected) : 0;
3257
3338
  data.unselectable != null ? (this.unselectable = !!data.unselectable) : 0;
@@ -3355,8 +3436,14 @@ class WunderbaumNode {
3355
3436
  const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel;
3356
3437
  for (const child of nodeData) {
3357
3438
  const subChildren = child.children;
3439
+ // Remove children property from source data because it should not be
3440
+ // passed to the constructor of WunderbaumNode:
3358
3441
  delete child.children;
3359
3442
  const n = new WunderbaumNode(tree, this, child);
3443
+ // Set `children` property again, so it can be used in `reload()`
3444
+ if (subChildren != null) {
3445
+ child.children = subChildren;
3446
+ }
3360
3447
  if (forceExpand && !n.isUnloaded()) {
3361
3448
  n.expanded = true;
3362
3449
  }
@@ -3772,15 +3859,12 @@ class WunderbaumNode {
3772
3859
  }
3773
3860
  return l;
3774
3861
  }
3775
- /** Return a string representing the hierachical node path, e.g. "a/b/c".
3862
+ /** Return a string representing the hierarchical node path, e.g. "a/b/c".
3776
3863
  * @param includeSelf
3777
3864
  * @param part property name or callback
3778
3865
  * @param separator
3779
3866
  */
3780
3867
  getPath(includeSelf = true, part = "title", separator = "/") {
3781
- // includeSelf = includeSelf !== false;
3782
- // part = part || "title";
3783
- // separator = separator || "/";
3784
3868
  let val;
3785
3869
  const path = [];
3786
3870
  const isFunc = typeof part === "function";
@@ -3795,7 +3879,7 @@ class WunderbaumNode {
3795
3879
  }, includeSelf);
3796
3880
  return path.join(separator);
3797
3881
  }
3798
- /** Return the preceeding node (under the same parent) or null. */
3882
+ /** Return the preceding node (under the same parent) or null. */
3799
3883
  getPrevSibling() {
3800
3884
  const ac = this.parent.children;
3801
3885
  const idx = ac.indexOf(this);
@@ -3824,7 +3908,7 @@ class WunderbaumNode {
3824
3908
  hasClass(className) {
3825
3909
  return this.classes ? this.classes.has(className) : false;
3826
3910
  }
3827
- /** Return true if node ist the currently focused node. @since 0.9.0 */
3911
+ /** Return true if node is the currently focused node. @since 0.9.0 */
3828
3912
  hasFocus() {
3829
3913
  return this.tree.focusNode === this;
3830
3914
  }
@@ -3879,7 +3963,7 @@ class WunderbaumNode {
3879
3963
  * an expand operation is currently possible.
3880
3964
  */
3881
3965
  isExpandable(andCollapsed = false) {
3882
- // `false` is never expandable (unoffical)
3966
+ // `false` is never expandable (unofficial)
3883
3967
  if ((andCollapsed && this.expanded) || this.children === false) {
3884
3968
  return false;
3885
3969
  }
@@ -3942,11 +4026,11 @@ class WunderbaumNode {
3942
4026
  isPartsel() {
3943
4027
  return !this.selected && !!this._partsel;
3944
4028
  }
3945
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4029
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3946
4030
  isRadio() {
3947
4031
  return !!this.parent.radiogroup || this.getOption("checkbox") === "radio";
3948
4032
  }
3949
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4033
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3950
4034
  isRendered() {
3951
4035
  return !!this._rowElem;
3952
4036
  }
@@ -4701,9 +4785,9 @@ class WunderbaumNode {
4701
4785
  const typeInfo = this.type ? tree.types[this.type] : null;
4702
4786
  const rowDiv = this._rowElem;
4703
4787
  // Row markup already exists
4704
- const nodeElem = rowDiv.querySelector("span.wb-node");
4705
- const expanderSpan = nodeElem.querySelector("i.wb-expander");
4706
- const checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
4788
+ const nodeSpan = rowDiv.querySelector("span.wb-node");
4789
+ const expanderElem = nodeSpan.querySelector("i.wb-expander");
4790
+ const checkboxElem = nodeSpan.querySelector("i.wb-checkbox");
4707
4791
  const rowClasses = ["wb-row"];
4708
4792
  this.expanded ? rowClasses.push("wb-expanded") : 0;
4709
4793
  this.lazy ? rowClasses.push("wb-lazy") : 0;
@@ -4728,7 +4812,7 @@ class WunderbaumNode {
4728
4812
  if (typeInfo && typeInfo.classes) {
4729
4813
  rowDiv.classList.add(...typeInfo.classes);
4730
4814
  }
4731
- if (expanderSpan) {
4815
+ if (expanderElem) {
4732
4816
  let image = null;
4733
4817
  if (this._isLoading) {
4734
4818
  image = iconMap.loading;
@@ -4745,16 +4829,20 @@ class WunderbaumNode {
4745
4829
  image = iconMap.expanderLazy;
4746
4830
  }
4747
4831
  if (image == null) {
4748
- expanderSpan.classList.add("wb-indent");
4832
+ expanderElem.className = "wb-expander";
4833
+ expanderElem.classList.add("wb-indent");
4834
+ }
4835
+ else if (TEST_HTML.test(image)) {
4836
+ expanderElem.replaceWith(elemFromHtml(image));
4749
4837
  }
4750
- else if (TEST_IMG.test(image)) {
4751
- expanderSpan.style.backgroundImage = `url('${image}')`;
4838
+ else if (TEST_FILE_PATH.test(image)) {
4839
+ expanderElem.style.backgroundImage = `url('${image}')`;
4752
4840
  }
4753
4841
  else {
4754
- expanderSpan.className = "wb-expander " + image;
4842
+ expanderElem.className = "wb-expander " + image;
4755
4843
  }
4756
4844
  }
4757
- if (checkboxSpan) {
4845
+ if (checkboxElem) {
4758
4846
  let cbclass = "wb-checkbox ";
4759
4847
  if (this.isRadio()) {
4760
4848
  cbclass += "wb-radio ";
@@ -4778,7 +4866,7 @@ class WunderbaumNode {
4778
4866
  cbclass += iconMap.checkUnchecked;
4779
4867
  }
4780
4868
  }
4781
- checkboxSpan.className = cbclass;
4869
+ checkboxElem.className = cbclass;
4782
4870
  }
4783
4871
  // Fix active cell in cell-nav mode
4784
4872
  if (!opts.isNew) {
@@ -4788,9 +4876,9 @@ class WunderbaumNode {
4788
4876
  colSpan.classList.remove("wb-error", "wb-invalid");
4789
4877
  }
4790
4878
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4791
- const iconSpan = nodeElem.querySelector("i.wb-icon");
4879
+ const iconSpan = nodeSpan.querySelector("i.wb-icon");
4792
4880
  if (iconSpan) {
4793
- this._createIcon(nodeElem, iconSpan, !expanderSpan);
4881
+ this._createIcon(nodeSpan, iconSpan, !expanderElem);
4794
4882
  }
4795
4883
  }
4796
4884
  // Adjust column width
@@ -5095,6 +5183,32 @@ class WunderbaumNode {
5095
5183
  setKey(key, refKey) {
5096
5184
  throw new Error("Not yet implemented");
5097
5185
  }
5186
+ // /**
5187
+ // * Calculate a *stable*, unique key for this node from its refKey (or title).
5188
+ // * We also add information from the parent, because a refKey may occur multiple
5189
+ // * times in a tree.
5190
+ // */
5191
+ // calcUniqueKey() {
5192
+ // // Assuming that the parent's key was calculated the same way, we implicitly
5193
+ // // involve the whole refKey-path:
5194
+ // const s = this.key + (this.refKey || this.title);
5195
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5196
+ // // https://security.stackexchange.com/q/209882/207588
5197
+ // const h1 = util.murmurHash3(s, true);
5198
+ // return "id_" + h1 + util.murmurHash3(h1 + s, true);
5199
+ // // const l = [];
5200
+ // // // eslint-disable-next-line @typescript-eslint/no-this-alias
5201
+ // // let node: WunderbaumNode = this;
5202
+ // // while (node.parent) {
5203
+ // // l.unshift(node.refKey || node.key);
5204
+ // // node = node.parent;
5205
+ // // }
5206
+ // // const path = l.join("/");
5207
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5208
+ // // https://security.stackexchange.com/q/209882/207588
5209
+ // // const h1 = util.murmurHash3(path, true);
5210
+ // // return "id_" + h1 + util.murmurHash3(h1 + path, true);
5211
+ // }
5098
5212
  /**
5099
5213
  * Trigger a repaint, typically after a status or data change.
5100
5214
  *
@@ -5126,6 +5240,23 @@ class WunderbaumNode {
5126
5240
  });
5127
5241
  return nodeList;
5128
5242
  }
5243
+ /**
5244
+ * Return an array of refKey values.
5245
+ *
5246
+ * RefKeys are unique identifiers for a node data, and are used to identify
5247
+ * clones.
5248
+ * If more than one node has the same refKey, it is only returned once.
5249
+ * @param selected if true, only return refKeys of selected nodes.
5250
+ */
5251
+ getRefKeys(selected = false) {
5252
+ const refKeys = new Set();
5253
+ this.visit((node) => {
5254
+ if (node.refKey != null && (!selected || node.selected)) {
5255
+ refKeys.add(node.refKey);
5256
+ }
5257
+ });
5258
+ return Array.from(refKeys);
5259
+ }
5129
5260
  /** Toggle the check/uncheck state. */
5130
5261
  toggleSelected(options) {
5131
5262
  let flag = this.isSelected();
@@ -5305,9 +5436,11 @@ class WunderbaumNode {
5305
5436
  if (selectMode === "hier") {
5306
5437
  this.fixSelection3AfterClick();
5307
5438
  }
5308
- else if (selectMode === "single") {
5439
+ else if (selectMode === "single" && flag) {
5309
5440
  tree.visit((n) => {
5310
- n.selected = false;
5441
+ if (n !== this) {
5442
+ n.selected = false;
5443
+ }
5311
5444
  });
5312
5445
  }
5313
5446
  }
@@ -5409,30 +5542,16 @@ class WunderbaumNode {
5409
5542
  this.tooltip = tooltip;
5410
5543
  this.update();
5411
5544
  }
5412
- _sortChildren(cmp, deep) {
5413
- const cl = this.children;
5414
- if (!cl) {
5415
- return;
5416
- }
5417
- cl.sort(cmp);
5418
- if (deep) {
5419
- for (let i = 0, l = cl.length; i < l; i++) {
5420
- if (cl[i].children) {
5421
- cl[i]._sortChildren(cmp, deep);
5422
- }
5423
- }
5424
- }
5425
- }
5426
5545
  /**
5427
5546
  * Sort child list by title or custom criteria.
5428
5547
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
5429
5548
  * (defaults to sorting by title).
5430
5549
  * @param {boolean} deep pass true to sort all descendant nodes recursively
5550
+ * @deprecated use {@link sort}
5431
5551
  */
5432
5552
  sortChildren(cmp = nodeTitleSorter, deep = false) {
5433
- this._sortChildren(cmp || nodeTitleSorter, deep);
5434
- this.tree.update(ChangeType.structure);
5435
- // this.triggerModify("sort"); // TODO
5553
+ this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" });
5554
+ return this.sort({ cmp: cmp ? cmp : undefined, deep: deep });
5436
5555
  }
5437
5556
  /**
5438
5557
  * Renumber nodes `_nativeIndex`. This is useful to allow to restore the
@@ -5454,74 +5573,142 @@ class WunderbaumNode {
5454
5573
  /**
5455
5574
  * Convenience method to implement column sorting.
5456
5575
  * @since 0.11.0
5576
+ * @deprecated use {@link sort}
5457
5577
  */
5458
5578
  sortByProperty(options) {
5459
- var _a, _b, _c;
5460
- const { caseInsensitive = true, deep = true, nativeOrderPropName = "_nativeIndex", updateColInfo = false, } = options;
5461
- let order;
5462
- let colDef;
5579
+ this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" });
5580
+ return this.sort(options);
5581
+ }
5582
+ /**
5583
+ * Implement column sorting.
5584
+ * @since 0.14.0
5585
+ */
5586
+ sort(options) {
5587
+ const tree = this.tree;
5588
+ let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined,
5589
+ // Support click on column sort header:
5590
+ updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options;
5591
+ propName !== null && propName !== void 0 ? propName : (propName = colId);
5592
+ if (propName === "*") {
5593
+ propName = "title";
5594
+ }
5595
+ const isFolder = tree.options.sortFoldersFirst === true
5596
+ ? (node) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER
5597
+ : tree.options.sortFoldersFirst;
5463
5598
  if (updateColInfo) {
5464
- colDef = this.tree["_columnsById"][options.colId];
5599
+ const colDef = this.tree["_columnsById"][options.colId];
5465
5600
  assert(colDef, `Invalid colId specified: ${options.colId}`);
5466
- order =
5467
- (_a = options.order) !== null && _a !== void 0 ? _a : rotate(colDef.sortOrder, ["asc", "desc", undefined]);
5601
+ order !== null && order !== void 0 ? order : (order = rotate(colDef.sortOrder, ["asc", "desc", undefined]));
5468
5602
  for (const col of this.tree.columns) {
5469
5603
  col.sortOrder = col === colDef ? order : undefined;
5470
5604
  }
5605
+ if (order === undefined) {
5606
+ propName = nativeOrderPropName;
5607
+ order = "asc";
5608
+ }
5471
5609
  this.tree.update(ChangeType.colStructure);
5472
5610
  }
5473
5611
  else {
5474
- order = (_b = options.order) !== null && _b !== void 0 ? _b : "asc";
5612
+ propName !== null && propName !== void 0 ? propName : (propName = "title");
5613
+ order !== null && order !== void 0 ? order : (order = "asc");
5614
+ }
5615
+ this.logDebug(`sort(), propName=${propName}, ${order}`, options);
5616
+ assert(propName || cmp || key, "No `propName` or `key` specified");
5617
+ // Define a key callback from the parameters we have
5618
+ if (key == null && cmp == null) {
5619
+ key = (node) => {
5620
+ let val;
5621
+ if (NODE_DICT_PROPS.has(propName)) {
5622
+ val = node[propName];
5623
+ }
5624
+ else {
5625
+ val = node.data[propName];
5626
+ }
5627
+ if (caseInsensitive && typeof val === "string") {
5628
+ val = val.toLowerCase();
5629
+ }
5630
+ return val;
5631
+ };
5475
5632
  }
5476
- let propName = (_c = options.propName) !== null && _c !== void 0 ? _c : (options.colId || "");
5477
- if (propName === "*") {
5478
- propName = "title";
5633
+ // Define a compare callback that uses the key callback
5634
+ if (cmp) {
5635
+ assert(!key, "`key` and `cmp` are mutually exclusive");
5636
+ tree.logDeprecate("SortOptions.cmp", {
5637
+ since: "0.14.0",
5638
+ hint: "use the `key` callback instead",
5639
+ });
5479
5640
  }
5480
- if (order == null) {
5481
- propName = nativeOrderPropName;
5482
- order = "asc";
5641
+ else {
5642
+ if (options.propName || options.caseInsensitive) {
5643
+ tree.logWarn("sort(): ignoring propName, caseInsensitive");
5644
+ }
5645
+ cmp = (a, b) => {
5646
+ if (isFolder) {
5647
+ const isFolderA = isFolder(a);
5648
+ if (isFolderA !== isFolder(b)) {
5649
+ return isFolderA ? -1 : 1;
5650
+ }
5651
+ }
5652
+ let x = key(a);
5653
+ let y = key(b);
5654
+ // Assure we have reasonable comparisons with null values:
5655
+ if (x == null) {
5656
+ x = typeof y === "string" ? "" : 0;
5657
+ }
5658
+ else if (typeof x === "boolean") {
5659
+ x = x ? 1 : 0;
5660
+ }
5661
+ if (y == null) {
5662
+ y = typeof x === "string" ? "" : 0;
5663
+ }
5664
+ else if (typeof y === "boolean") {
5665
+ y = y ? 1 : 0;
5666
+ }
5667
+ if (order === "desc") {
5668
+ return x === y ? 0 : x > y ? -1 : 1;
5669
+ }
5670
+ return x === y ? 0 : x > y ? 1 : -1;
5671
+ };
5483
5672
  }
5484
- this.logDebug(`sortByProperty(), propName=${propName}, ${order}`, options);
5485
- assert(propName, "No property name specified");
5486
- const cmp = (a, b) => {
5487
- let av, bv;
5488
- if (NODE_DICT_PROPS.has(propName)) {
5489
- av = a[propName];
5490
- bv = b[propName];
5491
- }
5492
- else {
5493
- av = a.data[propName];
5494
- bv = b.data[propName];
5495
- }
5496
- if (av == null && bv == null) {
5497
- return 0;
5498
- }
5499
- if (av == null) {
5500
- av = typeof bv === "string" ? "" : 0;
5501
- }
5502
- else if (typeof av === "boolean") {
5503
- av = av ? 1 : 0;
5504
- }
5505
- if (bv == null) {
5506
- bv = typeof av === "string" ? "" : 0;
5507
- }
5508
- else if (typeof bv === "boolean") {
5509
- bv = bv ? 1 : 0;
5673
+ function _sortChildren(cl) {
5674
+ if (!cl) {
5675
+ return;
5510
5676
  }
5511
- if (caseInsensitive) {
5512
- if (typeof av === "string") {
5513
- av = av.toLowerCase();
5514
- }
5515
- if (typeof bv === "string") {
5516
- bv = bv.toLowerCase();
5677
+ cl.sort(cmp);
5678
+ if (deep) {
5679
+ for (let i = 0, l = cl.length; i < l; i++) {
5680
+ if (cl[i].children) {
5681
+ _sortChildren(cl[i].children);
5682
+ }
5517
5683
  }
5518
5684
  }
5519
- if (order === "desc") {
5520
- return av === bv ? 0 : av > bv ? -1 : 1;
5685
+ }
5686
+ if (this.children) {
5687
+ _sortChildren(this.children);
5688
+ }
5689
+ this.tree.update(ChangeType.structure);
5690
+ // this.triggerModify("sort"); // TODO
5691
+ }
5692
+ /**
5693
+ * Re-apply current sorting if any (use after lazy load).
5694
+ * Example:
5695
+ * ```js
5696
+ * load: function (e) {
5697
+ * // Whe loading a lazy branch, apply current sort order if any
5698
+ * e.node.resort();
5699
+ * },
5700
+ * ```
5701
+ * @since 0.14.0
5702
+ */
5703
+ resort(options = {}) {
5704
+ for (const colDef of this.tree.columns) {
5705
+ if (colDef.sortOrder) {
5706
+ options.colId = colDef.id;
5707
+ options.order = colDef.sortOrder;
5708
+ this.sort(options);
5709
+ break;
5521
5710
  }
5522
- return av === bv ? 0 : av > bv ? 1 : -1;
5523
- };
5524
- return this.sortChildren(cmp, deep);
5711
+ }
5525
5712
  }
5526
5713
  /**
5527
5714
  * Trigger `modifyChild` event on a parent to signal that a child was modified.
@@ -5558,7 +5745,8 @@ class WunderbaumNode {
5558
5745
  * @param {function} callback the callback function.
5559
5746
  * Return false to stop iteration, return "skip" to skip this node and
5560
5747
  * its children only.
5561
- * @see {@link IterableIterator<WunderbaumNode>}, {@link Wunderbaum.visit}.
5748
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
5749
+ * @see {@link Wunderbaum.visit}.
5562
5750
  */
5563
5751
  visit(callback, includeSelf = false) {
5564
5752
  let res = true;
@@ -5631,7 +5819,7 @@ WunderbaumNode.sequence = 0;
5631
5819
  /*!
5632
5820
  * Wunderbaum - ext-edit
5633
5821
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
5634
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
5822
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
5635
5823
  */
5636
5824
  // const START_MARKER = "\uFFF7";
5637
5825
  class EditExtension extends WunderbaumExtension {
@@ -5866,7 +6054,7 @@ class EditExtension extends WunderbaumExtension {
5866
6054
  newValue = newValue.trim();
5867
6055
  }
5868
6056
  if (!node) {
5869
- this.tree.logDebug("stopEditTitle: not in edit mode.");
6057
+ // this.tree.logDebug("stopEditTitle: not in edit mode.");
5870
6058
  return;
5871
6059
  }
5872
6060
  node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue);
@@ -5966,8 +6154,8 @@ class EditExtension extends WunderbaumExtension {
5966
6154
  * https://github.com/mar10/wunderbaum
5967
6155
  *
5968
6156
  * Released under the MIT license.
5969
- * @version v0.13.0
5970
- * @date Sat, 08 Mar 2025 14:16:31 GMT
6157
+ * @version v0.14.1
6158
+ * @date Sun, 22 Mar 2026 05:52:05 GMT
5971
6159
  */
5972
6160
  // import "./wunderbaum.scss";
5973
6161
  class WbSystemRoot extends WunderbaumNode {
@@ -6016,17 +6204,17 @@ class Wunderbaum {
6016
6204
  this._disableUpdateIgnoreCount = 0;
6017
6205
  this._activeNode = null;
6018
6206
  this._focusNode = null;
6207
+ this._initialSource = null;
6019
6208
  /** Shared properties, referenced by `node.type`. */
6020
6209
  this.types = {};
6021
6210
  /** List of column definitions. */
6022
- this.columns = []; // any[] = [];
6211
+ this.columns = [];
6023
6212
  this._columnsById = {};
6024
6213
  // Modification Status
6025
6214
  this.pendingChangeTypes = new Set();
6026
6215
  /** Expose some useful methods of the util.ts module as `tree._util`. */
6027
6216
  this._util = util;
6028
6217
  // --- SELECT ---
6029
- // /** @internal */
6030
6218
  // public selectRangeAnchor: WunderbaumNode | null = null;
6031
6219
  // --- BREADCRUMB ---
6032
6220
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
@@ -6045,37 +6233,44 @@ class Wunderbaum {
6045
6233
  this.lastQuicksearchTerm = "";
6046
6234
  // --- EDIT ---
6047
6235
  this.lastClickTime = 0;
6048
- const opts = (this.options = extend({
6049
- id: null,
6050
- source: null, // URL for GET/PUT, Ajax options, or callback
6051
- element: null, // <div class="wunderbaum">
6236
+ // Set default options and merge with user options
6237
+ const initOptions = Object.assign({
6238
+ id: undefined,
6239
+ source: [], // URL for GET/PUT, Ajax options, or callback
6240
+ element: unsafeCast(null),
6052
6241
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
6053
6242
  header: null, // Show/hide header (pass bool or string)
6054
- // headerHeightPx: ROW_HEIGHT,
6055
6243
  rowHeightPx: DEFAULT_ROW_HEIGHT,
6056
6244
  iconMap: "bootstrap",
6057
- columns: null,
6058
- types: null,
6059
- // escapeTitles: true,
6245
+ columns: [], //util.unsafeCast<ColumnDefinitionList>(null),
6246
+ types: {},
6060
6247
  enabled: true,
6061
6248
  fixedCol: false,
6062
6249
  showSpinner: false,
6063
6250
  checkbox: false,
6064
6251
  minExpandLevel: 0,
6065
6252
  emptyChildListExpandable: false,
6066
- // updateThrottleWait: 200,
6067
6253
  skeleton: false,
6254
+ autoCollapse: false,
6255
+ adjustHeight: true,
6068
6256
  connectTopBreadcrumb: null,
6257
+ columnsFilterable: false,
6258
+ columnsMenu: false,
6259
+ columnsResizable: false,
6260
+ columnsSortable: false,
6069
6261
  selectMode: "multi", // SelectModeType
6262
+ scrollIntoViewOnExpandClick: true,
6263
+ // --- Extensions (actually set by exensions on init)
6264
+ dnd: unsafeCast(null),
6265
+ edit: unsafeCast(null),
6266
+ filter: unsafeCast(null),
6070
6267
  // --- KeyNav ---
6071
- navigationModeOption: null, // NavModeEnum,
6268
+ navigationModeOption: unsafeCast(null),
6072
6269
  quicksearch: true,
6073
6270
  // --- Events ---
6074
- iconBadge: null,
6075
- change: null,
6076
- // enhanceTitle: null,
6077
- error: null,
6078
- receive: null,
6271
+ // iconBadge: null,
6272
+ // change: null,
6273
+ // ...
6079
6274
  // --- Strings ---
6080
6275
  strings: {
6081
6276
  loadError: "Error",
@@ -6086,7 +6281,9 @@ class Wunderbaum {
6086
6281
  noMatch: "No results",
6087
6282
  matchIndex: "${match} of ${matches}",
6088
6283
  },
6089
- }, options));
6284
+ }, options);
6285
+ const opts = initOptions;
6286
+ this.options = opts;
6090
6287
  const readyDeferred = new Deferred();
6091
6288
  this.ready = readyDeferred.promise();
6092
6289
  let readyOk = false;
@@ -6113,7 +6310,8 @@ class Wunderbaum {
6113
6310
  this._callEvent("init", { error: err });
6114
6311
  }
6115
6312
  });
6116
- this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
6313
+ this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence;
6314
+ delete initOptions.id;
6117
6315
  this.root = new WbSystemRoot(this);
6118
6316
  this._registerExtension(new KeynavExtension(this));
6119
6317
  this._registerExtension(new EditExtension(this));
@@ -6123,19 +6321,20 @@ class Wunderbaum {
6123
6321
  this._registerExtension(new LoggerExtension(this));
6124
6322
  this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {});
6125
6323
  // --- Evaluate options
6126
- this.columns = opts.columns;
6127
- delete opts.columns;
6324
+ this.columns = initOptions.columns || [];
6325
+ delete initOptions.columns;
6128
6326
  if (!this.columns || !this.columns.length) {
6129
6327
  const title = typeof opts.header === "string" ? opts.header : this.id;
6130
6328
  this.columns = [{ id: "*", title: title, width: "*" }];
6131
6329
  }
6132
- if (opts.types) {
6133
- this.setTypes(opts.types, true);
6330
+ if (initOptions.types) {
6331
+ this.setTypes(initOptions.types, true);
6134
6332
  }
6135
- delete opts.types;
6333
+ delete initOptions.types;
6136
6334
  // --- Create Markup
6137
- this.element = elemFromSelector(opts.element);
6138
- assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
6335
+ this.element = elemFromSelector(initOptions.element);
6336
+ assert(!!this.element, `Invalid 'element' option: ${initOptions.element}`);
6337
+ delete initOptions.element;
6139
6338
  this.element.classList.add("wunderbaum");
6140
6339
  if (!this.element.getAttribute("tabindex")) {
6141
6340
  this.element.tabIndex = 0;
@@ -6211,11 +6410,11 @@ class Wunderbaum {
6211
6410
  }
6212
6411
  });
6213
6412
  // --- Load initial data
6214
- if (opts.source) {
6413
+ if (initOptions.source) {
6215
6414
  if (opts.showSpinner) {
6216
6415
  this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
6217
6416
  }
6218
- this.load(opts.source)
6417
+ this.load(initOptions.source)
6219
6418
  .then(() => {
6220
6419
  // The source may have defined columns, so we may adjust the nav mode
6221
6420
  if (opts.navigationModeOption == null) {
@@ -6248,15 +6447,18 @@ class Wunderbaum {
6248
6447
  // has a wrong value at start???
6249
6448
  this.update(ChangeType.any);
6250
6449
  // --- Bind listeners
6251
- this.element.addEventListener("scroll", (e) => {
6252
- // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6253
- this.update(ChangeType.scroll);
6254
- });
6450
+ this._registerEventHandlers();
6255
6451
  this.resizeObserver = new ResizeObserver((entries) => {
6256
6452
  // this.log("ResizeObserver: Size changed", entries);
6257
6453
  this.update(ChangeType.resize);
6258
6454
  });
6259
6455
  this.resizeObserver.observe(this.element);
6456
+ }
6457
+ _registerEventHandlers() {
6458
+ this.element.addEventListener("scroll", (e) => {
6459
+ // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6460
+ this.update(ChangeType.scroll);
6461
+ });
6260
6462
  onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
6261
6463
  var _a, _b;
6262
6464
  const info = Wunderbaum.getEventInfo(e);
@@ -6272,9 +6474,6 @@ class Wunderbaum {
6272
6474
  const node = info.node;
6273
6475
  const mouseEvent = e;
6274
6476
  // this.log("click", info);
6275
- // if (this._selectRange(info) === false) {
6276
- // return;
6277
- // }
6278
6477
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
6279
6478
  this.lastClickTime = Date.now();
6280
6479
  return false;
@@ -6293,20 +6492,22 @@ class Wunderbaum {
6293
6492
  (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) {
6294
6493
  node.startEditTitle();
6295
6494
  }
6296
- if (info.colIdx >= 0) {
6297
- node.setActive(true, { colIdx: info.colIdx, event: e });
6298
- }
6299
- else {
6300
- node.setActive(true, { event: e });
6301
- }
6302
6495
  if (info.region === NodeRegion.expander) {
6303
6496
  node.setExpanded(!node.isExpanded(), {
6304
- scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
6497
+ scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
6305
6498
  });
6306
6499
  }
6307
6500
  else if (info.region === NodeRegion.checkbox) {
6308
6501
  node.toggleSelected();
6309
6502
  }
6503
+ else {
6504
+ if (info.colIdx >= 0) {
6505
+ node.setActive(true, { colIdx: info.colIdx, event: e });
6506
+ }
6507
+ else {
6508
+ node.setActive(true, { event: e });
6509
+ }
6510
+ }
6310
6511
  }
6311
6512
  this.lastClickTime = Date.now();
6312
6513
  });
@@ -6342,7 +6543,7 @@ class Wunderbaum {
6342
6543
  const targetNode = Wunderbaum.getNode(e);
6343
6544
  this._callEvent("focus", { flag: flag, event: e });
6344
6545
  if (flag && this.isRowNav() && !this.isEditingTitle()) {
6345
- if (opts.navigationModeOption === NavModeEnum.row) {
6546
+ if (this.options.navigationModeOption === NavModeEnum.row) {
6346
6547
  targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive();
6347
6548
  }
6348
6549
  else {
@@ -6409,11 +6610,12 @@ class Wunderbaum {
6409
6610
  }
6410
6611
  /**
6411
6612
  * Return the icon-function -> icon-definition mapping.
6613
+ * @deprecated Use {@link Wunderbaum.iconMaps}
6412
6614
  */
6413
6615
  get iconMap() {
6414
6616
  const map = this.options.iconMap;
6415
6617
  if (typeof map === "string") {
6416
- return iconMaps[map];
6618
+ return defaultIconMaps[map];
6417
6619
  }
6418
6620
  return map;
6419
6621
  }
@@ -6466,7 +6668,38 @@ class Wunderbaum {
6466
6668
  ext.init();
6467
6669
  }
6468
6670
  }
6469
- /** Add node to tree's bookkeeping data structures. */
6671
+ /**
6672
+ * Calculate a *stable*, unique key for a node from its refKey (or title).
6673
+ * We also add information from the parent, because a refKey may occur multiple
6674
+ * times in a tree (but not as child of the same parent).
6675
+ * @internal
6676
+ */
6677
+ _calculateKey(data, parent) {
6678
+ if (data.key) {
6679
+ // Always use an explicitly passed key
6680
+ return data.key;
6681
+ }
6682
+ // Auto-keys are optional, use a monotonic counter by default:
6683
+ if (!this.options.autoKeys) {
6684
+ return "" + ++WunderbaumNode.sequence;
6685
+ }
6686
+ // Add the parent's key to the hash. Assuming this was generated by the
6687
+ // same algorithm, this should incorporate the whole path:
6688
+ const s = (parent ? parent.key : "") + (data.refKey || data.title);
6689
+ // 32-bit has a high probability of collisions, so we pump up to 64-bit
6690
+ // https://security.stackexchange.com/q/209882/207588
6691
+ const h1 = murmurHash3(s, true);
6692
+ let key = "id_" + h1 + murmurHash3(h1 + s, true);
6693
+ // Check for collisions
6694
+ // (Most likely if the same title occurs multiple in the same parent).
6695
+ const existingNode = this.keyMap.get(key);
6696
+ if (existingNode) {
6697
+ key += "." + ++Wunderbaum.sequence;
6698
+ this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data);
6699
+ }
6700
+ return key;
6701
+ }
6702
+ /** Add node to tree's bookkeeping data structures. @internal */
6470
6703
  _registerNode(node) {
6471
6704
  const key = node.key;
6472
6705
  assert(key != null, `Missing key: '${node}'.`);
@@ -6483,7 +6716,7 @@ class Wunderbaum {
6483
6716
  }
6484
6717
  }
6485
6718
  }
6486
- /** Remove node from tree's bookkeeping data structures. */
6719
+ /** Remove node from tree's bookkeeping data structures. @internal */
6487
6720
  _unregisterNode(node) {
6488
6721
  // Remove refKey reference from map (if any)
6489
6722
  const rk = node.refKey;
@@ -6602,6 +6835,16 @@ class Wunderbaum {
6602
6835
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6603
6836
  return this._getNodeByRowIdx(bottomIdx);
6604
6837
  }
6838
+ /** Return preceding visible node in the viewport. */
6839
+ _getPrevNodeInView(node, ofs = 1) {
6840
+ this.visitRows((n) => {
6841
+ node = n;
6842
+ if (ofs-- <= 0) {
6843
+ return false;
6844
+ }
6845
+ }, { reverse: true, start: node || this.getActiveNode() });
6846
+ return node;
6847
+ }
6605
6848
  /** Return following visible node in the viewport. */
6606
6849
  _getNextNodeInView(node, options) {
6607
6850
  let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
@@ -6853,22 +7096,39 @@ class Wunderbaum {
6853
7096
  /** Run code, but defer rendering of viewport until done.
6854
7097
  *
6855
7098
  * ```js
6856
- * tree.runWithDeferredUpdate(() => {
6857
- * return someFuncThatWouldUpdateManyNodes();
7099
+ * const res = tree.runWithDeferredUpdate(() => {
7100
+ * return someFunctionThatWouldUpdateManyNodes();
6858
7101
  * });
6859
7102
  * ```
6860
7103
  */
6861
- runWithDeferredUpdate(func, hint = null) {
7104
+ runWithDeferredUpdate(func) {
6862
7105
  try {
6863
7106
  this.enableUpdate(false);
6864
7107
  const res = func();
6865
- assert(!(res instanceof Promise), `Promise return not allowed: ${res}`);
7108
+ assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`);
6866
7109
  return res;
6867
7110
  }
6868
7111
  finally {
6869
7112
  this.enableUpdate(true);
6870
7113
  }
6871
7114
  }
7115
+ /** Run code, but defer rendering of viewport until done.
7116
+ *
7117
+ * ```js
7118
+ * const res = await tree.runWithDeferredUpdate(async () => {
7119
+ * return someAsyncFunctionThatWouldUpdateManyNodes();
7120
+ * });
7121
+ * ```
7122
+ */
7123
+ async runWithDeferredUpdateAsync(func) {
7124
+ try {
7125
+ this.enableUpdate(false);
7126
+ return await func();
7127
+ }
7128
+ finally {
7129
+ this.enableUpdate(true);
7130
+ }
7131
+ }
6872
7132
  /** Recursively expand all expandable nodes (triggers lazy load if needed). */
6873
7133
  async expandAll(flag = true, options) {
6874
7134
  await this.root.expandAll(flag, options);
@@ -6888,6 +7148,17 @@ class Wunderbaum {
6888
7148
  getSelectedNodes(stopOnParents = false) {
6889
7149
  return this.root.getSelectedNodes(stopOnParents);
6890
7150
  }
7151
+ /**
7152
+ * Return an array of refKey values.
7153
+ *
7154
+ * RefKeys are unique identifiers for a node data, and are used to identify
7155
+ * clones.
7156
+ * If more than one node has the same refKey, it is only returned once.
7157
+ * @param selected if true, only return refKeys of selected nodes.
7158
+ */
7159
+ getRefKeys(selected = false) {
7160
+ return this.root.getRefKeys(selected);
7161
+ }
6891
7162
  /*
6892
7163
  * Return an array of selected nodes.
6893
7164
  */
@@ -7169,6 +7440,18 @@ class Wunderbaum {
7169
7440
  format(name_cb, connectors) {
7170
7441
  return this.root.format(name_cb, connectors);
7171
7442
  }
7443
+ /**
7444
+ * Always returns null (so a tree instance behaves as `tree.root`).
7445
+ */
7446
+ get parent() {
7447
+ return null;
7448
+ }
7449
+ /**
7450
+ * Return a list of top-level nodes.
7451
+ */
7452
+ get children() {
7453
+ return this.root.children || [];
7454
+ }
7172
7455
  /**
7173
7456
  * Return the active cell (`span.wb-col`) of the currently active node or null.
7174
7457
  */
@@ -7287,7 +7570,7 @@ class Wunderbaum {
7287
7570
  }
7288
7571
  /** Return true if any node title or grid cell is currently beeing edited.
7289
7572
  *
7290
- * See also {@link Wunderbaum.isEditingTitle}.
7573
+ * See also {@link isEditingTitle}.
7291
7574
  */
7292
7575
  isEditing() {
7293
7576
  const focusElem = this.nodeListElement.querySelector("input:focus,select:focus");
@@ -7295,7 +7578,7 @@ class Wunderbaum {
7295
7578
  }
7296
7579
  /** Return true if any node is currently in edit-title mode.
7297
7580
  *
7298
- * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
7581
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
7299
7582
  */
7300
7583
  isEditingTitle() {
7301
7584
  return this._callMethod("edit.isEditingTitle");
@@ -7315,7 +7598,7 @@ class Wunderbaum {
7315
7598
  return res;
7316
7599
  }
7317
7600
  /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
7318
- * @see {@link Wunderbaum.logDebug}
7601
+ * @see {@link logDebug}
7319
7602
  */
7320
7603
  log(...args) {
7321
7604
  if (this.options.debugLevel >= 4) {
@@ -7324,7 +7607,7 @@ class Wunderbaum {
7324
7607
  }
7325
7608
  /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
7326
7609
  * and browser console level includes debug/verbose messages.
7327
- * @see {@link Wunderbaum.log}
7610
+ * @see {@link log}
7328
7611
  */
7329
7612
  logDebug(...args) {
7330
7613
  if (this.options.debugLevel >= 4) {
@@ -7362,6 +7645,19 @@ class Wunderbaum {
7362
7645
  console.warn(this.toString(), ...args); // eslint-disable-line no-console
7363
7646
  }
7364
7647
  }
7648
+ /** Emit a warning for deprecated methods. @internal */
7649
+ logDeprecate(method, options) {
7650
+ if (this.options.debugLevel >= 2) {
7651
+ let msg = `${this}: ${method} is deprecated`;
7652
+ if (options === null || options === void 0 ? void 0 : options.since) {
7653
+ msg += ` since ${options.since}`;
7654
+ }
7655
+ if (options === null || options === void 0 ? void 0 : options.hint) {
7656
+ msg += ` (${options.since})`;
7657
+ }
7658
+ console.warn(msg + "."); // eslint-disable-line no-console
7659
+ }
7660
+ }
7365
7661
  /** Reset column widths to default. @since 0.10.0 */
7366
7662
  resetColumns() {
7367
7663
  this.columns.forEach((col) => {
@@ -7530,46 +7826,64 @@ class Wunderbaum {
7530
7826
  this._focusNode = node;
7531
7827
  }
7532
7828
  /** Return the current selection/expansion/activation status. @experimental */
7533
- getState(options) {
7829
+ getState(options = {}) {
7534
7830
  var _a, _b;
7535
- let expandedKeys = undefined;
7536
- if (options.expandedKeys !== false) {
7537
- expandedKeys = [];
7831
+ const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options;
7832
+ const expandSet = new Set();
7833
+ if (expandedKeys) {
7538
7834
  for (const node of this) {
7539
- if (node.expanded) {
7540
- expandedKeys.push(node.key);
7835
+ if (node.isExpanded() && node.hasChildren()) {
7836
+ expandSet.add(node.key);
7541
7837
  }
7542
7838
  }
7543
7839
  }
7840
+ // Parents of active node are always expanded
7841
+ if (activeKey && this.activeNode) {
7842
+ this.activeNode.visitParents((n) => {
7843
+ if (n.parent) {
7844
+ expandSet.add(n.key);
7845
+ }
7846
+ }, false);
7847
+ }
7544
7848
  const state = {
7849
+ expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
7545
7850
  activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
7546
7851
  activeColIdx: this.activeColIdx,
7547
- selectedKeys: options.selectedKeys === false
7548
- ? undefined
7549
- : this.getSelectedNodes().flatMap((n) => n.key),
7550
- expandedKeys: expandedKeys,
7852
+ selectedKeys: selectedKeys
7853
+ ? this.getSelectedNodes().flatMap((n) => n.key)
7854
+ : undefined,
7551
7855
  };
7552
7856
  return state;
7553
7857
  }
7554
7858
  /** Apply selection/expansion/activation status. @experimental */
7555
- setState(state, options) {
7556
- this.runWithDeferredUpdate(() => {
7859
+ async setState(state, options = {}) {
7860
+ const { expandLazy = true } = options;
7861
+ return this.runWithDeferredUpdateAsync(async () => {
7557
7862
  var _a, _b;
7558
- if (state.selectedKeys) {
7559
- this.selectAll(false);
7560
- for (const key of state.selectedKeys) {
7561
- (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setSelected(true);
7863
+ if (state.expandedKeys && state.expandedKeys.length) {
7864
+ if (expandLazy) {
7865
+ // Expand all keys recursively, even if they are not in the tree yet
7866
+ await this._loadLazyNodes(state.expandedKeys, {
7867
+ expand: true,
7868
+ noEvents: true,
7869
+ });
7562
7870
  }
7563
- }
7564
- if (state.expandedKeys) {
7565
- for (const key of state.expandedKeys) {
7566
- (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setExpanded(true);
7871
+ else {
7872
+ for (const key of state.expandedKeys) {
7873
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true);
7874
+ }
7567
7875
  }
7568
7876
  }
7569
7877
  if (state.activeKey) {
7570
7878
  this.setActiveNode(state.activeKey);
7571
7879
  }
7572
- if (state.activeColIdx != null) {
7880
+ if (state.selectedKeys) {
7881
+ this.selectAll(false);
7882
+ for (const key of state.selectedKeys) {
7883
+ (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true);
7884
+ }
7885
+ }
7886
+ if (this.isCellNav() && state.activeColIdx != null) {
7573
7887
  this.setColumn(state.activeColIdx);
7574
7888
  }
7575
7889
  });
@@ -7730,18 +8044,33 @@ class Wunderbaum {
7730
8044
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
7731
8045
  * (defaults to sorting by title).
7732
8046
  * @param {boolean} deep pass true to sort all descendant nodes recursively
8047
+ * @deprecated use {@link sort}
7733
8048
  */
7734
8049
  sortChildren(cmp = nodeTitleSorter, deep = false) {
7735
- this.root.sortChildren(cmp, deep);
8050
+ this.logDeprecate("sortChildren()", { since: "0.14.0" });
8051
+ return this.sort({
8052
+ cmp: cmp ? cmp : undefined,
8053
+ deep: deep,
8054
+ propName: "title",
8055
+ });
7736
8056
  }
7737
8057
  /**
7738
8058
  * Convenience method to implement column sorting.
7739
8059
  * @see {@link WunderbaumNode.sortByProperty}.
7740
8060
  * @since 0.11.0
8061
+ * @deprecated use {@link sort}
7741
8062
  */
7742
8063
  sortByProperty(options) {
8064
+ this.logDeprecate("sortByProperty()", { since: "0.14.0" });
7743
8065
  this.root.sortByProperty(options);
7744
8066
  }
8067
+ /**
8068
+ * Sort nodes list by title or custom criteria.
8069
+ * @since 0.14.0
8070
+ */
8071
+ sort(options) {
8072
+ this.root.sort(options);
8073
+ }
7745
8074
  /** Convert tree to an array of plain objects.
7746
8075
  *
7747
8076
  * @param callback is called for every node, in order to allow
@@ -7993,12 +8322,10 @@ class Wunderbaum {
7993
8322
  iconElem = document.createElement("i");
7994
8323
  iconElem.className = "wb-icon";
7995
8324
  }
7996
- else if (icon.indexOf("<") >= 0) {
7997
- // HTML
8325
+ else if (TEST_HTML.test(icon)) {
7998
8326
  iconElem = elemFromHtml(icon);
7999
8327
  }
8000
- else if (TEST_IMG.test(icon)) {
8001
- // Image URL
8328
+ else if (TEST_FILE_PATH.test(icon)) {
8002
8329
  iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
8003
8330
  }
8004
8331
  else {
@@ -8236,7 +8563,8 @@ class Wunderbaum {
8236
8563
  }
8237
8564
  /**
8238
8565
  * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
8239
- * @see {@link IterableIterator<WunderbaumNode>}, {@link WunderbaumNode.visit}.
8566
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
8567
+ * @see {@link WunderbaumNode.visit}.
8240
8568
  *
8241
8569
  * @param {function} callback the callback function.
8242
8570
  * Return false to stop iteration, return "skip" to skip this node and
@@ -8379,11 +8707,71 @@ class Wunderbaum {
8379
8707
  *
8380
8708
  * Previous data is cleared. Note that also column- and type defintions may
8381
8709
  * be passed with the `source` object.
8710
+ * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request
8711
+ * and restore the previous state.
8382
8712
  */
8383
- load(source) {
8713
+ async load(source) {
8384
8714
  this.clear();
8715
+ this._initialSource = source;
8385
8716
  return this.root.load(source);
8386
8717
  }
8718
+ /** Reload the tree and optionally restore state.
8719
+ * Source defaults to last ajax url if any.
8720
+ * Restoring the active node requires stable keys
8721
+ * @see {@link WunderbaumOptions.autoKeys}
8722
+ * @see {@link Wunderbaum.load}
8723
+ * @experimental
8724
+ */
8725
+ async reload(options = {}) {
8726
+ const { source = this._initialSource, reactivate = true } = options;
8727
+ if (!source) {
8728
+ this.logWarn("No previous ajax source to reload.");
8729
+ return;
8730
+ }
8731
+ if (!reactivate) {
8732
+ return this.load(source);
8733
+ }
8734
+ const state = this.getState();
8735
+ await this.load(source);
8736
+ return this.setState(state);
8737
+ }
8738
+ /**
8739
+ * Make sure that all nodes in the given keyList are accessible.
8740
+ * This may include loading lazy parent nodes.
8741
+ * Recursively load (and optionally expand) all requested node paths.
8742
+ */
8743
+ async _loadLazyNodes(keyList, options = {}) {
8744
+ const { expand = true } = options;
8745
+ const keySet = new Set(keyList);
8746
+ // Make sure that all parent nodes are loaded (and expand if requested)
8747
+ while (keySet.size > 0) {
8748
+ const pendingNodes = [];
8749
+ const curSet = new Set(keySet);
8750
+ for (const key of curSet) {
8751
+ const node = this.findKey(key);
8752
+ if (!node) {
8753
+ continue; // key not yet found (need to load lazy parent?)
8754
+ }
8755
+ keySet.delete(key);
8756
+ if (expand) {
8757
+ pendingNodes.push(node.setExpanded(true));
8758
+ }
8759
+ else if (node.isUnloaded()) {
8760
+ pendingNodes.push(node.loadLazy());
8761
+ }
8762
+ if (node._rowElem) {
8763
+ node._render(); // show spinner even is update is suppressed
8764
+ }
8765
+ }
8766
+ if (pendingNodes.length === 0) {
8767
+ // will not load any more nodes, so if if there are still keys
8768
+ // left in the set, we will never find them
8769
+ this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet);
8770
+ break;
8771
+ }
8772
+ await Promise.allSettled(pendingNodes);
8773
+ }
8774
+ }
8387
8775
  /**
8388
8776
  * Disable render requests during operations that would trigger many updates.
8389
8777
  *
@@ -8481,8 +8869,20 @@ class Wunderbaum {
8481
8869
  }
8482
8870
  Wunderbaum.sequence = 0;
8483
8871
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8484
- Wunderbaum.version = "v0.13.0"; // Set to semver by 'grunt release'
8872
+ Wunderbaum.version = "v0.14.1"; // Set to semver by 'grunt release'
8485
8873
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8486
8874
  Wunderbaum.util = util;
8875
+ /** A map of default iconMaps.
8876
+ * May be used as default, when passing partial icon definition maps:
8877
+ * ```js
8878
+ * const tree = new mar10.Wunderbaum({
8879
+ * ...
8880
+ * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
8881
+ * folder: "bi bi-archive",
8882
+ * }),
8883
+ * });
8884
+ * ```
8885
+ */
8886
+ Wunderbaum.iconMaps = defaultIconMaps;
8487
8887
 
8488
8888
  export { Wunderbaum };