wunderbaum 0.13.0 → 0.14.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 - debounce.ts
9
9
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
10
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
10
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
11
11
  */
12
12
  /*
13
13
  * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21
@@ -299,7 +299,7 @@
299
299
  /*!
300
300
  * Wunderbaum - util
301
301
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
302
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
302
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
303
303
  */
304
304
  /** @module util */
305
305
  /** Readable names for `MouseEvent.button` */
@@ -449,7 +449,7 @@
449
449
  }
450
450
  return obj;
451
451
  }
452
- /** Shortcut for `throw new Error(msg)`.*/
452
+ /** Shortcut for `throw new Error(msg)`. */
453
453
  function error(msg) {
454
454
  throw new Error(msg);
455
455
  }
@@ -964,6 +964,10 @@
964
964
  }
965
965
  throw new Error(`Expected a string like '123px': ${defaults}`);
966
966
  }
967
+ /** Cast any value to <T>. */
968
+ function unsafeCast(value) {
969
+ return value;
970
+ }
967
971
  /** Return the the boolean value of the first non-null element.
968
972
  * Example:
969
973
  * ```js
@@ -1037,7 +1041,7 @@
1037
1041
  const throttledFn = (...args) => {
1038
1042
  if (waiting) {
1039
1043
  pendingArgs = args;
1040
- // console.log(`adaptiveThrottle() queing request #${waiting}...`, args);
1044
+ // console.log(`adaptiveThrottle() queueing request #${waiting}...`, args);
1041
1045
  waiting += 1;
1042
1046
  }
1043
1047
  else {
@@ -1092,6 +1096,60 @@
1092
1096
  };
1093
1097
  return throttledFn;
1094
1098
  }
1099
+ /**
1100
+ * MurmurHash3 implementation for strings.
1101
+ * @param key The input string to hash.
1102
+ * @param asString Optional convert result to zero-padded string of 8 characters.
1103
+ * @param seed Optional seed value.
1104
+ * @returns A 32-bit hash as a number or string.
1105
+ */
1106
+ function murmurHash3(key, asString = true, seed = 0) {
1107
+ let h1 = seed;
1108
+ const remainder = key.length & 3; // key.length % 4
1109
+ const bytes = key.length - remainder;
1110
+ const c1 = 0xcc9e2d51;
1111
+ const c2 = 0x1b873593;
1112
+ let i = 0;
1113
+ while (i < bytes) {
1114
+ let k1 = (key.charCodeAt(i) & 0xff) |
1115
+ ((key.charCodeAt(++i) & 0xff) << 8) |
1116
+ ((key.charCodeAt(++i) & 0xff) << 16) |
1117
+ ((key.charCodeAt(++i) & 0xff) << 24);
1118
+ ++i;
1119
+ k1 = Math.imul(k1, c1);
1120
+ k1 = (k1 << 15) | (k1 >>> 17);
1121
+ k1 = Math.imul(k1, c2);
1122
+ h1 ^= k1;
1123
+ h1 = (h1 << 13) | (h1 >>> 19);
1124
+ h1 = Math.imul(h1, 5) + 0xe6546b64;
1125
+ }
1126
+ let k1 = 0;
1127
+ switch (remainder) {
1128
+ case 3:
1129
+ k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
1130
+ // fall through
1131
+ case 2:
1132
+ k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
1133
+ // fall through
1134
+ case 1:
1135
+ k1 ^= key.charCodeAt(i) & 0xff;
1136
+ k1 = Math.imul(k1, c1);
1137
+ k1 = (k1 << 15) | (k1 >>> 17);
1138
+ k1 = Math.imul(k1, c2);
1139
+ h1 ^= k1;
1140
+ }
1141
+ h1 ^= key.length;
1142
+ h1 ^= h1 >>> 16;
1143
+ h1 = Math.imul(h1, 0x85ebca6b);
1144
+ h1 ^= h1 >>> 13;
1145
+ h1 = Math.imul(h1, 0xc2b2ae35);
1146
+ h1 ^= h1 >>> 16;
1147
+ if (asString) {
1148
+ // Convert to 8 digit hex string
1149
+ return (h1 >>> 0).toString(16).padStart(8, "0");
1150
+ }
1151
+ return h1 >>> 0; // Convert to unsigned 32-bit integer
1152
+ }
1095
1153
 
1096
1154
  var util = /*#__PURE__*/Object.freeze({
1097
1155
  __proto__: null,
@@ -1122,6 +1180,7 @@
1122
1180
  isFunction: isFunction,
1123
1181
  isMac: isMac,
1124
1182
  isPlainObject: isPlainObject,
1183
+ murmurHash3: murmurHash3,
1125
1184
  noop: noop,
1126
1185
  onEvent: onEvent,
1127
1186
  overrideMethod: overrideMethod,
@@ -1135,13 +1194,14 @@
1135
1194
  toPixel: toPixel,
1136
1195
  toSet: toSet,
1137
1196
  toggleCheckbox: toggleCheckbox,
1138
- type: type
1197
+ type: type,
1198
+ unsafeCast: unsafeCast
1139
1199
  });
1140
1200
 
1141
1201
  /*!
1142
1202
  * Wunderbaum - types
1143
1203
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1144
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1204
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1145
1205
  */
1146
1206
  /**
1147
1207
  * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
@@ -1209,7 +1269,7 @@
1209
1269
  /*!
1210
1270
  * Wunderbaum - wb_extension_base
1211
1271
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1212
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1272
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1213
1273
  */
1214
1274
  class WunderbaumExtension {
1215
1275
  constructor(tree, id, defaults) {
@@ -1268,7 +1328,7 @@
1268
1328
  /*!
1269
1329
  * Wunderbaum - ext-filter
1270
1330
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1271
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1331
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1272
1332
  */
1273
1333
  const START_MARKER = "\uFFF7";
1274
1334
  const END_MARKER = "\uFFF8";
@@ -1556,6 +1616,10 @@
1556
1616
  */
1557
1617
  filterBranches(filter, options) {
1558
1618
  assert(options.matchBranch === undefined, "filterBranches() is deprecated.");
1619
+ this.tree.logDeprecate("filterBranches()", {
1620
+ since: "0.9.0",
1621
+ hint: "Use `filterNodes` instead and set `options.matchBranch: true`",
1622
+ });
1559
1623
  options.matchBranch = true;
1560
1624
  return this._applyFilterNoUpdate(filter, options);
1561
1625
  }
@@ -1620,7 +1684,7 @@
1620
1684
  }
1621
1685
  }
1622
1686
  /**
1623
- * @description Marks the matching charecters of `text` either by `mark` or
1687
+ * @description Marks the matching characters of `text` either by `mark` or
1624
1688
  * by exotic*Chars (if `escapeTitles` is `true`) based on `matches`
1625
1689
  * which is an array of matching groups.
1626
1690
  * @param {string} text
@@ -1659,7 +1723,7 @@
1659
1723
  /*!
1660
1724
  * Wunderbaum - ext-keynav
1661
1725
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1662
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1726
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1663
1727
  */
1664
1728
  const QUICKSEARCH_DELAY = 500;
1665
1729
  class KeynavExtension extends WunderbaumExtension {
@@ -2023,7 +2087,7 @@
2023
2087
  /*!
2024
2088
  * Wunderbaum - ext-logger
2025
2089
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2026
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2090
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2027
2091
  */
2028
2092
  class LoggerExtension extends WunderbaumExtension {
2029
2093
  constructor(tree) {
@@ -2065,7 +2129,7 @@
2065
2129
  /*!
2066
2130
  * Wunderbaum - ext-dnd
2067
2131
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2068
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2132
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2069
2133
  */
2070
2134
  const nodeMimeType = "application/x-wunderbaum-node";
2071
2135
  class DndExtension extends WunderbaumExtension {
@@ -2301,7 +2365,6 @@
2301
2365
  */
2302
2366
  onDragEvent(e) {
2303
2367
  var _a;
2304
- // const tree = this.tree;
2305
2368
  const dndOpts = this.treeOpts.dnd;
2306
2369
  const srcNode = Wunderbaum.getNode(e);
2307
2370
  if (!srcNode) {
@@ -2327,7 +2390,7 @@
2327
2390
  return false;
2328
2391
  }
2329
2392
  const nodeData = srcNode.toDict(true, (n) => {
2330
- // We don't want to re-use the key on drop:
2393
+ // We don't want to reuse the key on drop:
2331
2394
  n._orgKey = n.key;
2332
2395
  delete n.key;
2333
2396
  });
@@ -2389,6 +2452,7 @@
2389
2452
  };
2390
2453
  if (!targetNode) {
2391
2454
  this._leaveNode();
2455
+ e.preventDefault(); // Don't open file in browser when dropped in empty area
2392
2456
  return;
2393
2457
  }
2394
2458
  if (["drop"].includes(e.type)) {
@@ -2494,19 +2558,20 @@
2494
2558
  nodeData = nodeData ? JSON.parse(nodeData) : null;
2495
2559
  const srcNode = this.srcNode;
2496
2560
  const lastDropEffect = this.lastDropEffect;
2497
- setTimeout(() => {
2498
- // Decouple this call, because drop actions may prevent the dragend event
2499
- // from being fired on some browsers
2500
- targetNode._callEvent("dnd.drop", {
2501
- event: e,
2502
- region: region,
2503
- suggestedDropMode: region === "over" ? "appendChild" : region,
2504
- suggestedDropEffect: lastDropEffect,
2505
- // suggestedDropEffect: e.dataTransfer?.dropEffect,
2506
- sourceNode: srcNode,
2507
- sourceNodeData: nodeData,
2508
- });
2509
- }, 10);
2561
+ /* Before v0.14.0, we decoupled `_callEvent` like so:
2562
+ Decouple this call, because drop actions may prevent the dragend
2563
+ event from being fired on some browsers.
2564
+ setTimeout(() => {...}, 10);
2565
+ however this made e.dataTransfer.items inaccessible */
2566
+ targetNode._callEvent("dnd.drop", {
2567
+ event: e,
2568
+ region: region,
2569
+ suggestedDropMode: region === "over" ? "appendChild" : region,
2570
+ suggestedDropEffect: lastDropEffect,
2571
+ sourceNode: srcNode,
2572
+ sourceNodeData: nodeData,
2573
+ dataTransfer: e.dataTransfer,
2574
+ });
2510
2575
  }
2511
2576
  return false;
2512
2577
  }
@@ -2515,7 +2580,7 @@
2515
2580
  /*!
2516
2581
  * Wunderbaum - drag_observer
2517
2582
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2518
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2583
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2519
2584
  */
2520
2585
  /**
2521
2586
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2664,7 +2729,7 @@
2664
2729
  /*!
2665
2730
  * Wunderbaum - common
2666
2731
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2667
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2732
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2668
2733
  */
2669
2734
  const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
2670
2735
  /**
@@ -2684,12 +2749,18 @@
2684
2749
  const RENDER_MAX_PREFETCH = 5;
2685
2750
  /** Minimum column width if not set otherwise. */
2686
2751
  const DEFAULT_MIN_COL_WIDTH = 4;
2752
+ /**
2753
+ * A value for `node.type` that by convention may be used to mark a node as directory.
2754
+ * It may be used to sort 'directories' to the top.
2755
+ */
2756
+ const NODE_TYPE_FOLDER = "folder";
2687
2757
  /** Regular expression to detect if a string describes an image URL (in contrast
2688
2758
  * to a class name). Strings are considered image urls if they contain '.' or '/'.
2759
+ * `<` is ignored, because it is probably an html tag.
2689
2760
  */
2690
- const TEST_IMG = new RegExp(/\.|\//);
2691
- // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
2692
- // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
2761
+ const TEST_FILE_PATH = /^(?!.*<).*[/.]/;
2762
+ /** Regular expression to detect if a string describes an HTML element. */
2763
+ const TEST_HTML = /</;
2693
2764
  /**
2694
2765
  * Default node icons for icon libraries
2695
2766
  *
@@ -2697,7 +2768,7 @@
2697
2768
  * - 'fontawesome6' {@link https://fontawesome.com/icons}
2698
2769
  *
2699
2770
  */
2700
- const iconMaps = {
2771
+ const defaultIconMaps = {
2701
2772
  bootstrap: {
2702
2773
  error: "bi bi-exclamation-triangle",
2703
2774
  // loading: "bi bi-hourglass-split wb-busy",
@@ -2745,7 +2816,7 @@
2745
2816
  radioChecked: "fa-solid fa-circle",
2746
2817
  radioUnchecked: "fa-regular fa-circle",
2747
2818
  radioUnknown: "fa-regular fa-circle-question",
2748
- folder: "fa-solid fa-folder-closed",
2819
+ folder: "fa-regular fa-folder-closed",
2749
2820
  folderOpen: "fa-regular fa-folder-open",
2750
2821
  folderLazy: "fa-solid fa-folder-plus",
2751
2822
  doc: "fa-regular fa-file",
@@ -2818,12 +2889,20 @@
2818
2889
  return reMatch.test(node.title);
2819
2890
  };
2820
2891
  }
2821
- /** Compare two nodes by title (case-insensitive). */
2892
+ /** Compare two nodes by title (case-insensitive).
2893
+ * @deprecated Use `key` option instead of `cmp` in sort methods.
2894
+ */
2822
2895
  function nodeTitleSorter(a, b) {
2823
2896
  const x = a.title.toLowerCase();
2824
2897
  const y = b.title.toLowerCase();
2825
2898
  return x === y ? 0 : x > y ? 1 : -1;
2826
2899
  }
2900
+ // /** Compare nodes by title (case-insensitive). */
2901
+ // export function nodeTitleKeyGetter(
2902
+ // node: WunderbaumNode
2903
+ // ): string | number | Array<any> {
2904
+ // return node.title.toLowerCase();
2905
+ // }
2827
2906
  /**
2828
2907
  * Convert 'flat' to 'nested' format.
2829
2908
  *
@@ -3014,7 +3093,7 @@
3014
3093
  /*!
3015
3094
  * Wunderbaum - ext-grid
3016
3095
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3017
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3096
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
3018
3097
  */
3019
3098
  class GridExtension extends WunderbaumExtension {
3020
3099
  constructor(tree) {
@@ -3074,7 +3153,7 @@
3074
3153
  super.init();
3075
3154
  }
3076
3155
  /**
3077
- * Hanldes drag and sragstop events for column resizing.
3156
+ * Handles drag and sragstop events for column resizing.
3078
3157
  */
3079
3158
  handleDrag(e) {
3080
3159
  const custom = e.customData;
@@ -3105,7 +3184,7 @@
3105
3184
  /*!
3106
3185
  * Wunderbaum - deferred
3107
3186
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3108
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3187
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
3109
3188
  */
3110
3189
  /**
3111
3190
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -3158,7 +3237,7 @@
3158
3237
  /*!
3159
3238
  * Wunderbaum - wunderbaum_node
3160
3239
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3161
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3240
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
3162
3241
  */
3163
3242
  /** WunderbaumNode properties that can be passed with source data.
3164
3243
  * (Any other source properties will be stored as `node.data.PROP`.)
@@ -3209,7 +3288,7 @@
3209
3288
  */
3210
3289
  class WunderbaumNode {
3211
3290
  constructor(tree, parent, data) {
3212
- var _a, _b;
3291
+ var _a;
3213
3292
  /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
3214
3293
  * times within a tree (in this case we have 'clone nodes').
3215
3294
  * @see Use {@link setKey} to modify.
@@ -3239,8 +3318,8 @@
3239
3318
  assert(!data.children, "'children' not allowed here");
3240
3319
  this.tree = tree;
3241
3320
  this.parent = parent;
3242
- this.key = "" + ((_a = data.key) !== null && _a !== void 0 ? _a : ++WunderbaumNode.sequence);
3243
- this.title = "" + ((_b = data.title) !== null && _b !== void 0 ? _b : "<" + this.key + ">");
3321
+ this.key = tree._calculateKey(data, parent);
3322
+ this.title = "" + ((_a = data.title) !== null && _a !== void 0 ? _a : "<" + this.key + ">");
3244
3323
  this.expanded = !!data.expanded;
3245
3324
  this.lazy = !!data.lazy;
3246
3325
  // We set the following node properties only if a matching data value is
@@ -3361,8 +3440,14 @@
3361
3440
  const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel;
3362
3441
  for (const child of nodeData) {
3363
3442
  const subChildren = child.children;
3443
+ // Remove children property from source data because it should not be
3444
+ // passed to the constructor of WunderbaumNode:
3364
3445
  delete child.children;
3365
3446
  const n = new WunderbaumNode(tree, this, child);
3447
+ // Set `children` property again, so it can be used in `reload()`
3448
+ if (subChildren != null) {
3449
+ child.children = subChildren;
3450
+ }
3366
3451
  if (forceExpand && !n.isUnloaded()) {
3367
3452
  n.expanded = true;
3368
3453
  }
@@ -3778,15 +3863,12 @@
3778
3863
  }
3779
3864
  return l;
3780
3865
  }
3781
- /** Return a string representing the hierachical node path, e.g. "a/b/c".
3866
+ /** Return a string representing the hierarchical node path, e.g. "a/b/c".
3782
3867
  * @param includeSelf
3783
3868
  * @param part property name or callback
3784
3869
  * @param separator
3785
3870
  */
3786
3871
  getPath(includeSelf = true, part = "title", separator = "/") {
3787
- // includeSelf = includeSelf !== false;
3788
- // part = part || "title";
3789
- // separator = separator || "/";
3790
3872
  let val;
3791
3873
  const path = [];
3792
3874
  const isFunc = typeof part === "function";
@@ -3801,7 +3883,7 @@
3801
3883
  }, includeSelf);
3802
3884
  return path.join(separator);
3803
3885
  }
3804
- /** Return the preceeding node (under the same parent) or null. */
3886
+ /** Return the preceding node (under the same parent) or null. */
3805
3887
  getPrevSibling() {
3806
3888
  const ac = this.parent.children;
3807
3889
  const idx = ac.indexOf(this);
@@ -3830,7 +3912,7 @@
3830
3912
  hasClass(className) {
3831
3913
  return this.classes ? this.classes.has(className) : false;
3832
3914
  }
3833
- /** Return true if node ist the currently focused node. @since 0.9.0 */
3915
+ /** Return true if node is the currently focused node. @since 0.9.0 */
3834
3916
  hasFocus() {
3835
3917
  return this.tree.focusNode === this;
3836
3918
  }
@@ -3885,7 +3967,7 @@
3885
3967
  * an expand operation is currently possible.
3886
3968
  */
3887
3969
  isExpandable(andCollapsed = false) {
3888
- // `false` is never expandable (unoffical)
3970
+ // `false` is never expandable (unofficial)
3889
3971
  if ((andCollapsed && this.expanded) || this.children === false) {
3890
3972
  return false;
3891
3973
  }
@@ -3948,11 +4030,11 @@
3948
4030
  isPartsel() {
3949
4031
  return !this.selected && !!this._partsel;
3950
4032
  }
3951
- /** 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. */
3952
4034
  isRadio() {
3953
4035
  return !!this.parent.radiogroup || this.getOption("checkbox") === "radio";
3954
4036
  }
3955
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4037
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3956
4038
  isRendered() {
3957
4039
  return !!this._rowElem;
3958
4040
  }
@@ -4707,9 +4789,9 @@
4707
4789
  const typeInfo = this.type ? tree.types[this.type] : null;
4708
4790
  const rowDiv = this._rowElem;
4709
4791
  // Row markup already exists
4710
- const nodeElem = rowDiv.querySelector("span.wb-node");
4711
- const expanderSpan = nodeElem.querySelector("i.wb-expander");
4712
- const checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
4792
+ const nodeSpan = rowDiv.querySelector("span.wb-node");
4793
+ const expanderElem = nodeSpan.querySelector("i.wb-expander");
4794
+ const checkboxElem = nodeSpan.querySelector("i.wb-checkbox");
4713
4795
  const rowClasses = ["wb-row"];
4714
4796
  this.expanded ? rowClasses.push("wb-expanded") : 0;
4715
4797
  this.lazy ? rowClasses.push("wb-lazy") : 0;
@@ -4734,7 +4816,7 @@
4734
4816
  if (typeInfo && typeInfo.classes) {
4735
4817
  rowDiv.classList.add(...typeInfo.classes);
4736
4818
  }
4737
- if (expanderSpan) {
4819
+ if (expanderElem) {
4738
4820
  let image = null;
4739
4821
  if (this._isLoading) {
4740
4822
  image = iconMap.loading;
@@ -4751,16 +4833,20 @@
4751
4833
  image = iconMap.expanderLazy;
4752
4834
  }
4753
4835
  if (image == null) {
4754
- expanderSpan.classList.add("wb-indent");
4836
+ expanderElem.className = "wb-expander";
4837
+ expanderElem.classList.add("wb-indent");
4838
+ }
4839
+ else if (TEST_HTML.test(image)) {
4840
+ expanderElem.replaceWith(elemFromHtml(image));
4755
4841
  }
4756
- else if (TEST_IMG.test(image)) {
4757
- expanderSpan.style.backgroundImage = `url('${image}')`;
4842
+ else if (TEST_FILE_PATH.test(image)) {
4843
+ expanderElem.style.backgroundImage = `url('${image}')`;
4758
4844
  }
4759
4845
  else {
4760
- expanderSpan.className = "wb-expander " + image;
4846
+ expanderElem.className = "wb-expander " + image;
4761
4847
  }
4762
4848
  }
4763
- if (checkboxSpan) {
4849
+ if (checkboxElem) {
4764
4850
  let cbclass = "wb-checkbox ";
4765
4851
  if (this.isRadio()) {
4766
4852
  cbclass += "wb-radio ";
@@ -4784,7 +4870,7 @@
4784
4870
  cbclass += iconMap.checkUnchecked;
4785
4871
  }
4786
4872
  }
4787
- checkboxSpan.className = cbclass;
4873
+ checkboxElem.className = cbclass;
4788
4874
  }
4789
4875
  // Fix active cell in cell-nav mode
4790
4876
  if (!opts.isNew) {
@@ -4794,9 +4880,9 @@
4794
4880
  colSpan.classList.remove("wb-error", "wb-invalid");
4795
4881
  }
4796
4882
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4797
- const iconSpan = nodeElem.querySelector("i.wb-icon");
4883
+ const iconSpan = nodeSpan.querySelector("i.wb-icon");
4798
4884
  if (iconSpan) {
4799
- this._createIcon(nodeElem, iconSpan, !expanderSpan);
4885
+ this._createIcon(nodeSpan, iconSpan, !expanderElem);
4800
4886
  }
4801
4887
  }
4802
4888
  // Adjust column width
@@ -5101,6 +5187,32 @@
5101
5187
  setKey(key, refKey) {
5102
5188
  throw new Error("Not yet implemented");
5103
5189
  }
5190
+ // /**
5191
+ // * Calculate a *stable*, unique key for this node from its refKey (or title).
5192
+ // * We also add information from the parent, because a refKey may occur multiple
5193
+ // * times in a tree.
5194
+ // */
5195
+ // calcUniqueKey() {
5196
+ // // Assuming that the parent's key was calculated the same way, we implicitly
5197
+ // // involve the whole refKey-path:
5198
+ // const s = this.key + (this.refKey || this.title);
5199
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5200
+ // // https://security.stackexchange.com/q/209882/207588
5201
+ // const h1 = util.murmurHash3(s, true);
5202
+ // return "id_" + h1 + util.murmurHash3(h1 + s, true);
5203
+ // // const l = [];
5204
+ // // // eslint-disable-next-line @typescript-eslint/no-this-alias
5205
+ // // let node: WunderbaumNode = this;
5206
+ // // while (node.parent) {
5207
+ // // l.unshift(node.refKey || node.key);
5208
+ // // node = node.parent;
5209
+ // // }
5210
+ // // const path = l.join("/");
5211
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5212
+ // // https://security.stackexchange.com/q/209882/207588
5213
+ // // const h1 = util.murmurHash3(path, true);
5214
+ // // return "id_" + h1 + util.murmurHash3(h1 + path, true);
5215
+ // }
5104
5216
  /**
5105
5217
  * Trigger a repaint, typically after a status or data change.
5106
5218
  *
@@ -5132,6 +5244,23 @@
5132
5244
  });
5133
5245
  return nodeList;
5134
5246
  }
5247
+ /**
5248
+ * Return an array of refKey values.
5249
+ *
5250
+ * RefKeys are unique identifiers for a node data, and are used to identify
5251
+ * clones.
5252
+ * If more than one node has the same refKey, it is only returned once.
5253
+ * @param selected if true, only return refKeys of selected nodes.
5254
+ */
5255
+ getRefKeys(selected = false) {
5256
+ const refKeys = new Set();
5257
+ this.visit((node) => {
5258
+ if (node.refKey != null && (!selected || node.selected)) {
5259
+ refKeys.add(node.refKey);
5260
+ }
5261
+ });
5262
+ return Array.from(refKeys);
5263
+ }
5135
5264
  /** Toggle the check/uncheck state. */
5136
5265
  toggleSelected(options) {
5137
5266
  let flag = this.isSelected();
@@ -5311,9 +5440,11 @@
5311
5440
  if (selectMode === "hier") {
5312
5441
  this.fixSelection3AfterClick();
5313
5442
  }
5314
- else if (selectMode === "single") {
5443
+ else if (selectMode === "single" && flag) {
5315
5444
  tree.visit((n) => {
5316
- n.selected = false;
5445
+ if (n !== this) {
5446
+ n.selected = false;
5447
+ }
5317
5448
  });
5318
5449
  }
5319
5450
  }
@@ -5415,30 +5546,16 @@
5415
5546
  this.tooltip = tooltip;
5416
5547
  this.update();
5417
5548
  }
5418
- _sortChildren(cmp, deep) {
5419
- const cl = this.children;
5420
- if (!cl) {
5421
- return;
5422
- }
5423
- cl.sort(cmp);
5424
- if (deep) {
5425
- for (let i = 0, l = cl.length; i < l; i++) {
5426
- if (cl[i].children) {
5427
- cl[i]._sortChildren(cmp, deep);
5428
- }
5429
- }
5430
- }
5431
- }
5432
5549
  /**
5433
5550
  * Sort child list by title or custom criteria.
5434
5551
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
5435
5552
  * (defaults to sorting by title).
5436
5553
  * @param {boolean} deep pass true to sort all descendant nodes recursively
5554
+ * @deprecated use {@link sort}
5437
5555
  */
5438
5556
  sortChildren(cmp = nodeTitleSorter, deep = false) {
5439
- this._sortChildren(cmp || nodeTitleSorter, deep);
5440
- this.tree.update(ChangeType.structure);
5441
- // this.triggerModify("sort"); // TODO
5557
+ this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" });
5558
+ return this.sort({ cmp: cmp ? cmp : undefined, deep: deep });
5442
5559
  }
5443
5560
  /**
5444
5561
  * Renumber nodes `_nativeIndex`. This is useful to allow to restore the
@@ -5460,74 +5577,142 @@
5460
5577
  /**
5461
5578
  * Convenience method to implement column sorting.
5462
5579
  * @since 0.11.0
5580
+ * @deprecated use {@link sort}
5463
5581
  */
5464
5582
  sortByProperty(options) {
5465
- var _a, _b, _c;
5466
- const { caseInsensitive = true, deep = true, nativeOrderPropName = "_nativeIndex", updateColInfo = false, } = options;
5467
- let order;
5468
- let colDef;
5583
+ this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" });
5584
+ return this.sort(options);
5585
+ }
5586
+ /**
5587
+ * Implement column sorting.
5588
+ * @since 0.14.0
5589
+ */
5590
+ sort(options) {
5591
+ const tree = this.tree;
5592
+ let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined,
5593
+ // Support click on column sort header:
5594
+ updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options;
5595
+ propName !== null && propName !== void 0 ? propName : (propName = colId);
5596
+ if (propName === "*") {
5597
+ propName = "title";
5598
+ }
5599
+ const isFolder = tree.options.sortFoldersFirst === true
5600
+ ? (node) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER
5601
+ : tree.options.sortFoldersFirst;
5469
5602
  if (updateColInfo) {
5470
- colDef = this.tree["_columnsById"][options.colId];
5603
+ const colDef = this.tree["_columnsById"][options.colId];
5471
5604
  assert(colDef, `Invalid colId specified: ${options.colId}`);
5472
- order =
5473
- (_a = options.order) !== null && _a !== void 0 ? _a : rotate(colDef.sortOrder, ["asc", "desc", undefined]);
5605
+ order !== null && order !== void 0 ? order : (order = rotate(colDef.sortOrder, ["asc", "desc", undefined]));
5474
5606
  for (const col of this.tree.columns) {
5475
5607
  col.sortOrder = col === colDef ? order : undefined;
5476
5608
  }
5609
+ if (order === undefined) {
5610
+ propName = nativeOrderPropName;
5611
+ order = "asc";
5612
+ }
5477
5613
  this.tree.update(ChangeType.colStructure);
5478
5614
  }
5479
5615
  else {
5480
- order = (_b = options.order) !== null && _b !== void 0 ? _b : "asc";
5616
+ propName !== null && propName !== void 0 ? propName : (propName = "title");
5617
+ order !== null && order !== void 0 ? order : (order = "asc");
5618
+ }
5619
+ this.logDebug(`sort(), propName=${propName}, ${order}`, options);
5620
+ assert(propName || cmp || key, "No `propName` or `key` specified");
5621
+ // Define a key callback from the parameters we have
5622
+ if (key == null && cmp == null) {
5623
+ key = (node) => {
5624
+ let val;
5625
+ if (NODE_DICT_PROPS.has(propName)) {
5626
+ val = node[propName];
5627
+ }
5628
+ else {
5629
+ val = node.data[propName];
5630
+ }
5631
+ if (caseInsensitive && typeof val === "string") {
5632
+ val = val.toLowerCase();
5633
+ }
5634
+ return val;
5635
+ };
5481
5636
  }
5482
- let propName = (_c = options.propName) !== null && _c !== void 0 ? _c : (options.colId || "");
5483
- if (propName === "*") {
5484
- propName = "title";
5637
+ // Define a compare callback that uses the key callback
5638
+ if (cmp) {
5639
+ assert(!key, "`key` and `cmp` are mutually exclusive");
5640
+ tree.logDeprecate("SortOptions.cmp", {
5641
+ since: "0.14.0",
5642
+ hint: "use the `key` callback instead",
5643
+ });
5485
5644
  }
5486
- if (order == null) {
5487
- propName = nativeOrderPropName;
5488
- order = "asc";
5645
+ else {
5646
+ if (options.propName || options.caseInsensitive) {
5647
+ tree.logWarn("sort(): ignoring propName, caseInsensitive");
5648
+ }
5649
+ cmp = (a, b) => {
5650
+ if (isFolder) {
5651
+ const isFolderA = isFolder(a);
5652
+ if (isFolderA !== isFolder(b)) {
5653
+ return isFolderA ? -1 : 1;
5654
+ }
5655
+ }
5656
+ let x = key(a);
5657
+ let y = key(b);
5658
+ // Assure we have reasonable comparisons with null values:
5659
+ if (x == null) {
5660
+ x = typeof y === "string" ? "" : 0;
5661
+ }
5662
+ else if (typeof x === "boolean") {
5663
+ x = x ? 1 : 0;
5664
+ }
5665
+ if (y == null) {
5666
+ y = typeof x === "string" ? "" : 0;
5667
+ }
5668
+ else if (typeof y === "boolean") {
5669
+ y = y ? 1 : 0;
5670
+ }
5671
+ if (order === "desc") {
5672
+ return x === y ? 0 : x > y ? -1 : 1;
5673
+ }
5674
+ return x === y ? 0 : x > y ? 1 : -1;
5675
+ };
5489
5676
  }
5490
- this.logDebug(`sortByProperty(), propName=${propName}, ${order}`, options);
5491
- assert(propName, "No property name specified");
5492
- const cmp = (a, b) => {
5493
- let av, bv;
5494
- if (NODE_DICT_PROPS.has(propName)) {
5495
- av = a[propName];
5496
- bv = b[propName];
5497
- }
5498
- else {
5499
- av = a.data[propName];
5500
- bv = b.data[propName];
5501
- }
5502
- if (av == null && bv == null) {
5503
- return 0;
5504
- }
5505
- if (av == null) {
5506
- av = typeof bv === "string" ? "" : 0;
5507
- }
5508
- else if (typeof av === "boolean") {
5509
- av = av ? 1 : 0;
5510
- }
5511
- if (bv == null) {
5512
- bv = typeof av === "string" ? "" : 0;
5513
- }
5514
- else if (typeof bv === "boolean") {
5515
- bv = bv ? 1 : 0;
5677
+ function _sortChildren(cl) {
5678
+ if (!cl) {
5679
+ return;
5516
5680
  }
5517
- if (caseInsensitive) {
5518
- if (typeof av === "string") {
5519
- av = av.toLowerCase();
5520
- }
5521
- if (typeof bv === "string") {
5522
- bv = bv.toLowerCase();
5681
+ cl.sort(cmp);
5682
+ if (deep) {
5683
+ for (let i = 0, l = cl.length; i < l; i++) {
5684
+ if (cl[i].children) {
5685
+ _sortChildren(cl[i].children);
5686
+ }
5523
5687
  }
5524
5688
  }
5525
- if (order === "desc") {
5526
- return av === bv ? 0 : av > bv ? -1 : 1;
5689
+ }
5690
+ if (this.children) {
5691
+ _sortChildren(this.children);
5692
+ }
5693
+ this.tree.update(ChangeType.structure);
5694
+ // this.triggerModify("sort"); // TODO
5695
+ }
5696
+ /**
5697
+ * Re-apply current sorting if any (use after lazy load).
5698
+ * Example:
5699
+ * ```js
5700
+ * load: function (e) {
5701
+ * // Whe loading a lazy branch, apply current sort order if any
5702
+ * e.node.resort();
5703
+ * },
5704
+ * ```
5705
+ * @since 0.14.0
5706
+ */
5707
+ resort(options = {}) {
5708
+ for (const colDef of this.tree.columns) {
5709
+ if (colDef.sortOrder) {
5710
+ options.colId = colDef.id;
5711
+ options.order = colDef.sortOrder;
5712
+ this.sort(options);
5713
+ break;
5527
5714
  }
5528
- return av === bv ? 0 : av > bv ? 1 : -1;
5529
- };
5530
- return this.sortChildren(cmp, deep);
5715
+ }
5531
5716
  }
5532
5717
  /**
5533
5718
  * Trigger `modifyChild` event on a parent to signal that a child was modified.
@@ -5564,7 +5749,8 @@
5564
5749
  * @param {function} callback the callback function.
5565
5750
  * Return false to stop iteration, return "skip" to skip this node and
5566
5751
  * its children only.
5567
- * @see {@link IterableIterator<WunderbaumNode>}, {@link Wunderbaum.visit}.
5752
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
5753
+ * @see {@link Wunderbaum.visit}.
5568
5754
  */
5569
5755
  visit(callback, includeSelf = false) {
5570
5756
  let res = true;
@@ -5637,7 +5823,7 @@
5637
5823
  /*!
5638
5824
  * Wunderbaum - ext-edit
5639
5825
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
5640
- * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
5826
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
5641
5827
  */
5642
5828
  // const START_MARKER = "\uFFF7";
5643
5829
  class EditExtension extends WunderbaumExtension {
@@ -5872,7 +6058,7 @@
5872
6058
  newValue = newValue.trim();
5873
6059
  }
5874
6060
  if (!node) {
5875
- this.tree.logDebug("stopEditTitle: not in edit mode.");
6061
+ // this.tree.logDebug("stopEditTitle: not in edit mode.");
5876
6062
  return;
5877
6063
  }
5878
6064
  node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue);
@@ -5972,8 +6158,8 @@
5972
6158
  * https://github.com/mar10/wunderbaum
5973
6159
  *
5974
6160
  * Released under the MIT license.
5975
- * @version v0.13.0
5976
- * @date Sat, 08 Mar 2025 14:16:31 GMT
6161
+ * @version v0.14.0
6162
+ * @date Fri, 20 Mar 2026 16:58:31 GMT
5977
6163
  */
5978
6164
  // import "./wunderbaum.scss";
5979
6165
  class WbSystemRoot extends WunderbaumNode {
@@ -6022,17 +6208,17 @@
6022
6208
  this._disableUpdateIgnoreCount = 0;
6023
6209
  this._activeNode = null;
6024
6210
  this._focusNode = null;
6211
+ this._initialSource = null;
6025
6212
  /** Shared properties, referenced by `node.type`. */
6026
6213
  this.types = {};
6027
6214
  /** List of column definitions. */
6028
- this.columns = []; // any[] = [];
6215
+ this.columns = [];
6029
6216
  this._columnsById = {};
6030
6217
  // Modification Status
6031
6218
  this.pendingChangeTypes = new Set();
6032
6219
  /** Expose some useful methods of the util.ts module as `tree._util`. */
6033
6220
  this._util = util;
6034
6221
  // --- SELECT ---
6035
- // /** @internal */
6036
6222
  // public selectRangeAnchor: WunderbaumNode | null = null;
6037
6223
  // --- BREADCRUMB ---
6038
6224
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
@@ -6051,37 +6237,44 @@
6051
6237
  this.lastQuicksearchTerm = "";
6052
6238
  // --- EDIT ---
6053
6239
  this.lastClickTime = 0;
6054
- const opts = (this.options = extend({
6055
- id: null,
6056
- source: null, // URL for GET/PUT, Ajax options, or callback
6057
- element: null, // <div class="wunderbaum">
6240
+ // Set default options and merge with user options
6241
+ const initOptions = Object.assign({
6242
+ id: undefined,
6243
+ source: [], // URL for GET/PUT, Ajax options, or callback
6244
+ element: unsafeCast(null),
6058
6245
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
6059
6246
  header: null, // Show/hide header (pass bool or string)
6060
- // headerHeightPx: ROW_HEIGHT,
6061
6247
  rowHeightPx: DEFAULT_ROW_HEIGHT,
6062
6248
  iconMap: "bootstrap",
6063
- columns: null,
6064
- types: null,
6065
- // escapeTitles: true,
6249
+ columns: [], //util.unsafeCast<ColumnDefinitionList>(null),
6250
+ types: {},
6066
6251
  enabled: true,
6067
6252
  fixedCol: false,
6068
6253
  showSpinner: false,
6069
6254
  checkbox: false,
6070
6255
  minExpandLevel: 0,
6071
6256
  emptyChildListExpandable: false,
6072
- // updateThrottleWait: 200,
6073
6257
  skeleton: false,
6258
+ autoCollapse: false,
6259
+ adjustHeight: true,
6074
6260
  connectTopBreadcrumb: null,
6261
+ columnsFilterable: false,
6262
+ columnsMenu: false,
6263
+ columnsResizable: false,
6264
+ columnsSortable: false,
6075
6265
  selectMode: "multi", // SelectModeType
6266
+ scrollIntoViewOnExpandClick: true,
6267
+ // --- Extensions (actually set by exensions on init)
6268
+ dnd: unsafeCast(null),
6269
+ edit: unsafeCast(null),
6270
+ filter: unsafeCast(null),
6076
6271
  // --- KeyNav ---
6077
- navigationModeOption: null, // NavModeEnum,
6272
+ navigationModeOption: unsafeCast(null),
6078
6273
  quicksearch: true,
6079
6274
  // --- Events ---
6080
- iconBadge: null,
6081
- change: null,
6082
- // enhanceTitle: null,
6083
- error: null,
6084
- receive: null,
6275
+ // iconBadge: null,
6276
+ // change: null,
6277
+ // ...
6085
6278
  // --- Strings ---
6086
6279
  strings: {
6087
6280
  loadError: "Error",
@@ -6092,7 +6285,9 @@
6092
6285
  noMatch: "No results",
6093
6286
  matchIndex: "${match} of ${matches}",
6094
6287
  },
6095
- }, options));
6288
+ }, options);
6289
+ const opts = initOptions;
6290
+ this.options = opts;
6096
6291
  const readyDeferred = new Deferred();
6097
6292
  this.ready = readyDeferred.promise();
6098
6293
  let readyOk = false;
@@ -6119,7 +6314,8 @@
6119
6314
  this._callEvent("init", { error: err });
6120
6315
  }
6121
6316
  });
6122
- this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
6317
+ this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence;
6318
+ delete initOptions.id;
6123
6319
  this.root = new WbSystemRoot(this);
6124
6320
  this._registerExtension(new KeynavExtension(this));
6125
6321
  this._registerExtension(new EditExtension(this));
@@ -6129,19 +6325,20 @@
6129
6325
  this._registerExtension(new LoggerExtension(this));
6130
6326
  this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {});
6131
6327
  // --- Evaluate options
6132
- this.columns = opts.columns;
6133
- delete opts.columns;
6328
+ this.columns = initOptions.columns || [];
6329
+ delete initOptions.columns;
6134
6330
  if (!this.columns || !this.columns.length) {
6135
6331
  const title = typeof opts.header === "string" ? opts.header : this.id;
6136
6332
  this.columns = [{ id: "*", title: title, width: "*" }];
6137
6333
  }
6138
- if (opts.types) {
6139
- this.setTypes(opts.types, true);
6334
+ if (initOptions.types) {
6335
+ this.setTypes(initOptions.types, true);
6140
6336
  }
6141
- delete opts.types;
6337
+ delete initOptions.types;
6142
6338
  // --- Create Markup
6143
- this.element = elemFromSelector(opts.element);
6144
- assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
6339
+ this.element = elemFromSelector(initOptions.element);
6340
+ assert(!!this.element, `Invalid 'element' option: ${initOptions.element}`);
6341
+ delete initOptions.element;
6145
6342
  this.element.classList.add("wunderbaum");
6146
6343
  if (!this.element.getAttribute("tabindex")) {
6147
6344
  this.element.tabIndex = 0;
@@ -6217,11 +6414,11 @@
6217
6414
  }
6218
6415
  });
6219
6416
  // --- Load initial data
6220
- if (opts.source) {
6417
+ if (initOptions.source) {
6221
6418
  if (opts.showSpinner) {
6222
6419
  this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
6223
6420
  }
6224
- this.load(opts.source)
6421
+ this.load(initOptions.source)
6225
6422
  .then(() => {
6226
6423
  // The source may have defined columns, so we may adjust the nav mode
6227
6424
  if (opts.navigationModeOption == null) {
@@ -6254,15 +6451,18 @@
6254
6451
  // has a wrong value at start???
6255
6452
  this.update(ChangeType.any);
6256
6453
  // --- Bind listeners
6257
- this.element.addEventListener("scroll", (e) => {
6258
- // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6259
- this.update(ChangeType.scroll);
6260
- });
6454
+ this._registerEventHandlers();
6261
6455
  this.resizeObserver = new ResizeObserver((entries) => {
6262
6456
  // this.log("ResizeObserver: Size changed", entries);
6263
6457
  this.update(ChangeType.resize);
6264
6458
  });
6265
6459
  this.resizeObserver.observe(this.element);
6460
+ }
6461
+ _registerEventHandlers() {
6462
+ this.element.addEventListener("scroll", (e) => {
6463
+ // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6464
+ this.update(ChangeType.scroll);
6465
+ });
6266
6466
  onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
6267
6467
  var _a, _b;
6268
6468
  const info = Wunderbaum.getEventInfo(e);
@@ -6278,9 +6478,6 @@
6278
6478
  const node = info.node;
6279
6479
  const mouseEvent = e;
6280
6480
  // this.log("click", info);
6281
- // if (this._selectRange(info) === false) {
6282
- // return;
6283
- // }
6284
6481
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
6285
6482
  this.lastClickTime = Date.now();
6286
6483
  return false;
@@ -6299,20 +6496,22 @@
6299
6496
  (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) {
6300
6497
  node.startEditTitle();
6301
6498
  }
6302
- if (info.colIdx >= 0) {
6303
- node.setActive(true, { colIdx: info.colIdx, event: e });
6304
- }
6305
- else {
6306
- node.setActive(true, { event: e });
6307
- }
6308
6499
  if (info.region === NodeRegion.expander) {
6309
6500
  node.setExpanded(!node.isExpanded(), {
6310
- scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
6501
+ scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
6311
6502
  });
6312
6503
  }
6313
6504
  else if (info.region === NodeRegion.checkbox) {
6314
6505
  node.toggleSelected();
6315
6506
  }
6507
+ else {
6508
+ if (info.colIdx >= 0) {
6509
+ node.setActive(true, { colIdx: info.colIdx, event: e });
6510
+ }
6511
+ else {
6512
+ node.setActive(true, { event: e });
6513
+ }
6514
+ }
6316
6515
  }
6317
6516
  this.lastClickTime = Date.now();
6318
6517
  });
@@ -6348,7 +6547,7 @@
6348
6547
  const targetNode = Wunderbaum.getNode(e);
6349
6548
  this._callEvent("focus", { flag: flag, event: e });
6350
6549
  if (flag && this.isRowNav() && !this.isEditingTitle()) {
6351
- if (opts.navigationModeOption === NavModeEnum.row) {
6550
+ if (this.options.navigationModeOption === NavModeEnum.row) {
6352
6551
  targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive();
6353
6552
  }
6354
6553
  else {
@@ -6415,11 +6614,12 @@
6415
6614
  }
6416
6615
  /**
6417
6616
  * Return the icon-function -> icon-definition mapping.
6617
+ * @deprecated Use {@link Wunderbaum.iconMaps}
6418
6618
  */
6419
6619
  get iconMap() {
6420
6620
  const map = this.options.iconMap;
6421
6621
  if (typeof map === "string") {
6422
- return iconMaps[map];
6622
+ return defaultIconMaps[map];
6423
6623
  }
6424
6624
  return map;
6425
6625
  }
@@ -6472,7 +6672,38 @@
6472
6672
  ext.init();
6473
6673
  }
6474
6674
  }
6475
- /** Add node to tree's bookkeeping data structures. */
6675
+ /**
6676
+ * Calculate a *stable*, unique key for a node from its refKey (or title).
6677
+ * We also add information from the parent, because a refKey may occur multiple
6678
+ * times in a tree (but not as child of the same parent).
6679
+ * @internal
6680
+ */
6681
+ _calculateKey(data, parent) {
6682
+ if (data.key) {
6683
+ // Always use an explicitly passed key
6684
+ return data.key;
6685
+ }
6686
+ // Auto-keys are optional, use a monotonic counter by default:
6687
+ if (!this.options.autoKeys) {
6688
+ return "" + ++WunderbaumNode.sequence;
6689
+ }
6690
+ // Add the parent's key to the hash. Assuming this was generated by the
6691
+ // same algorithm, this should incorporate the whole path:
6692
+ const s = (parent ? parent.key : "") + (data.refKey || data.title);
6693
+ // 32-bit has a high probability of collisions, so we pump up to 64-bit
6694
+ // https://security.stackexchange.com/q/209882/207588
6695
+ const h1 = murmurHash3(s, true);
6696
+ let key = "id_" + h1 + murmurHash3(h1 + s, true);
6697
+ // Check for collisions
6698
+ // (Most likely if the same title occurs multiple in the same parent).
6699
+ const existingNode = this.keyMap.get(key);
6700
+ if (existingNode) {
6701
+ key += "." + ++Wunderbaum.sequence;
6702
+ this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data);
6703
+ }
6704
+ return key;
6705
+ }
6706
+ /** Add node to tree's bookkeeping data structures. @internal */
6476
6707
  _registerNode(node) {
6477
6708
  const key = node.key;
6478
6709
  assert(key != null, `Missing key: '${node}'.`);
@@ -6489,7 +6720,7 @@
6489
6720
  }
6490
6721
  }
6491
6722
  }
6492
- /** Remove node from tree's bookkeeping data structures. */
6723
+ /** Remove node from tree's bookkeeping data structures. @internal */
6493
6724
  _unregisterNode(node) {
6494
6725
  // Remove refKey reference from map (if any)
6495
6726
  const rk = node.refKey;
@@ -6608,6 +6839,16 @@
6608
6839
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6609
6840
  return this._getNodeByRowIdx(bottomIdx);
6610
6841
  }
6842
+ /** Return preceding visible node in the viewport. */
6843
+ _getPrevNodeInView(node, ofs = 1) {
6844
+ this.visitRows((n) => {
6845
+ node = n;
6846
+ if (ofs-- <= 0) {
6847
+ return false;
6848
+ }
6849
+ }, { reverse: true, start: node || this.getActiveNode() });
6850
+ return node;
6851
+ }
6611
6852
  /** Return following visible node in the viewport. */
6612
6853
  _getNextNodeInView(node, options) {
6613
6854
  let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
@@ -6859,22 +7100,39 @@
6859
7100
  /** Run code, but defer rendering of viewport until done.
6860
7101
  *
6861
7102
  * ```js
6862
- * tree.runWithDeferredUpdate(() => {
6863
- * return someFuncThatWouldUpdateManyNodes();
7103
+ * const res = tree.runWithDeferredUpdate(() => {
7104
+ * return someFunctionThatWouldUpdateManyNodes();
6864
7105
  * });
6865
7106
  * ```
6866
7107
  */
6867
- runWithDeferredUpdate(func, hint = null) {
7108
+ runWithDeferredUpdate(func) {
6868
7109
  try {
6869
7110
  this.enableUpdate(false);
6870
7111
  const res = func();
6871
- assert(!(res instanceof Promise), `Promise return not allowed: ${res}`);
7112
+ assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`);
6872
7113
  return res;
6873
7114
  }
6874
7115
  finally {
6875
7116
  this.enableUpdate(true);
6876
7117
  }
6877
7118
  }
7119
+ /** Run code, but defer rendering of viewport until done.
7120
+ *
7121
+ * ```js
7122
+ * const res = await tree.runWithDeferredUpdate(async () => {
7123
+ * return someAsyncFunctionThatWouldUpdateManyNodes();
7124
+ * });
7125
+ * ```
7126
+ */
7127
+ async runWithDeferredUpdateAsync(func) {
7128
+ try {
7129
+ this.enableUpdate(false);
7130
+ return await func();
7131
+ }
7132
+ finally {
7133
+ this.enableUpdate(true);
7134
+ }
7135
+ }
6878
7136
  /** Recursively expand all expandable nodes (triggers lazy load if needed). */
6879
7137
  async expandAll(flag = true, options) {
6880
7138
  await this.root.expandAll(flag, options);
@@ -6894,6 +7152,17 @@
6894
7152
  getSelectedNodes(stopOnParents = false) {
6895
7153
  return this.root.getSelectedNodes(stopOnParents);
6896
7154
  }
7155
+ /**
7156
+ * Return an array of refKey values.
7157
+ *
7158
+ * RefKeys are unique identifiers for a node data, and are used to identify
7159
+ * clones.
7160
+ * If more than one node has the same refKey, it is only returned once.
7161
+ * @param selected if true, only return refKeys of selected nodes.
7162
+ */
7163
+ getRefKeys(selected = false) {
7164
+ return this.root.getRefKeys(selected);
7165
+ }
6897
7166
  /*
6898
7167
  * Return an array of selected nodes.
6899
7168
  */
@@ -7175,6 +7444,18 @@
7175
7444
  format(name_cb, connectors) {
7176
7445
  return this.root.format(name_cb, connectors);
7177
7446
  }
7447
+ /**
7448
+ * Always returns null (so a tree instance behaves as `tree.root`).
7449
+ */
7450
+ get parent() {
7451
+ return null;
7452
+ }
7453
+ /**
7454
+ * Return a list of top-level nodes.
7455
+ */
7456
+ get children() {
7457
+ return this.root.children || [];
7458
+ }
7178
7459
  /**
7179
7460
  * Return the active cell (`span.wb-col`) of the currently active node or null.
7180
7461
  */
@@ -7293,7 +7574,7 @@
7293
7574
  }
7294
7575
  /** Return true if any node title or grid cell is currently beeing edited.
7295
7576
  *
7296
- * See also {@link Wunderbaum.isEditingTitle}.
7577
+ * See also {@link isEditingTitle}.
7297
7578
  */
7298
7579
  isEditing() {
7299
7580
  const focusElem = this.nodeListElement.querySelector("input:focus,select:focus");
@@ -7301,7 +7582,7 @@
7301
7582
  }
7302
7583
  /** Return true if any node is currently in edit-title mode.
7303
7584
  *
7304
- * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
7585
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
7305
7586
  */
7306
7587
  isEditingTitle() {
7307
7588
  return this._callMethod("edit.isEditingTitle");
@@ -7321,7 +7602,7 @@
7321
7602
  return res;
7322
7603
  }
7323
7604
  /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
7324
- * @see {@link Wunderbaum.logDebug}
7605
+ * @see {@link logDebug}
7325
7606
  */
7326
7607
  log(...args) {
7327
7608
  if (this.options.debugLevel >= 4) {
@@ -7330,7 +7611,7 @@
7330
7611
  }
7331
7612
  /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
7332
7613
  * and browser console level includes debug/verbose messages.
7333
- * @see {@link Wunderbaum.log}
7614
+ * @see {@link log}
7334
7615
  */
7335
7616
  logDebug(...args) {
7336
7617
  if (this.options.debugLevel >= 4) {
@@ -7368,6 +7649,19 @@
7368
7649
  console.warn(this.toString(), ...args); // eslint-disable-line no-console
7369
7650
  }
7370
7651
  }
7652
+ /** Emit a warning for deprecated methods. @internal */
7653
+ logDeprecate(method, options) {
7654
+ if (this.options.debugLevel >= 2) {
7655
+ let msg = `${this}: ${method} is deprecated`;
7656
+ if (options === null || options === void 0 ? void 0 : options.since) {
7657
+ msg += ` since ${options.since}`;
7658
+ }
7659
+ if (options === null || options === void 0 ? void 0 : options.hint) {
7660
+ msg += ` (${options.since})`;
7661
+ }
7662
+ console.warn(msg + "."); // eslint-disable-line no-console
7663
+ }
7664
+ }
7371
7665
  /** Reset column widths to default. @since 0.10.0 */
7372
7666
  resetColumns() {
7373
7667
  this.columns.forEach((col) => {
@@ -7536,46 +7830,64 @@
7536
7830
  this._focusNode = node;
7537
7831
  }
7538
7832
  /** Return the current selection/expansion/activation status. @experimental */
7539
- getState(options) {
7833
+ getState(options = {}) {
7540
7834
  var _a, _b;
7541
- let expandedKeys = undefined;
7542
- if (options.expandedKeys !== false) {
7543
- expandedKeys = [];
7835
+ const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options;
7836
+ const expandSet = new Set();
7837
+ if (expandedKeys) {
7544
7838
  for (const node of this) {
7545
- if (node.expanded) {
7546
- expandedKeys.push(node.key);
7839
+ if (node.isExpanded() && node.hasChildren()) {
7840
+ expandSet.add(node.key);
7547
7841
  }
7548
7842
  }
7549
7843
  }
7844
+ // Parents of active node are always expanded
7845
+ if (activeKey && this.activeNode) {
7846
+ this.activeNode.visitParents((n) => {
7847
+ if (n.parent) {
7848
+ expandSet.add(n.key);
7849
+ }
7850
+ }, false);
7851
+ }
7550
7852
  const state = {
7853
+ expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
7551
7854
  activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
7552
7855
  activeColIdx: this.activeColIdx,
7553
- selectedKeys: options.selectedKeys === false
7554
- ? undefined
7555
- : this.getSelectedNodes().flatMap((n) => n.key),
7556
- expandedKeys: expandedKeys,
7856
+ selectedKeys: selectedKeys
7857
+ ? this.getSelectedNodes().flatMap((n) => n.key)
7858
+ : undefined,
7557
7859
  };
7558
7860
  return state;
7559
7861
  }
7560
7862
  /** Apply selection/expansion/activation status. @experimental */
7561
- setState(state, options) {
7562
- this.runWithDeferredUpdate(() => {
7863
+ async setState(state, options = {}) {
7864
+ const { expandLazy = true } = options;
7865
+ return this.runWithDeferredUpdateAsync(async () => {
7563
7866
  var _a, _b;
7564
- if (state.selectedKeys) {
7565
- this.selectAll(false);
7566
- for (const key of state.selectedKeys) {
7567
- (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setSelected(true);
7867
+ if (state.expandedKeys && state.expandedKeys.length) {
7868
+ if (expandLazy) {
7869
+ // Expand all keys recursively, even if they are not in the tree yet
7870
+ await this._loadLazyNodes(state.expandedKeys, {
7871
+ expand: true,
7872
+ noEvents: true,
7873
+ });
7568
7874
  }
7569
- }
7570
- if (state.expandedKeys) {
7571
- for (const key of state.expandedKeys) {
7572
- (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setExpanded(true);
7875
+ else {
7876
+ for (const key of state.expandedKeys) {
7877
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true);
7878
+ }
7573
7879
  }
7574
7880
  }
7575
7881
  if (state.activeKey) {
7576
7882
  this.setActiveNode(state.activeKey);
7577
7883
  }
7578
- if (state.activeColIdx != null) {
7884
+ if (state.selectedKeys) {
7885
+ this.selectAll(false);
7886
+ for (const key of state.selectedKeys) {
7887
+ (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true);
7888
+ }
7889
+ }
7890
+ if (this.isCellNav() && state.activeColIdx != null) {
7579
7891
  this.setColumn(state.activeColIdx);
7580
7892
  }
7581
7893
  });
@@ -7736,18 +8048,33 @@
7736
8048
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
7737
8049
  * (defaults to sorting by title).
7738
8050
  * @param {boolean} deep pass true to sort all descendant nodes recursively
8051
+ * @deprecated use {@link sort}
7739
8052
  */
7740
8053
  sortChildren(cmp = nodeTitleSorter, deep = false) {
7741
- this.root.sortChildren(cmp, deep);
8054
+ this.logDeprecate("sortChildren()", { since: "0.14.0" });
8055
+ return this.sort({
8056
+ cmp: cmp ? cmp : undefined,
8057
+ deep: deep,
8058
+ propName: "title",
8059
+ });
7742
8060
  }
7743
8061
  /**
7744
8062
  * Convenience method to implement column sorting.
7745
8063
  * @see {@link WunderbaumNode.sortByProperty}.
7746
8064
  * @since 0.11.0
8065
+ * @deprecated use {@link sort}
7747
8066
  */
7748
8067
  sortByProperty(options) {
8068
+ this.logDeprecate("sortByProperty()", { since: "0.14.0" });
7749
8069
  this.root.sortByProperty(options);
7750
8070
  }
8071
+ /**
8072
+ * Sort nodes list by title or custom criteria.
8073
+ * @since 0.14.0
8074
+ */
8075
+ sort(options) {
8076
+ this.root.sort(options);
8077
+ }
7751
8078
  /** Convert tree to an array of plain objects.
7752
8079
  *
7753
8080
  * @param callback is called for every node, in order to allow
@@ -7999,12 +8326,10 @@
7999
8326
  iconElem = document.createElement("i");
8000
8327
  iconElem.className = "wb-icon";
8001
8328
  }
8002
- else if (icon.indexOf("<") >= 0) {
8003
- // HTML
8329
+ else if (TEST_HTML.test(icon)) {
8004
8330
  iconElem = elemFromHtml(icon);
8005
8331
  }
8006
- else if (TEST_IMG.test(icon)) {
8007
- // Image URL
8332
+ else if (TEST_FILE_PATH.test(icon)) {
8008
8333
  iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
8009
8334
  }
8010
8335
  else {
@@ -8242,7 +8567,8 @@
8242
8567
  }
8243
8568
  /**
8244
8569
  * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
8245
- * @see {@link IterableIterator<WunderbaumNode>}, {@link WunderbaumNode.visit}.
8570
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
8571
+ * @see {@link WunderbaumNode.visit}.
8246
8572
  *
8247
8573
  * @param {function} callback the callback function.
8248
8574
  * Return false to stop iteration, return "skip" to skip this node and
@@ -8385,11 +8711,71 @@
8385
8711
  *
8386
8712
  * Previous data is cleared. Note that also column- and type defintions may
8387
8713
  * be passed with the `source` object.
8714
+ * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request
8715
+ * and restore the previous state.
8388
8716
  */
8389
- load(source) {
8717
+ async load(source) {
8390
8718
  this.clear();
8719
+ this._initialSource = source;
8391
8720
  return this.root.load(source);
8392
8721
  }
8722
+ /** Reload the tree and optionally restore state.
8723
+ * Source defaults to last ajax url if any.
8724
+ * Restoring the active node requires stable keys
8725
+ * @see {@link WunderbaumOptions.autoKeys}
8726
+ * @see {@link Wunderbaum.load}
8727
+ * @experimental
8728
+ */
8729
+ async reload(options = {}) {
8730
+ const { source = this._initialSource, reactivate = true } = options;
8731
+ if (!source) {
8732
+ this.logWarn("No previous ajax source to reload.");
8733
+ return;
8734
+ }
8735
+ if (!reactivate) {
8736
+ return this.load(source);
8737
+ }
8738
+ const state = this.getState();
8739
+ await this.load(source);
8740
+ return this.setState(state);
8741
+ }
8742
+ /**
8743
+ * Make sure that all nodes in the given keyList are accessible.
8744
+ * This may include loading lazy parent nodes.
8745
+ * Recursively load (and optionally expand) all requested node paths.
8746
+ */
8747
+ async _loadLazyNodes(keyList, options = {}) {
8748
+ const { expand = true } = options;
8749
+ const keySet = new Set(keyList);
8750
+ // Make sure that all parent nodes are loaded (and expand if requested)
8751
+ while (keySet.size > 0) {
8752
+ const pendingNodes = [];
8753
+ const curSet = new Set(keySet);
8754
+ for (const key of curSet) {
8755
+ const node = this.findKey(key);
8756
+ if (!node) {
8757
+ continue; // key not yet found (need to load lazy parent?)
8758
+ }
8759
+ keySet.delete(key);
8760
+ if (expand) {
8761
+ pendingNodes.push(node.setExpanded(true));
8762
+ }
8763
+ else if (node.isUnloaded()) {
8764
+ pendingNodes.push(node.loadLazy());
8765
+ }
8766
+ if (node._rowElem) {
8767
+ node._render(); // show spinner even is update is suppressed
8768
+ }
8769
+ }
8770
+ if (pendingNodes.length === 0) {
8771
+ // will not load any more nodes, so if if there are still keys
8772
+ // left in the set, we will never find them
8773
+ this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet);
8774
+ break;
8775
+ }
8776
+ await Promise.allSettled(pendingNodes);
8777
+ }
8778
+ }
8393
8779
  /**
8394
8780
  * Disable render requests during operations that would trigger many updates.
8395
8781
  *
@@ -8487,9 +8873,21 @@
8487
8873
  }
8488
8874
  Wunderbaum.sequence = 0;
8489
8875
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8490
- Wunderbaum.version = "v0.13.0"; // Set to semver by 'grunt release'
8876
+ Wunderbaum.version = "v0.14.0"; // Set to semver by 'grunt release'
8491
8877
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8492
8878
  Wunderbaum.util = util;
8879
+ /** A map of default iconMaps.
8880
+ * May be used as default, when passing partial icon definition maps:
8881
+ * ```js
8882
+ * const tree = new mar10.Wunderbaum({
8883
+ * ...
8884
+ * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
8885
+ * folder: "bi bi-archive",
8886
+ * }),
8887
+ * });
8888
+ * ```
8889
+ */
8890
+ Wunderbaum.iconMaps = defaultIconMaps;
8493
8891
 
8494
8892
  exports.Wunderbaum = Wunderbaum;
8495
8893