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.
@@ -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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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.1, Sun, 22 Mar 2026 05:52:05 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
@@ -3257,7 +3336,9 @@
3257
3336
  : 0;
3258
3337
  data.colspan != null ? (this.colspan = !!data.colspan) : 0;
3259
3338
  // Selection
3260
- data.checkbox != null ? intToBool(data.checkbox) : 0;
3339
+ data.checkbox != null
3340
+ ? (this.checkbox = intToBool(data.checkbox))
3341
+ : 0;
3261
3342
  data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0;
3262
3343
  data.selected != null ? (this.selected = !!data.selected) : 0;
3263
3344
  data.unselectable != null ? (this.unselectable = !!data.unselectable) : 0;
@@ -3361,8 +3442,14 @@
3361
3442
  const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel;
3362
3443
  for (const child of nodeData) {
3363
3444
  const subChildren = child.children;
3445
+ // Remove children property from source data because it should not be
3446
+ // passed to the constructor of WunderbaumNode:
3364
3447
  delete child.children;
3365
3448
  const n = new WunderbaumNode(tree, this, child);
3449
+ // Set `children` property again, so it can be used in `reload()`
3450
+ if (subChildren != null) {
3451
+ child.children = subChildren;
3452
+ }
3366
3453
  if (forceExpand && !n.isUnloaded()) {
3367
3454
  n.expanded = true;
3368
3455
  }
@@ -3778,15 +3865,12 @@
3778
3865
  }
3779
3866
  return l;
3780
3867
  }
3781
- /** Return a string representing the hierachical node path, e.g. "a/b/c".
3868
+ /** Return a string representing the hierarchical node path, e.g. "a/b/c".
3782
3869
  * @param includeSelf
3783
3870
  * @param part property name or callback
3784
3871
  * @param separator
3785
3872
  */
3786
3873
  getPath(includeSelf = true, part = "title", separator = "/") {
3787
- // includeSelf = includeSelf !== false;
3788
- // part = part || "title";
3789
- // separator = separator || "/";
3790
3874
  let val;
3791
3875
  const path = [];
3792
3876
  const isFunc = typeof part === "function";
@@ -3801,7 +3885,7 @@
3801
3885
  }, includeSelf);
3802
3886
  return path.join(separator);
3803
3887
  }
3804
- /** Return the preceeding node (under the same parent) or null. */
3888
+ /** Return the preceding node (under the same parent) or null. */
3805
3889
  getPrevSibling() {
3806
3890
  const ac = this.parent.children;
3807
3891
  const idx = ac.indexOf(this);
@@ -3830,7 +3914,7 @@
3830
3914
  hasClass(className) {
3831
3915
  return this.classes ? this.classes.has(className) : false;
3832
3916
  }
3833
- /** Return true if node ist the currently focused node. @since 0.9.0 */
3917
+ /** Return true if node is the currently focused node. @since 0.9.0 */
3834
3918
  hasFocus() {
3835
3919
  return this.tree.focusNode === this;
3836
3920
  }
@@ -3885,7 +3969,7 @@
3885
3969
  * an expand operation is currently possible.
3886
3970
  */
3887
3971
  isExpandable(andCollapsed = false) {
3888
- // `false` is never expandable (unoffical)
3972
+ // `false` is never expandable (unofficial)
3889
3973
  if ((andCollapsed && this.expanded) || this.children === false) {
3890
3974
  return false;
3891
3975
  }
@@ -3948,11 +4032,11 @@
3948
4032
  isPartsel() {
3949
4033
  return !this.selected && !!this._partsel;
3950
4034
  }
3951
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4035
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3952
4036
  isRadio() {
3953
4037
  return !!this.parent.radiogroup || this.getOption("checkbox") === "radio";
3954
4038
  }
3955
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4039
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3956
4040
  isRendered() {
3957
4041
  return !!this._rowElem;
3958
4042
  }
@@ -4707,9 +4791,9 @@
4707
4791
  const typeInfo = this.type ? tree.types[this.type] : null;
4708
4792
  const rowDiv = this._rowElem;
4709
4793
  // 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");
4794
+ const nodeSpan = rowDiv.querySelector("span.wb-node");
4795
+ const expanderElem = nodeSpan.querySelector("i.wb-expander");
4796
+ const checkboxElem = nodeSpan.querySelector("i.wb-checkbox");
4713
4797
  const rowClasses = ["wb-row"];
4714
4798
  this.expanded ? rowClasses.push("wb-expanded") : 0;
4715
4799
  this.lazy ? rowClasses.push("wb-lazy") : 0;
@@ -4734,7 +4818,7 @@
4734
4818
  if (typeInfo && typeInfo.classes) {
4735
4819
  rowDiv.classList.add(...typeInfo.classes);
4736
4820
  }
4737
- if (expanderSpan) {
4821
+ if (expanderElem) {
4738
4822
  let image = null;
4739
4823
  if (this._isLoading) {
4740
4824
  image = iconMap.loading;
@@ -4751,16 +4835,20 @@
4751
4835
  image = iconMap.expanderLazy;
4752
4836
  }
4753
4837
  if (image == null) {
4754
- expanderSpan.classList.add("wb-indent");
4838
+ expanderElem.className = "wb-expander";
4839
+ expanderElem.classList.add("wb-indent");
4840
+ }
4841
+ else if (TEST_HTML.test(image)) {
4842
+ expanderElem.replaceWith(elemFromHtml(image));
4755
4843
  }
4756
- else if (TEST_IMG.test(image)) {
4757
- expanderSpan.style.backgroundImage = `url('${image}')`;
4844
+ else if (TEST_FILE_PATH.test(image)) {
4845
+ expanderElem.style.backgroundImage = `url('${image}')`;
4758
4846
  }
4759
4847
  else {
4760
- expanderSpan.className = "wb-expander " + image;
4848
+ expanderElem.className = "wb-expander " + image;
4761
4849
  }
4762
4850
  }
4763
- if (checkboxSpan) {
4851
+ if (checkboxElem) {
4764
4852
  let cbclass = "wb-checkbox ";
4765
4853
  if (this.isRadio()) {
4766
4854
  cbclass += "wb-radio ";
@@ -4784,7 +4872,7 @@
4784
4872
  cbclass += iconMap.checkUnchecked;
4785
4873
  }
4786
4874
  }
4787
- checkboxSpan.className = cbclass;
4875
+ checkboxElem.className = cbclass;
4788
4876
  }
4789
4877
  // Fix active cell in cell-nav mode
4790
4878
  if (!opts.isNew) {
@@ -4794,9 +4882,9 @@
4794
4882
  colSpan.classList.remove("wb-error", "wb-invalid");
4795
4883
  }
4796
4884
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4797
- const iconSpan = nodeElem.querySelector("i.wb-icon");
4885
+ const iconSpan = nodeSpan.querySelector("i.wb-icon");
4798
4886
  if (iconSpan) {
4799
- this._createIcon(nodeElem, iconSpan, !expanderSpan);
4887
+ this._createIcon(nodeSpan, iconSpan, !expanderElem);
4800
4888
  }
4801
4889
  }
4802
4890
  // Adjust column width
@@ -5101,6 +5189,32 @@
5101
5189
  setKey(key, refKey) {
5102
5190
  throw new Error("Not yet implemented");
5103
5191
  }
5192
+ // /**
5193
+ // * Calculate a *stable*, unique key for this node from its refKey (or title).
5194
+ // * We also add information from the parent, because a refKey may occur multiple
5195
+ // * times in a tree.
5196
+ // */
5197
+ // calcUniqueKey() {
5198
+ // // Assuming that the parent's key was calculated the same way, we implicitly
5199
+ // // involve the whole refKey-path:
5200
+ // const s = this.key + (this.refKey || this.title);
5201
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5202
+ // // https://security.stackexchange.com/q/209882/207588
5203
+ // const h1 = util.murmurHash3(s, true);
5204
+ // return "id_" + h1 + util.murmurHash3(h1 + s, true);
5205
+ // // const l = [];
5206
+ // // // eslint-disable-next-line @typescript-eslint/no-this-alias
5207
+ // // let node: WunderbaumNode = this;
5208
+ // // while (node.parent) {
5209
+ // // l.unshift(node.refKey || node.key);
5210
+ // // node = node.parent;
5211
+ // // }
5212
+ // // const path = l.join("/");
5213
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5214
+ // // https://security.stackexchange.com/q/209882/207588
5215
+ // // const h1 = util.murmurHash3(path, true);
5216
+ // // return "id_" + h1 + util.murmurHash3(h1 + path, true);
5217
+ // }
5104
5218
  /**
5105
5219
  * Trigger a repaint, typically after a status or data change.
5106
5220
  *
@@ -5132,6 +5246,23 @@
5132
5246
  });
5133
5247
  return nodeList;
5134
5248
  }
5249
+ /**
5250
+ * Return an array of refKey values.
5251
+ *
5252
+ * RefKeys are unique identifiers for a node data, and are used to identify
5253
+ * clones.
5254
+ * If more than one node has the same refKey, it is only returned once.
5255
+ * @param selected if true, only return refKeys of selected nodes.
5256
+ */
5257
+ getRefKeys(selected = false) {
5258
+ const refKeys = new Set();
5259
+ this.visit((node) => {
5260
+ if (node.refKey != null && (!selected || node.selected)) {
5261
+ refKeys.add(node.refKey);
5262
+ }
5263
+ });
5264
+ return Array.from(refKeys);
5265
+ }
5135
5266
  /** Toggle the check/uncheck state. */
5136
5267
  toggleSelected(options) {
5137
5268
  let flag = this.isSelected();
@@ -5311,9 +5442,11 @@
5311
5442
  if (selectMode === "hier") {
5312
5443
  this.fixSelection3AfterClick();
5313
5444
  }
5314
- else if (selectMode === "single") {
5445
+ else if (selectMode === "single" && flag) {
5315
5446
  tree.visit((n) => {
5316
- n.selected = false;
5447
+ if (n !== this) {
5448
+ n.selected = false;
5449
+ }
5317
5450
  });
5318
5451
  }
5319
5452
  }
@@ -5415,30 +5548,16 @@
5415
5548
  this.tooltip = tooltip;
5416
5549
  this.update();
5417
5550
  }
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
5551
  /**
5433
5552
  * Sort child list by title or custom criteria.
5434
5553
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
5435
5554
  * (defaults to sorting by title).
5436
5555
  * @param {boolean} deep pass true to sort all descendant nodes recursively
5556
+ * @deprecated use {@link sort}
5437
5557
  */
5438
5558
  sortChildren(cmp = nodeTitleSorter, deep = false) {
5439
- this._sortChildren(cmp || nodeTitleSorter, deep);
5440
- this.tree.update(ChangeType.structure);
5441
- // this.triggerModify("sort"); // TODO
5559
+ this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" });
5560
+ return this.sort({ cmp: cmp ? cmp : undefined, deep: deep });
5442
5561
  }
5443
5562
  /**
5444
5563
  * Renumber nodes `_nativeIndex`. This is useful to allow to restore the
@@ -5460,74 +5579,142 @@
5460
5579
  /**
5461
5580
  * Convenience method to implement column sorting.
5462
5581
  * @since 0.11.0
5582
+ * @deprecated use {@link sort}
5463
5583
  */
5464
5584
  sortByProperty(options) {
5465
- var _a, _b, _c;
5466
- const { caseInsensitive = true, deep = true, nativeOrderPropName = "_nativeIndex", updateColInfo = false, } = options;
5467
- let order;
5468
- let colDef;
5585
+ this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" });
5586
+ return this.sort(options);
5587
+ }
5588
+ /**
5589
+ * Implement column sorting.
5590
+ * @since 0.14.0
5591
+ */
5592
+ sort(options) {
5593
+ const tree = this.tree;
5594
+ let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined,
5595
+ // Support click on column sort header:
5596
+ updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options;
5597
+ propName !== null && propName !== void 0 ? propName : (propName = colId);
5598
+ if (propName === "*") {
5599
+ propName = "title";
5600
+ }
5601
+ const isFolder = tree.options.sortFoldersFirst === true
5602
+ ? (node) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER
5603
+ : tree.options.sortFoldersFirst;
5469
5604
  if (updateColInfo) {
5470
- colDef = this.tree["_columnsById"][options.colId];
5605
+ const colDef = this.tree["_columnsById"][options.colId];
5471
5606
  assert(colDef, `Invalid colId specified: ${options.colId}`);
5472
- order =
5473
- (_a = options.order) !== null && _a !== void 0 ? _a : rotate(colDef.sortOrder, ["asc", "desc", undefined]);
5607
+ order !== null && order !== void 0 ? order : (order = rotate(colDef.sortOrder, ["asc", "desc", undefined]));
5474
5608
  for (const col of this.tree.columns) {
5475
5609
  col.sortOrder = col === colDef ? order : undefined;
5476
5610
  }
5611
+ if (order === undefined) {
5612
+ propName = nativeOrderPropName;
5613
+ order = "asc";
5614
+ }
5477
5615
  this.tree.update(ChangeType.colStructure);
5478
5616
  }
5479
5617
  else {
5480
- order = (_b = options.order) !== null && _b !== void 0 ? _b : "asc";
5618
+ propName !== null && propName !== void 0 ? propName : (propName = "title");
5619
+ order !== null && order !== void 0 ? order : (order = "asc");
5620
+ }
5621
+ this.logDebug(`sort(), propName=${propName}, ${order}`, options);
5622
+ assert(propName || cmp || key, "No `propName` or `key` specified");
5623
+ // Define a key callback from the parameters we have
5624
+ if (key == null && cmp == null) {
5625
+ key = (node) => {
5626
+ let val;
5627
+ if (NODE_DICT_PROPS.has(propName)) {
5628
+ val = node[propName];
5629
+ }
5630
+ else {
5631
+ val = node.data[propName];
5632
+ }
5633
+ if (caseInsensitive && typeof val === "string") {
5634
+ val = val.toLowerCase();
5635
+ }
5636
+ return val;
5637
+ };
5481
5638
  }
5482
- let propName = (_c = options.propName) !== null && _c !== void 0 ? _c : (options.colId || "");
5483
- if (propName === "*") {
5484
- propName = "title";
5639
+ // Define a compare callback that uses the key callback
5640
+ if (cmp) {
5641
+ assert(!key, "`key` and `cmp` are mutually exclusive");
5642
+ tree.logDeprecate("SortOptions.cmp", {
5643
+ since: "0.14.0",
5644
+ hint: "use the `key` callback instead",
5645
+ });
5485
5646
  }
5486
- if (order == null) {
5487
- propName = nativeOrderPropName;
5488
- order = "asc";
5647
+ else {
5648
+ if (options.propName || options.caseInsensitive) {
5649
+ tree.logWarn("sort(): ignoring propName, caseInsensitive");
5650
+ }
5651
+ cmp = (a, b) => {
5652
+ if (isFolder) {
5653
+ const isFolderA = isFolder(a);
5654
+ if (isFolderA !== isFolder(b)) {
5655
+ return isFolderA ? -1 : 1;
5656
+ }
5657
+ }
5658
+ let x = key(a);
5659
+ let y = key(b);
5660
+ // Assure we have reasonable comparisons with null values:
5661
+ if (x == null) {
5662
+ x = typeof y === "string" ? "" : 0;
5663
+ }
5664
+ else if (typeof x === "boolean") {
5665
+ x = x ? 1 : 0;
5666
+ }
5667
+ if (y == null) {
5668
+ y = typeof x === "string" ? "" : 0;
5669
+ }
5670
+ else if (typeof y === "boolean") {
5671
+ y = y ? 1 : 0;
5672
+ }
5673
+ if (order === "desc") {
5674
+ return x === y ? 0 : x > y ? -1 : 1;
5675
+ }
5676
+ return x === y ? 0 : x > y ? 1 : -1;
5677
+ };
5489
5678
  }
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;
5679
+ function _sortChildren(cl) {
5680
+ if (!cl) {
5681
+ return;
5516
5682
  }
5517
- if (caseInsensitive) {
5518
- if (typeof av === "string") {
5519
- av = av.toLowerCase();
5520
- }
5521
- if (typeof bv === "string") {
5522
- bv = bv.toLowerCase();
5683
+ cl.sort(cmp);
5684
+ if (deep) {
5685
+ for (let i = 0, l = cl.length; i < l; i++) {
5686
+ if (cl[i].children) {
5687
+ _sortChildren(cl[i].children);
5688
+ }
5523
5689
  }
5524
5690
  }
5525
- if (order === "desc") {
5526
- return av === bv ? 0 : av > bv ? -1 : 1;
5691
+ }
5692
+ if (this.children) {
5693
+ _sortChildren(this.children);
5694
+ }
5695
+ this.tree.update(ChangeType.structure);
5696
+ // this.triggerModify("sort"); // TODO
5697
+ }
5698
+ /**
5699
+ * Re-apply current sorting if any (use after lazy load).
5700
+ * Example:
5701
+ * ```js
5702
+ * load: function (e) {
5703
+ * // Whe loading a lazy branch, apply current sort order if any
5704
+ * e.node.resort();
5705
+ * },
5706
+ * ```
5707
+ * @since 0.14.0
5708
+ */
5709
+ resort(options = {}) {
5710
+ for (const colDef of this.tree.columns) {
5711
+ if (colDef.sortOrder) {
5712
+ options.colId = colDef.id;
5713
+ options.order = colDef.sortOrder;
5714
+ this.sort(options);
5715
+ break;
5527
5716
  }
5528
- return av === bv ? 0 : av > bv ? 1 : -1;
5529
- };
5530
- return this.sortChildren(cmp, deep);
5717
+ }
5531
5718
  }
5532
5719
  /**
5533
5720
  * Trigger `modifyChild` event on a parent to signal that a child was modified.
@@ -5564,7 +5751,8 @@
5564
5751
  * @param {function} callback the callback function.
5565
5752
  * Return false to stop iteration, return "skip" to skip this node and
5566
5753
  * its children only.
5567
- * @see {@link IterableIterator<WunderbaumNode>}, {@link Wunderbaum.visit}.
5754
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
5755
+ * @see {@link Wunderbaum.visit}.
5568
5756
  */
5569
5757
  visit(callback, includeSelf = false) {
5570
5758
  let res = true;
@@ -5637,7 +5825,7 @@
5637
5825
  /*!
5638
5826
  * Wunderbaum - ext-edit
5639
5827
  * 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)
5828
+ * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
5641
5829
  */
5642
5830
  // const START_MARKER = "\uFFF7";
5643
5831
  class EditExtension extends WunderbaumExtension {
@@ -5872,7 +6060,7 @@
5872
6060
  newValue = newValue.trim();
5873
6061
  }
5874
6062
  if (!node) {
5875
- this.tree.logDebug("stopEditTitle: not in edit mode.");
6063
+ // this.tree.logDebug("stopEditTitle: not in edit mode.");
5876
6064
  return;
5877
6065
  }
5878
6066
  node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue);
@@ -5972,8 +6160,8 @@
5972
6160
  * https://github.com/mar10/wunderbaum
5973
6161
  *
5974
6162
  * Released under the MIT license.
5975
- * @version v0.13.0
5976
- * @date Sat, 08 Mar 2025 14:16:31 GMT
6163
+ * @version v0.14.1
6164
+ * @date Sun, 22 Mar 2026 05:52:05 GMT
5977
6165
  */
5978
6166
  // import "./wunderbaum.scss";
5979
6167
  class WbSystemRoot extends WunderbaumNode {
@@ -6022,17 +6210,17 @@
6022
6210
  this._disableUpdateIgnoreCount = 0;
6023
6211
  this._activeNode = null;
6024
6212
  this._focusNode = null;
6213
+ this._initialSource = null;
6025
6214
  /** Shared properties, referenced by `node.type`. */
6026
6215
  this.types = {};
6027
6216
  /** List of column definitions. */
6028
- this.columns = []; // any[] = [];
6217
+ this.columns = [];
6029
6218
  this._columnsById = {};
6030
6219
  // Modification Status
6031
6220
  this.pendingChangeTypes = new Set();
6032
6221
  /** Expose some useful methods of the util.ts module as `tree._util`. */
6033
6222
  this._util = util;
6034
6223
  // --- SELECT ---
6035
- // /** @internal */
6036
6224
  // public selectRangeAnchor: WunderbaumNode | null = null;
6037
6225
  // --- BREADCRUMB ---
6038
6226
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
@@ -6051,37 +6239,44 @@
6051
6239
  this.lastQuicksearchTerm = "";
6052
6240
  // --- EDIT ---
6053
6241
  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">
6242
+ // Set default options and merge with user options
6243
+ const initOptions = Object.assign({
6244
+ id: undefined,
6245
+ source: [], // URL for GET/PUT, Ajax options, or callback
6246
+ element: unsafeCast(null),
6058
6247
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
6059
6248
  header: null, // Show/hide header (pass bool or string)
6060
- // headerHeightPx: ROW_HEIGHT,
6061
6249
  rowHeightPx: DEFAULT_ROW_HEIGHT,
6062
6250
  iconMap: "bootstrap",
6063
- columns: null,
6064
- types: null,
6065
- // escapeTitles: true,
6251
+ columns: [], //util.unsafeCast<ColumnDefinitionList>(null),
6252
+ types: {},
6066
6253
  enabled: true,
6067
6254
  fixedCol: false,
6068
6255
  showSpinner: false,
6069
6256
  checkbox: false,
6070
6257
  minExpandLevel: 0,
6071
6258
  emptyChildListExpandable: false,
6072
- // updateThrottleWait: 200,
6073
6259
  skeleton: false,
6260
+ autoCollapse: false,
6261
+ adjustHeight: true,
6074
6262
  connectTopBreadcrumb: null,
6263
+ columnsFilterable: false,
6264
+ columnsMenu: false,
6265
+ columnsResizable: false,
6266
+ columnsSortable: false,
6075
6267
  selectMode: "multi", // SelectModeType
6268
+ scrollIntoViewOnExpandClick: true,
6269
+ // --- Extensions (actually set by exensions on init)
6270
+ dnd: unsafeCast(null),
6271
+ edit: unsafeCast(null),
6272
+ filter: unsafeCast(null),
6076
6273
  // --- KeyNav ---
6077
- navigationModeOption: null, // NavModeEnum,
6274
+ navigationModeOption: unsafeCast(null),
6078
6275
  quicksearch: true,
6079
6276
  // --- Events ---
6080
- iconBadge: null,
6081
- change: null,
6082
- // enhanceTitle: null,
6083
- error: null,
6084
- receive: null,
6277
+ // iconBadge: null,
6278
+ // change: null,
6279
+ // ...
6085
6280
  // --- Strings ---
6086
6281
  strings: {
6087
6282
  loadError: "Error",
@@ -6092,7 +6287,9 @@
6092
6287
  noMatch: "No results",
6093
6288
  matchIndex: "${match} of ${matches}",
6094
6289
  },
6095
- }, options));
6290
+ }, options);
6291
+ const opts = initOptions;
6292
+ this.options = opts;
6096
6293
  const readyDeferred = new Deferred();
6097
6294
  this.ready = readyDeferred.promise();
6098
6295
  let readyOk = false;
@@ -6119,7 +6316,8 @@
6119
6316
  this._callEvent("init", { error: err });
6120
6317
  }
6121
6318
  });
6122
- this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
6319
+ this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence;
6320
+ delete initOptions.id;
6123
6321
  this.root = new WbSystemRoot(this);
6124
6322
  this._registerExtension(new KeynavExtension(this));
6125
6323
  this._registerExtension(new EditExtension(this));
@@ -6129,19 +6327,20 @@
6129
6327
  this._registerExtension(new LoggerExtension(this));
6130
6328
  this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {});
6131
6329
  // --- Evaluate options
6132
- this.columns = opts.columns;
6133
- delete opts.columns;
6330
+ this.columns = initOptions.columns || [];
6331
+ delete initOptions.columns;
6134
6332
  if (!this.columns || !this.columns.length) {
6135
6333
  const title = typeof opts.header === "string" ? opts.header : this.id;
6136
6334
  this.columns = [{ id: "*", title: title, width: "*" }];
6137
6335
  }
6138
- if (opts.types) {
6139
- this.setTypes(opts.types, true);
6336
+ if (initOptions.types) {
6337
+ this.setTypes(initOptions.types, true);
6140
6338
  }
6141
- delete opts.types;
6339
+ delete initOptions.types;
6142
6340
  // --- Create Markup
6143
- this.element = elemFromSelector(opts.element);
6144
- assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
6341
+ this.element = elemFromSelector(initOptions.element);
6342
+ assert(!!this.element, `Invalid 'element' option: ${initOptions.element}`);
6343
+ delete initOptions.element;
6145
6344
  this.element.classList.add("wunderbaum");
6146
6345
  if (!this.element.getAttribute("tabindex")) {
6147
6346
  this.element.tabIndex = 0;
@@ -6217,11 +6416,11 @@
6217
6416
  }
6218
6417
  });
6219
6418
  // --- Load initial data
6220
- if (opts.source) {
6419
+ if (initOptions.source) {
6221
6420
  if (opts.showSpinner) {
6222
6421
  this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
6223
6422
  }
6224
- this.load(opts.source)
6423
+ this.load(initOptions.source)
6225
6424
  .then(() => {
6226
6425
  // The source may have defined columns, so we may adjust the nav mode
6227
6426
  if (opts.navigationModeOption == null) {
@@ -6254,15 +6453,18 @@
6254
6453
  // has a wrong value at start???
6255
6454
  this.update(ChangeType.any);
6256
6455
  // --- Bind listeners
6257
- this.element.addEventListener("scroll", (e) => {
6258
- // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6259
- this.update(ChangeType.scroll);
6260
- });
6456
+ this._registerEventHandlers();
6261
6457
  this.resizeObserver = new ResizeObserver((entries) => {
6262
6458
  // this.log("ResizeObserver: Size changed", entries);
6263
6459
  this.update(ChangeType.resize);
6264
6460
  });
6265
6461
  this.resizeObserver.observe(this.element);
6462
+ }
6463
+ _registerEventHandlers() {
6464
+ this.element.addEventListener("scroll", (e) => {
6465
+ // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6466
+ this.update(ChangeType.scroll);
6467
+ });
6266
6468
  onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
6267
6469
  var _a, _b;
6268
6470
  const info = Wunderbaum.getEventInfo(e);
@@ -6278,9 +6480,6 @@
6278
6480
  const node = info.node;
6279
6481
  const mouseEvent = e;
6280
6482
  // this.log("click", info);
6281
- // if (this._selectRange(info) === false) {
6282
- // return;
6283
- // }
6284
6483
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
6285
6484
  this.lastClickTime = Date.now();
6286
6485
  return false;
@@ -6299,20 +6498,22 @@
6299
6498
  (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) {
6300
6499
  node.startEditTitle();
6301
6500
  }
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
6501
  if (info.region === NodeRegion.expander) {
6309
6502
  node.setExpanded(!node.isExpanded(), {
6310
- scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
6503
+ scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
6311
6504
  });
6312
6505
  }
6313
6506
  else if (info.region === NodeRegion.checkbox) {
6314
6507
  node.toggleSelected();
6315
6508
  }
6509
+ else {
6510
+ if (info.colIdx >= 0) {
6511
+ node.setActive(true, { colIdx: info.colIdx, event: e });
6512
+ }
6513
+ else {
6514
+ node.setActive(true, { event: e });
6515
+ }
6516
+ }
6316
6517
  }
6317
6518
  this.lastClickTime = Date.now();
6318
6519
  });
@@ -6348,7 +6549,7 @@
6348
6549
  const targetNode = Wunderbaum.getNode(e);
6349
6550
  this._callEvent("focus", { flag: flag, event: e });
6350
6551
  if (flag && this.isRowNav() && !this.isEditingTitle()) {
6351
- if (opts.navigationModeOption === NavModeEnum.row) {
6552
+ if (this.options.navigationModeOption === NavModeEnum.row) {
6352
6553
  targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive();
6353
6554
  }
6354
6555
  else {
@@ -6415,11 +6616,12 @@
6415
6616
  }
6416
6617
  /**
6417
6618
  * Return the icon-function -> icon-definition mapping.
6619
+ * @deprecated Use {@link Wunderbaum.iconMaps}
6418
6620
  */
6419
6621
  get iconMap() {
6420
6622
  const map = this.options.iconMap;
6421
6623
  if (typeof map === "string") {
6422
- return iconMaps[map];
6624
+ return defaultIconMaps[map];
6423
6625
  }
6424
6626
  return map;
6425
6627
  }
@@ -6472,7 +6674,38 @@
6472
6674
  ext.init();
6473
6675
  }
6474
6676
  }
6475
- /** Add node to tree's bookkeeping data structures. */
6677
+ /**
6678
+ * Calculate a *stable*, unique key for a node from its refKey (or title).
6679
+ * We also add information from the parent, because a refKey may occur multiple
6680
+ * times in a tree (but not as child of the same parent).
6681
+ * @internal
6682
+ */
6683
+ _calculateKey(data, parent) {
6684
+ if (data.key) {
6685
+ // Always use an explicitly passed key
6686
+ return data.key;
6687
+ }
6688
+ // Auto-keys are optional, use a monotonic counter by default:
6689
+ if (!this.options.autoKeys) {
6690
+ return "" + ++WunderbaumNode.sequence;
6691
+ }
6692
+ // Add the parent's key to the hash. Assuming this was generated by the
6693
+ // same algorithm, this should incorporate the whole path:
6694
+ const s = (parent ? parent.key : "") + (data.refKey || data.title);
6695
+ // 32-bit has a high probability of collisions, so we pump up to 64-bit
6696
+ // https://security.stackexchange.com/q/209882/207588
6697
+ const h1 = murmurHash3(s, true);
6698
+ let key = "id_" + h1 + murmurHash3(h1 + s, true);
6699
+ // Check for collisions
6700
+ // (Most likely if the same title occurs multiple in the same parent).
6701
+ const existingNode = this.keyMap.get(key);
6702
+ if (existingNode) {
6703
+ key += "." + ++Wunderbaum.sequence;
6704
+ this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data);
6705
+ }
6706
+ return key;
6707
+ }
6708
+ /** Add node to tree's bookkeeping data structures. @internal */
6476
6709
  _registerNode(node) {
6477
6710
  const key = node.key;
6478
6711
  assert(key != null, `Missing key: '${node}'.`);
@@ -6489,7 +6722,7 @@
6489
6722
  }
6490
6723
  }
6491
6724
  }
6492
- /** Remove node from tree's bookkeeping data structures. */
6725
+ /** Remove node from tree's bookkeeping data structures. @internal */
6493
6726
  _unregisterNode(node) {
6494
6727
  // Remove refKey reference from map (if any)
6495
6728
  const rk = node.refKey;
@@ -6608,6 +6841,16 @@
6608
6841
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6609
6842
  return this._getNodeByRowIdx(bottomIdx);
6610
6843
  }
6844
+ /** Return preceding visible node in the viewport. */
6845
+ _getPrevNodeInView(node, ofs = 1) {
6846
+ this.visitRows((n) => {
6847
+ node = n;
6848
+ if (ofs-- <= 0) {
6849
+ return false;
6850
+ }
6851
+ }, { reverse: true, start: node || this.getActiveNode() });
6852
+ return node;
6853
+ }
6611
6854
  /** Return following visible node in the viewport. */
6612
6855
  _getNextNodeInView(node, options) {
6613
6856
  let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
@@ -6859,22 +7102,39 @@
6859
7102
  /** Run code, but defer rendering of viewport until done.
6860
7103
  *
6861
7104
  * ```js
6862
- * tree.runWithDeferredUpdate(() => {
6863
- * return someFuncThatWouldUpdateManyNodes();
7105
+ * const res = tree.runWithDeferredUpdate(() => {
7106
+ * return someFunctionThatWouldUpdateManyNodes();
6864
7107
  * });
6865
7108
  * ```
6866
7109
  */
6867
- runWithDeferredUpdate(func, hint = null) {
7110
+ runWithDeferredUpdate(func) {
6868
7111
  try {
6869
7112
  this.enableUpdate(false);
6870
7113
  const res = func();
6871
- assert(!(res instanceof Promise), `Promise return not allowed: ${res}`);
7114
+ assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`);
6872
7115
  return res;
6873
7116
  }
6874
7117
  finally {
6875
7118
  this.enableUpdate(true);
6876
7119
  }
6877
7120
  }
7121
+ /** Run code, but defer rendering of viewport until done.
7122
+ *
7123
+ * ```js
7124
+ * const res = await tree.runWithDeferredUpdate(async () => {
7125
+ * return someAsyncFunctionThatWouldUpdateManyNodes();
7126
+ * });
7127
+ * ```
7128
+ */
7129
+ async runWithDeferredUpdateAsync(func) {
7130
+ try {
7131
+ this.enableUpdate(false);
7132
+ return await func();
7133
+ }
7134
+ finally {
7135
+ this.enableUpdate(true);
7136
+ }
7137
+ }
6878
7138
  /** Recursively expand all expandable nodes (triggers lazy load if needed). */
6879
7139
  async expandAll(flag = true, options) {
6880
7140
  await this.root.expandAll(flag, options);
@@ -6894,6 +7154,17 @@
6894
7154
  getSelectedNodes(stopOnParents = false) {
6895
7155
  return this.root.getSelectedNodes(stopOnParents);
6896
7156
  }
7157
+ /**
7158
+ * Return an array of refKey values.
7159
+ *
7160
+ * RefKeys are unique identifiers for a node data, and are used to identify
7161
+ * clones.
7162
+ * If more than one node has the same refKey, it is only returned once.
7163
+ * @param selected if true, only return refKeys of selected nodes.
7164
+ */
7165
+ getRefKeys(selected = false) {
7166
+ return this.root.getRefKeys(selected);
7167
+ }
6897
7168
  /*
6898
7169
  * Return an array of selected nodes.
6899
7170
  */
@@ -7175,6 +7446,18 @@
7175
7446
  format(name_cb, connectors) {
7176
7447
  return this.root.format(name_cb, connectors);
7177
7448
  }
7449
+ /**
7450
+ * Always returns null (so a tree instance behaves as `tree.root`).
7451
+ */
7452
+ get parent() {
7453
+ return null;
7454
+ }
7455
+ /**
7456
+ * Return a list of top-level nodes.
7457
+ */
7458
+ get children() {
7459
+ return this.root.children || [];
7460
+ }
7178
7461
  /**
7179
7462
  * Return the active cell (`span.wb-col`) of the currently active node or null.
7180
7463
  */
@@ -7293,7 +7576,7 @@
7293
7576
  }
7294
7577
  /** Return true if any node title or grid cell is currently beeing edited.
7295
7578
  *
7296
- * See also {@link Wunderbaum.isEditingTitle}.
7579
+ * See also {@link isEditingTitle}.
7297
7580
  */
7298
7581
  isEditing() {
7299
7582
  const focusElem = this.nodeListElement.querySelector("input:focus,select:focus");
@@ -7301,7 +7584,7 @@
7301
7584
  }
7302
7585
  /** Return true if any node is currently in edit-title mode.
7303
7586
  *
7304
- * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
7587
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
7305
7588
  */
7306
7589
  isEditingTitle() {
7307
7590
  return this._callMethod("edit.isEditingTitle");
@@ -7321,7 +7604,7 @@
7321
7604
  return res;
7322
7605
  }
7323
7606
  /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
7324
- * @see {@link Wunderbaum.logDebug}
7607
+ * @see {@link logDebug}
7325
7608
  */
7326
7609
  log(...args) {
7327
7610
  if (this.options.debugLevel >= 4) {
@@ -7330,7 +7613,7 @@
7330
7613
  }
7331
7614
  /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
7332
7615
  * and browser console level includes debug/verbose messages.
7333
- * @see {@link Wunderbaum.log}
7616
+ * @see {@link log}
7334
7617
  */
7335
7618
  logDebug(...args) {
7336
7619
  if (this.options.debugLevel >= 4) {
@@ -7368,6 +7651,19 @@
7368
7651
  console.warn(this.toString(), ...args); // eslint-disable-line no-console
7369
7652
  }
7370
7653
  }
7654
+ /** Emit a warning for deprecated methods. @internal */
7655
+ logDeprecate(method, options) {
7656
+ if (this.options.debugLevel >= 2) {
7657
+ let msg = `${this}: ${method} is deprecated`;
7658
+ if (options === null || options === void 0 ? void 0 : options.since) {
7659
+ msg += ` since ${options.since}`;
7660
+ }
7661
+ if (options === null || options === void 0 ? void 0 : options.hint) {
7662
+ msg += ` (${options.since})`;
7663
+ }
7664
+ console.warn(msg + "."); // eslint-disable-line no-console
7665
+ }
7666
+ }
7371
7667
  /** Reset column widths to default. @since 0.10.0 */
7372
7668
  resetColumns() {
7373
7669
  this.columns.forEach((col) => {
@@ -7536,46 +7832,64 @@
7536
7832
  this._focusNode = node;
7537
7833
  }
7538
7834
  /** Return the current selection/expansion/activation status. @experimental */
7539
- getState(options) {
7835
+ getState(options = {}) {
7540
7836
  var _a, _b;
7541
- let expandedKeys = undefined;
7542
- if (options.expandedKeys !== false) {
7543
- expandedKeys = [];
7837
+ const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options;
7838
+ const expandSet = new Set();
7839
+ if (expandedKeys) {
7544
7840
  for (const node of this) {
7545
- if (node.expanded) {
7546
- expandedKeys.push(node.key);
7841
+ if (node.isExpanded() && node.hasChildren()) {
7842
+ expandSet.add(node.key);
7547
7843
  }
7548
7844
  }
7549
7845
  }
7846
+ // Parents of active node are always expanded
7847
+ if (activeKey && this.activeNode) {
7848
+ this.activeNode.visitParents((n) => {
7849
+ if (n.parent) {
7850
+ expandSet.add(n.key);
7851
+ }
7852
+ }, false);
7853
+ }
7550
7854
  const state = {
7855
+ expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
7551
7856
  activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
7552
7857
  activeColIdx: this.activeColIdx,
7553
- selectedKeys: options.selectedKeys === false
7554
- ? undefined
7555
- : this.getSelectedNodes().flatMap((n) => n.key),
7556
- expandedKeys: expandedKeys,
7858
+ selectedKeys: selectedKeys
7859
+ ? this.getSelectedNodes().flatMap((n) => n.key)
7860
+ : undefined,
7557
7861
  };
7558
7862
  return state;
7559
7863
  }
7560
7864
  /** Apply selection/expansion/activation status. @experimental */
7561
- setState(state, options) {
7562
- this.runWithDeferredUpdate(() => {
7865
+ async setState(state, options = {}) {
7866
+ const { expandLazy = true } = options;
7867
+ return this.runWithDeferredUpdateAsync(async () => {
7563
7868
  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);
7869
+ if (state.expandedKeys && state.expandedKeys.length) {
7870
+ if (expandLazy) {
7871
+ // Expand all keys recursively, even if they are not in the tree yet
7872
+ await this._loadLazyNodes(state.expandedKeys, {
7873
+ expand: true,
7874
+ noEvents: true,
7875
+ });
7568
7876
  }
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);
7877
+ else {
7878
+ for (const key of state.expandedKeys) {
7879
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true);
7880
+ }
7573
7881
  }
7574
7882
  }
7575
7883
  if (state.activeKey) {
7576
7884
  this.setActiveNode(state.activeKey);
7577
7885
  }
7578
- if (state.activeColIdx != null) {
7886
+ if (state.selectedKeys) {
7887
+ this.selectAll(false);
7888
+ for (const key of state.selectedKeys) {
7889
+ (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true);
7890
+ }
7891
+ }
7892
+ if (this.isCellNav() && state.activeColIdx != null) {
7579
7893
  this.setColumn(state.activeColIdx);
7580
7894
  }
7581
7895
  });
@@ -7736,18 +8050,33 @@
7736
8050
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
7737
8051
  * (defaults to sorting by title).
7738
8052
  * @param {boolean} deep pass true to sort all descendant nodes recursively
8053
+ * @deprecated use {@link sort}
7739
8054
  */
7740
8055
  sortChildren(cmp = nodeTitleSorter, deep = false) {
7741
- this.root.sortChildren(cmp, deep);
8056
+ this.logDeprecate("sortChildren()", { since: "0.14.0" });
8057
+ return this.sort({
8058
+ cmp: cmp ? cmp : undefined,
8059
+ deep: deep,
8060
+ propName: "title",
8061
+ });
7742
8062
  }
7743
8063
  /**
7744
8064
  * Convenience method to implement column sorting.
7745
8065
  * @see {@link WunderbaumNode.sortByProperty}.
7746
8066
  * @since 0.11.0
8067
+ * @deprecated use {@link sort}
7747
8068
  */
7748
8069
  sortByProperty(options) {
8070
+ this.logDeprecate("sortByProperty()", { since: "0.14.0" });
7749
8071
  this.root.sortByProperty(options);
7750
8072
  }
8073
+ /**
8074
+ * Sort nodes list by title or custom criteria.
8075
+ * @since 0.14.0
8076
+ */
8077
+ sort(options) {
8078
+ this.root.sort(options);
8079
+ }
7751
8080
  /** Convert tree to an array of plain objects.
7752
8081
  *
7753
8082
  * @param callback is called for every node, in order to allow
@@ -7999,12 +8328,10 @@
7999
8328
  iconElem = document.createElement("i");
8000
8329
  iconElem.className = "wb-icon";
8001
8330
  }
8002
- else if (icon.indexOf("<") >= 0) {
8003
- // HTML
8331
+ else if (TEST_HTML.test(icon)) {
8004
8332
  iconElem = elemFromHtml(icon);
8005
8333
  }
8006
- else if (TEST_IMG.test(icon)) {
8007
- // Image URL
8334
+ else if (TEST_FILE_PATH.test(icon)) {
8008
8335
  iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
8009
8336
  }
8010
8337
  else {
@@ -8242,7 +8569,8 @@
8242
8569
  }
8243
8570
  /**
8244
8571
  * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
8245
- * @see {@link IterableIterator<WunderbaumNode>}, {@link WunderbaumNode.visit}.
8572
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
8573
+ * @see {@link WunderbaumNode.visit}.
8246
8574
  *
8247
8575
  * @param {function} callback the callback function.
8248
8576
  * Return false to stop iteration, return "skip" to skip this node and
@@ -8385,11 +8713,71 @@
8385
8713
  *
8386
8714
  * Previous data is cleared. Note that also column- and type defintions may
8387
8715
  * be passed with the `source` object.
8716
+ * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request
8717
+ * and restore the previous state.
8388
8718
  */
8389
- load(source) {
8719
+ async load(source) {
8390
8720
  this.clear();
8721
+ this._initialSource = source;
8391
8722
  return this.root.load(source);
8392
8723
  }
8724
+ /** Reload the tree and optionally restore state.
8725
+ * Source defaults to last ajax url if any.
8726
+ * Restoring the active node requires stable keys
8727
+ * @see {@link WunderbaumOptions.autoKeys}
8728
+ * @see {@link Wunderbaum.load}
8729
+ * @experimental
8730
+ */
8731
+ async reload(options = {}) {
8732
+ const { source = this._initialSource, reactivate = true } = options;
8733
+ if (!source) {
8734
+ this.logWarn("No previous ajax source to reload.");
8735
+ return;
8736
+ }
8737
+ if (!reactivate) {
8738
+ return this.load(source);
8739
+ }
8740
+ const state = this.getState();
8741
+ await this.load(source);
8742
+ return this.setState(state);
8743
+ }
8744
+ /**
8745
+ * Make sure that all nodes in the given keyList are accessible.
8746
+ * This may include loading lazy parent nodes.
8747
+ * Recursively load (and optionally expand) all requested node paths.
8748
+ */
8749
+ async _loadLazyNodes(keyList, options = {}) {
8750
+ const { expand = true } = options;
8751
+ const keySet = new Set(keyList);
8752
+ // Make sure that all parent nodes are loaded (and expand if requested)
8753
+ while (keySet.size > 0) {
8754
+ const pendingNodes = [];
8755
+ const curSet = new Set(keySet);
8756
+ for (const key of curSet) {
8757
+ const node = this.findKey(key);
8758
+ if (!node) {
8759
+ continue; // key not yet found (need to load lazy parent?)
8760
+ }
8761
+ keySet.delete(key);
8762
+ if (expand) {
8763
+ pendingNodes.push(node.setExpanded(true));
8764
+ }
8765
+ else if (node.isUnloaded()) {
8766
+ pendingNodes.push(node.loadLazy());
8767
+ }
8768
+ if (node._rowElem) {
8769
+ node._render(); // show spinner even is update is suppressed
8770
+ }
8771
+ }
8772
+ if (pendingNodes.length === 0) {
8773
+ // will not load any more nodes, so if if there are still keys
8774
+ // left in the set, we will never find them
8775
+ this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet);
8776
+ break;
8777
+ }
8778
+ await Promise.allSettled(pendingNodes);
8779
+ }
8780
+ }
8393
8781
  /**
8394
8782
  * Disable render requests during operations that would trigger many updates.
8395
8783
  *
@@ -8487,9 +8875,21 @@
8487
8875
  }
8488
8876
  Wunderbaum.sequence = 0;
8489
8877
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8490
- Wunderbaum.version = "v0.13.0"; // Set to semver by 'grunt release'
8878
+ Wunderbaum.version = "v0.14.1"; // Set to semver by 'grunt release'
8491
8879
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8492
8880
  Wunderbaum.util = util;
8881
+ /** A map of default iconMaps.
8882
+ * May be used as default, when passing partial icon definition maps:
8883
+ * ```js
8884
+ * const tree = new mar10.Wunderbaum({
8885
+ * ...
8886
+ * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
8887
+ * folder: "bi bi-archive",
8888
+ * }),
8889
+ * });
8890
+ * ```
8891
+ */
8892
+ Wunderbaum.iconMaps = defaultIconMaps;
8493
8893
 
8494
8894
  exports.Wunderbaum = Wunderbaum;
8495
8895