wunderbaum 0.12.1 → 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.
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Wunderbaum - debounce.ts
3
3
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
4
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
4
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
  /*
7
7
  * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21
@@ -293,7 +293,7 @@ function throttle(func, wait = 0, options = {}) {
293
293
  /*!
294
294
  * Wunderbaum - util
295
295
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
296
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
296
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
297
297
  */
298
298
  /** @module util */
299
299
  /** Readable names for `MouseEvent.button` */
@@ -443,7 +443,7 @@ function each(obj, callback) {
443
443
  }
444
444
  return obj;
445
445
  }
446
- /** Shortcut for `throw new Error(msg)`.*/
446
+ /** Shortcut for `throw new Error(msg)`. */
447
447
  function error(msg) {
448
448
  throw new Error(msg);
449
449
  }
@@ -676,18 +676,6 @@ function elemFromSelector(obj) {
676
676
  }
677
677
  return obj;
678
678
  }
679
- // /** Return a EventTarget from selector or cast an existing element. */
680
- // export function eventTargetFromSelector(
681
- // obj: string | EventTarget
682
- // ): EventTarget | null {
683
- // if (!obj) {
684
- // return null;
685
- // }
686
- // if (typeof obj === "string") {
687
- // return document.querySelector(obj) as EventTarget;
688
- // }
689
- // return obj as EventTarget;
690
- // }
691
679
  /**
692
680
  * Return a canonical descriptive string for a keyboard or mouse event.
693
681
  *
@@ -970,6 +958,10 @@ function toPixel(...defaults) {
970
958
  }
971
959
  throw new Error(`Expected a string like '123px': ${defaults}`);
972
960
  }
961
+ /** Cast any value to <T>. */
962
+ function unsafeCast(value) {
963
+ return value;
964
+ }
973
965
  /** Return the the boolean value of the first non-null element.
974
966
  * Example:
975
967
  * ```js
@@ -1043,7 +1035,7 @@ function adaptiveThrottle(callback, options) {
1043
1035
  const throttledFn = (...args) => {
1044
1036
  if (waiting) {
1045
1037
  pendingArgs = args;
1046
- // console.log(`adaptiveThrottle() queing request #${waiting}...`, args);
1038
+ // console.log(`adaptiveThrottle() queueing request #${waiting}...`, args);
1047
1039
  waiting += 1;
1048
1040
  }
1049
1041
  else {
@@ -1098,6 +1090,60 @@ function adaptiveThrottle(callback, options) {
1098
1090
  };
1099
1091
  return throttledFn;
1100
1092
  }
1093
+ /**
1094
+ * MurmurHash3 implementation for strings.
1095
+ * @param key The input string to hash.
1096
+ * @param asString Optional convert result to zero-padded string of 8 characters.
1097
+ * @param seed Optional seed value.
1098
+ * @returns A 32-bit hash as a number or string.
1099
+ */
1100
+ function murmurHash3(key, asString = true, seed = 0) {
1101
+ let h1 = seed;
1102
+ const remainder = key.length & 3; // key.length % 4
1103
+ const bytes = key.length - remainder;
1104
+ const c1 = 0xcc9e2d51;
1105
+ const c2 = 0x1b873593;
1106
+ let i = 0;
1107
+ while (i < bytes) {
1108
+ let k1 = (key.charCodeAt(i) & 0xff) |
1109
+ ((key.charCodeAt(++i) & 0xff) << 8) |
1110
+ ((key.charCodeAt(++i) & 0xff) << 16) |
1111
+ ((key.charCodeAt(++i) & 0xff) << 24);
1112
+ ++i;
1113
+ k1 = Math.imul(k1, c1);
1114
+ k1 = (k1 << 15) | (k1 >>> 17);
1115
+ k1 = Math.imul(k1, c2);
1116
+ h1 ^= k1;
1117
+ h1 = (h1 << 13) | (h1 >>> 19);
1118
+ h1 = Math.imul(h1, 5) + 0xe6546b64;
1119
+ }
1120
+ let k1 = 0;
1121
+ switch (remainder) {
1122
+ case 3:
1123
+ k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
1124
+ // fall through
1125
+ case 2:
1126
+ k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
1127
+ // fall through
1128
+ case 1:
1129
+ k1 ^= key.charCodeAt(i) & 0xff;
1130
+ k1 = Math.imul(k1, c1);
1131
+ k1 = (k1 << 15) | (k1 >>> 17);
1132
+ k1 = Math.imul(k1, c2);
1133
+ h1 ^= k1;
1134
+ }
1135
+ h1 ^= key.length;
1136
+ h1 ^= h1 >>> 16;
1137
+ h1 = Math.imul(h1, 0x85ebca6b);
1138
+ h1 ^= h1 >>> 13;
1139
+ h1 = Math.imul(h1, 0xc2b2ae35);
1140
+ h1 ^= h1 >>> 16;
1141
+ if (asString) {
1142
+ // Convert to 8 digit hex string
1143
+ return (h1 >>> 0).toString(16).padStart(8, "0");
1144
+ }
1145
+ return h1 >>> 0; // Convert to unsigned 32-bit integer
1146
+ }
1101
1147
 
1102
1148
  var util = /*#__PURE__*/Object.freeze({
1103
1149
  __proto__: null,
@@ -1128,6 +1174,7 @@ var util = /*#__PURE__*/Object.freeze({
1128
1174
  isFunction: isFunction,
1129
1175
  isMac: isMac,
1130
1176
  isPlainObject: isPlainObject,
1177
+ murmurHash3: murmurHash3,
1131
1178
  noop: noop,
1132
1179
  onEvent: onEvent,
1133
1180
  overrideMethod: overrideMethod,
@@ -1141,13 +1188,14 @@ var util = /*#__PURE__*/Object.freeze({
1141
1188
  toPixel: toPixel,
1142
1189
  toSet: toSet,
1143
1190
  toggleCheckbox: toggleCheckbox,
1144
- type: type
1191
+ type: type,
1192
+ unsafeCast: unsafeCast
1145
1193
  });
1146
1194
 
1147
1195
  /*!
1148
1196
  * Wunderbaum - types
1149
1197
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1150
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1198
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1151
1199
  */
1152
1200
  /**
1153
1201
  * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
@@ -1215,7 +1263,7 @@ var NavModeEnum;
1215
1263
  /*!
1216
1264
  * Wunderbaum - wb_extension_base
1217
1265
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1218
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1266
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1219
1267
  */
1220
1268
  class WunderbaumExtension {
1221
1269
  constructor(tree, id, defaults) {
@@ -1274,7 +1322,7 @@ class WunderbaumExtension {
1274
1322
  /*!
1275
1323
  * Wunderbaum - ext-filter
1276
1324
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1277
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1325
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1278
1326
  */
1279
1327
  const START_MARKER = "\uFFF7";
1280
1328
  const END_MARKER = "\uFFF8";
@@ -1286,7 +1334,7 @@ class FilterExtension extends WunderbaumExtension {
1286
1334
  autoApply: true, // Re-apply last filter if lazy data is loaded
1287
1335
  autoExpand: false, // Expand all branches that contain matches while filtered
1288
1336
  matchBranch: false, // Whether to implicitly match all children of matched nodes
1289
- connectInput: null, // Element or selector of an input control for filter query strings
1337
+ connect: null, // Element or selector of an input control for filter query strings
1290
1338
  fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
1291
1339
  hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
1292
1340
  highlight: true, // Highlight matches by wrapping inside <mark> tags
@@ -1294,36 +1342,117 @@ class FilterExtension extends WunderbaumExtension {
1294
1342
  mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
1295
1343
  noData: true, // Display a 'no data' status node if result is empty
1296
1344
  });
1345
+ this.queryInput = null;
1346
+ this.prevButton = null;
1347
+ this.nextButton = null;
1348
+ this.modeButton = null;
1349
+ this.matchInfoElem = null;
1297
1350
  this.lastFilterArgs = null;
1298
1351
  }
1299
1352
  init() {
1300
1353
  super.init();
1301
- const connectInput = this.getPluginOption("connectInput");
1302
- if (connectInput) {
1303
- this.queryInput = elemFromSelector(connectInput);
1304
- assert(this.queryInput, `Invalid 'filter.connectInput' option: ${connectInput}.`);
1305
- onEvent(this.queryInput, "input", debounce((e) => {
1306
- // this.tree.log("query", e);
1307
- this.filterNodes(this.queryInput.value.trim(), {});
1308
- }, 700));
1354
+ const connect = this.getPluginOption("connect");
1355
+ if (connect) {
1356
+ this._connectControls();
1309
1357
  }
1310
1358
  }
1311
1359
  setPluginOption(name, value) {
1312
- // alert("filter opt=" + name + ", " + value)
1313
1360
  super.setPluginOption(name, value);
1314
1361
  switch (name) {
1315
1362
  case "mode":
1316
- this.tree.filterMode = value === "hide" ? "hide" : "dim";
1363
+ this.tree.filterMode =
1364
+ value === "hide" ? "hide" : value === "mark" ? "mark" : "dim";
1317
1365
  this.tree.updateFilter();
1318
1366
  break;
1319
1367
  }
1320
1368
  }
1369
+ _updatedConnectedControls() {
1370
+ var _a;
1371
+ const filterActive = this.tree.filterMode !== null;
1372
+ const activeNode = this.tree.getActiveNode();
1373
+ const matchCount = filterActive ? this.countMatches() : 0;
1374
+ const strings = this.treeOpts.strings;
1375
+ let matchIdx = "?";
1376
+ if (this.matchInfoElem) {
1377
+ if (filterActive) {
1378
+ let info;
1379
+ if (matchCount === 0) {
1380
+ info = strings.noMatch;
1381
+ }
1382
+ else if (activeNode && activeNode.match >= 1) {
1383
+ matchIdx = (_a = activeNode.match) !== null && _a !== void 0 ? _a : "?";
1384
+ info = strings.matchIndex;
1385
+ }
1386
+ else {
1387
+ info = strings.queryResult;
1388
+ }
1389
+ info = info
1390
+ .replace("${count}", this.tree.count().toLocaleString())
1391
+ .replace("${match}", "" + matchIdx)
1392
+ .replace("${matches}", matchCount.toLocaleString());
1393
+ this.matchInfoElem.textContent = info;
1394
+ }
1395
+ else {
1396
+ this.matchInfoElem.textContent = "";
1397
+ }
1398
+ }
1399
+ if (this.nextButton instanceof HTMLButtonElement) {
1400
+ this.nextButton.disabled = !matchCount;
1401
+ }
1402
+ if (this.prevButton instanceof HTMLButtonElement) {
1403
+ this.prevButton.disabled = !matchCount;
1404
+ }
1405
+ if (this.modeButton) {
1406
+ this.modeButton.disabled = !filterActive;
1407
+ this.modeButton.classList.toggle("wb-filter-hide", this.tree.filterMode === "hide");
1408
+ }
1409
+ }
1410
+ _connectControls() {
1411
+ const tree = this.tree;
1412
+ const connect = this.getPluginOption("connect");
1413
+ if (!connect) {
1414
+ return;
1415
+ }
1416
+ this.queryInput = elemFromSelector(connect.inputElem);
1417
+ if (!this.queryInput) {
1418
+ throw new Error(`Invalid 'filter.connect' option: ${connect.inputElem}.`);
1419
+ }
1420
+ this.prevButton = elemFromSelector(connect.prevButton);
1421
+ this.nextButton = elemFromSelector(connect.nextButton);
1422
+ this.modeButton = elemFromSelector(connect.modeButton);
1423
+ this.matchInfoElem = elemFromSelector(connect.matchInfoElem);
1424
+ if (this.prevButton) {
1425
+ onEvent(this.prevButton, "click", () => {
1426
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "prevMatch");
1427
+ this._updatedConnectedControls();
1428
+ });
1429
+ }
1430
+ if (this.nextButton) {
1431
+ onEvent(this.nextButton, "click", () => {
1432
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "nextMatch");
1433
+ this._updatedConnectedControls();
1434
+ });
1435
+ }
1436
+ if (this.modeButton) {
1437
+ onEvent(this.modeButton, "click", (e) => {
1438
+ if (!this.tree.filterMode) {
1439
+ return;
1440
+ }
1441
+ this.setPluginOption("mode", tree.filterMode === "dim" ? "hide" : "dim");
1442
+ });
1443
+ }
1444
+ onEvent(this.queryInput, "input", debounce((e) => {
1445
+ this.filterNodes(this.queryInput.value.trim(), {});
1446
+ }, 700));
1447
+ this._updatedConnectedControls();
1448
+ }
1321
1449
  _applyFilterNoUpdate(filter, _opts) {
1322
1450
  return this.tree.runWithDeferredUpdate(() => {
1323
1451
  return this._applyFilterImpl(filter, _opts);
1324
1452
  });
1325
1453
  }
1326
1454
  _applyFilterImpl(filter, _opts) {
1455
+ var _a;
1327
1456
  let //temp,
1328
1457
  count = 0;
1329
1458
  const start = Date.now();
@@ -1405,11 +1534,11 @@ class FilterExtension extends WunderbaumExtension {
1405
1534
  return !!res;
1406
1535
  };
1407
1536
  }
1408
- tree.filterMode = opts.mode;
1537
+ tree.filterMode = (_a = opts.mode) !== null && _a !== void 0 ? _a : "dim";
1409
1538
  // eslint-disable-next-line prefer-rest-params
1410
1539
  this.lastFilterArgs = arguments;
1411
1540
  tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
1412
- tree.element.classList.toggle("wb-ext-filter-dim", !hideMode);
1541
+ tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim");
1413
1542
  tree.element.classList.toggle("wb-ext-filter-hide-expanders", !!opts.hideExpanders);
1414
1543
  // Reset current filter
1415
1544
  tree.root.subMatchCount = 0;
@@ -1418,10 +1547,6 @@ class FilterExtension extends WunderbaumExtension {
1418
1547
  delete node.titleWithHighlight;
1419
1548
  node.subMatchCount = 0;
1420
1549
  });
1421
- // statusNode = tree.root.findDirectChild(KEY_NODATA);
1422
- // if (statusNode) {
1423
- // statusNode.remove();
1424
- // }
1425
1550
  tree.setStatus(NodeStatusType.ok);
1426
1551
  // Adjust node.hide, .match, and .subMatchCount properties
1427
1552
  treeOpts.autoCollapse = false; // #528
@@ -1432,7 +1557,7 @@ class FilterExtension extends WunderbaumExtension {
1432
1557
  let res = filter(node);
1433
1558
  if (res === "skip") {
1434
1559
  node.visit(function (c) {
1435
- c.match = false;
1560
+ c.match = undefined;
1436
1561
  }, true);
1437
1562
  return "skip";
1438
1563
  }
@@ -1443,7 +1568,7 @@ class FilterExtension extends WunderbaumExtension {
1443
1568
  }
1444
1569
  if (res) {
1445
1570
  count++;
1446
- node.match = true;
1571
+ node.match = count;
1447
1572
  node.visitParents((p) => {
1448
1573
  if (p !== node) {
1449
1574
  p.subMatchCount += 1;
@@ -1470,6 +1595,7 @@ class FilterExtension extends WunderbaumExtension {
1470
1595
  }
1471
1596
  // Redraw whole tree
1472
1597
  tree.logDebug(`Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.`);
1598
+ this._updatedConnectedControls();
1473
1599
  return count;
1474
1600
  }
1475
1601
  /**
@@ -1484,6 +1610,10 @@ class FilterExtension extends WunderbaumExtension {
1484
1610
  */
1485
1611
  filterBranches(filter, options) {
1486
1612
  assert(options.matchBranch === undefined, "filterBranches() is deprecated.");
1613
+ this.tree.logDeprecate("filterBranches()", {
1614
+ since: "0.9.0",
1615
+ hint: "Use `filterNodes` instead and set `options.matchBranch: true`",
1616
+ });
1487
1617
  options.matchBranch = true;
1488
1618
  return this._applyFilterNoUpdate(filter, options);
1489
1619
  }
@@ -1514,34 +1644,22 @@ class FilterExtension extends WunderbaumExtension {
1514
1644
  else {
1515
1645
  tree.logWarn("updateFilter(): no filter active.");
1516
1646
  }
1647
+ this._updatedConnectedControls();
1517
1648
  }
1518
1649
  /**
1519
1650
  * [ext-filter] Reset the filter.
1520
1651
  */
1521
1652
  clearFilter() {
1522
1653
  const tree = this.tree;
1523
- // statusNode = tree.root.findDirectChild(KEY_NODATA),
1524
- // escapeTitles = tree.options.escapeTitles;
1525
1654
  tree.enableUpdate(false);
1526
- // if (statusNode) {
1527
- // statusNode.remove();
1528
- // }
1529
1655
  tree.setStatus(NodeStatusType.ok);
1530
1656
  // we also counted root node's subMatchCount
1531
1657
  delete tree.root.match;
1532
1658
  delete tree.root.subMatchCount;
1533
1659
  tree.visit((node) => {
1534
- // if (node.match && node._rowElem) {
1535
- // let titleElem = node._rowElem.querySelector("span.wb-title")!;
1536
- // node._callEvent("enhanceTitle", { titleElem: titleElem });
1537
- // }
1538
1660
  delete node.match;
1539
1661
  delete node.subMatchCount;
1540
1662
  delete node.titleWithHighlight;
1541
- // if (node.subMatchBadge) {
1542
- // node.subMatchBadge.remove();
1543
- // delete node.subMatchBadge;
1544
- // }
1545
1663
  if (node._filterAutoExpanded && node.expanded) {
1546
1664
  node.setExpanded(false, {
1547
1665
  noAnimation: true,
@@ -1555,12 +1673,12 @@ class FilterExtension extends WunderbaumExtension {
1555
1673
  tree.element.classList.remove(
1556
1674
  // "wb-ext-filter",
1557
1675
  "wb-ext-filter-dim", "wb-ext-filter-hide");
1558
- // tree._callHook("treeStructureChanged", this, "clearFilter");
1676
+ this._updatedConnectedControls();
1559
1677
  tree.enableUpdate(true);
1560
1678
  }
1561
1679
  }
1562
1680
  /**
1563
- * @description Marks the matching charecters of `text` either by `mark` or
1681
+ * @description Marks the matching characters of `text` either by `mark` or
1564
1682
  * by exotic*Chars (if `escapeTitles` is `true`) based on `matches`
1565
1683
  * which is an array of matching groups.
1566
1684
  * @param {string} text
@@ -1599,7 +1717,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1599
1717
  /*!
1600
1718
  * Wunderbaum - ext-keynav
1601
1719
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1602
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1720
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1603
1721
  */
1604
1722
  const QUICKSEARCH_DELAY = 500;
1605
1723
  class KeynavExtension extends WunderbaumExtension {
@@ -1963,7 +2081,7 @@ class KeynavExtension extends WunderbaumExtension {
1963
2081
  /*!
1964
2082
  * Wunderbaum - ext-logger
1965
2083
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1966
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2084
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1967
2085
  */
1968
2086
  class LoggerExtension extends WunderbaumExtension {
1969
2087
  constructor(tree) {
@@ -2005,7 +2123,7 @@ class LoggerExtension extends WunderbaumExtension {
2005
2123
  /*!
2006
2124
  * Wunderbaum - ext-dnd
2007
2125
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2008
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2126
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2009
2127
  */
2010
2128
  const nodeMimeType = "application/x-wunderbaum-node";
2011
2129
  class DndExtension extends WunderbaumExtension {
@@ -2241,7 +2359,6 @@ class DndExtension extends WunderbaumExtension {
2241
2359
  */
2242
2360
  onDragEvent(e) {
2243
2361
  var _a;
2244
- // const tree = this.tree;
2245
2362
  const dndOpts = this.treeOpts.dnd;
2246
2363
  const srcNode = Wunderbaum.getNode(e);
2247
2364
  if (!srcNode) {
@@ -2267,7 +2384,7 @@ class DndExtension extends WunderbaumExtension {
2267
2384
  return false;
2268
2385
  }
2269
2386
  const nodeData = srcNode.toDict(true, (n) => {
2270
- // We don't want to re-use the key on drop:
2387
+ // We don't want to reuse the key on drop:
2271
2388
  n._orgKey = n.key;
2272
2389
  delete n.key;
2273
2390
  });
@@ -2329,6 +2446,7 @@ class DndExtension extends WunderbaumExtension {
2329
2446
  };
2330
2447
  if (!targetNode) {
2331
2448
  this._leaveNode();
2449
+ e.preventDefault(); // Don't open file in browser when dropped in empty area
2332
2450
  return;
2333
2451
  }
2334
2452
  if (["drop"].includes(e.type)) {
@@ -2434,19 +2552,20 @@ class DndExtension extends WunderbaumExtension {
2434
2552
  nodeData = nodeData ? JSON.parse(nodeData) : null;
2435
2553
  const srcNode = this.srcNode;
2436
2554
  const lastDropEffect = this.lastDropEffect;
2437
- setTimeout(() => {
2438
- // Decouple this call, because drop actions may prevent the dragend event
2439
- // from being fired on some browsers
2440
- targetNode._callEvent("dnd.drop", {
2441
- event: e,
2442
- region: region,
2443
- suggestedDropMode: region === "over" ? "appendChild" : region,
2444
- suggestedDropEffect: lastDropEffect,
2445
- // suggestedDropEffect: e.dataTransfer?.dropEffect,
2446
- sourceNode: srcNode,
2447
- sourceNodeData: nodeData,
2448
- });
2449
- }, 10);
2555
+ /* Before v0.14.0, we decoupled `_callEvent` like so:
2556
+ Decouple this call, because drop actions may prevent the dragend
2557
+ event from being fired on some browsers.
2558
+ setTimeout(() => {...}, 10);
2559
+ however this made e.dataTransfer.items inaccessible */
2560
+ targetNode._callEvent("dnd.drop", {
2561
+ event: e,
2562
+ region: region,
2563
+ suggestedDropMode: region === "over" ? "appendChild" : region,
2564
+ suggestedDropEffect: lastDropEffect,
2565
+ sourceNode: srcNode,
2566
+ sourceNodeData: nodeData,
2567
+ dataTransfer: e.dataTransfer,
2568
+ });
2450
2569
  }
2451
2570
  return false;
2452
2571
  }
@@ -2455,7 +2574,7 @@ class DndExtension extends WunderbaumExtension {
2455
2574
  /*!
2456
2575
  * Wunderbaum - drag_observer
2457
2576
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2458
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2577
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2459
2578
  */
2460
2579
  /**
2461
2580
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2604,7 +2723,7 @@ class DragObserver {
2604
2723
  /*!
2605
2724
  * Wunderbaum - common
2606
2725
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2607
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2726
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2608
2727
  */
2609
2728
  const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
2610
2729
  /**
@@ -2624,12 +2743,18 @@ const TITLE_SPAN_PAD_Y = 7;
2624
2743
  const RENDER_MAX_PREFETCH = 5;
2625
2744
  /** Minimum column width if not set otherwise. */
2626
2745
  const DEFAULT_MIN_COL_WIDTH = 4;
2746
+ /**
2747
+ * A value for `node.type` that by convention may be used to mark a node as directory.
2748
+ * It may be used to sort 'directories' to the top.
2749
+ */
2750
+ const NODE_TYPE_FOLDER = "folder";
2627
2751
  /** Regular expression to detect if a string describes an image URL (in contrast
2628
2752
  * to a class name). Strings are considered image urls if they contain '.' or '/'.
2753
+ * `<` is ignored, because it is probably an html tag.
2629
2754
  */
2630
- const TEST_IMG = new RegExp(/\.|\//);
2631
- // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
2632
- // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
2755
+ const TEST_FILE_PATH = /^(?!.*<).*[/.]/;
2756
+ /** Regular expression to detect if a string describes an HTML element. */
2757
+ const TEST_HTML = /</;
2633
2758
  /**
2634
2759
  * Default node icons for icon libraries
2635
2760
  *
@@ -2637,7 +2762,7 @@ const TEST_IMG = new RegExp(/\.|\//);
2637
2762
  * - 'fontawesome6' {@link https://fontawesome.com/icons}
2638
2763
  *
2639
2764
  */
2640
- const iconMaps = {
2765
+ const defaultIconMaps = {
2641
2766
  bootstrap: {
2642
2767
  error: "bi bi-exclamation-triangle",
2643
2768
  // loading: "bi bi-hourglass-split wb-busy",
@@ -2685,7 +2810,7 @@ const iconMaps = {
2685
2810
  radioChecked: "fa-solid fa-circle",
2686
2811
  radioUnchecked: "fa-regular fa-circle",
2687
2812
  radioUnknown: "fa-regular fa-circle-question",
2688
- folder: "fa-solid fa-folder-closed",
2813
+ folder: "fa-regular fa-folder-closed",
2689
2814
  folderOpen: "fa-regular fa-folder-open",
2690
2815
  folderLazy: "fa-solid fa-folder-plus",
2691
2816
  doc: "fa-regular fa-file",
@@ -2717,29 +2842,20 @@ const RESERVED_TREE_SOURCE_KEYS = new Set([
2717
2842
  // "Escape",
2718
2843
  // ]);
2719
2844
  /** Map `KeyEvent.key` to navigation action. */
2720
- const KEY_TO_ACTION_DICT = {
2721
- " ": "toggleSelect",
2722
- "+": "expand",
2723
- Add: "expand",
2845
+ const KEY_TO_NAVIGATION_MAP = {
2724
2846
  ArrowDown: "down",
2725
2847
  ArrowLeft: "left",
2726
2848
  ArrowRight: "right",
2727
2849
  ArrowUp: "up",
2728
2850
  Backspace: "parent",
2729
- "/": "collapseAll",
2730
- Divide: "collapseAll",
2731
2851
  End: "lastCol",
2732
2852
  Home: "firstCol",
2733
2853
  "Control+End": "last",
2734
2854
  "Control+Home": "first",
2735
2855
  "Meta+ArrowDown": "last", // macOs
2736
2856
  "Meta+ArrowUp": "first", // macOs
2737
- "*": "expandAll",
2738
- Multiply: "expandAll",
2739
2857
  PageDown: "pageDown",
2740
2858
  PageUp: "pageUp",
2741
- "-": "collapse",
2742
- Subtract: "collapse",
2743
2859
  };
2744
2860
  /** Return a callback that returns true if the node title matches the string
2745
2861
  * or regular expression.
@@ -2767,12 +2883,20 @@ function makeNodeTitleStartMatcher(s) {
2767
2883
  return reMatch.test(node.title);
2768
2884
  };
2769
2885
  }
2770
- /** Compare two nodes by title (case-insensitive). */
2886
+ /** Compare two nodes by title (case-insensitive).
2887
+ * @deprecated Use `key` option instead of `cmp` in sort methods.
2888
+ */
2771
2889
  function nodeTitleSorter(a, b) {
2772
2890
  const x = a.title.toLowerCase();
2773
2891
  const y = b.title.toLowerCase();
2774
2892
  return x === y ? 0 : x > y ? 1 : -1;
2775
2893
  }
2894
+ // /** Compare nodes by title (case-insensitive). */
2895
+ // export function nodeTitleKeyGetter(
2896
+ // node: WunderbaumNode
2897
+ // ): string | number | Array<any> {
2898
+ // return node.title.toLowerCase();
2899
+ // }
2776
2900
  /**
2777
2901
  * Convert 'flat' to 'nested' format.
2778
2902
  *
@@ -2963,7 +3087,7 @@ function decompressSourceData(source) {
2963
3087
  /*!
2964
3088
  * Wunderbaum - ext-grid
2965
3089
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2966
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3090
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2967
3091
  */
2968
3092
  class GridExtension extends WunderbaumExtension {
2969
3093
  constructor(tree) {
@@ -3023,7 +3147,7 @@ class GridExtension extends WunderbaumExtension {
3023
3147
  super.init();
3024
3148
  }
3025
3149
  /**
3026
- * Hanldes drag and sragstop events for column resizing.
3150
+ * Handles drag and sragstop events for column resizing.
3027
3151
  */
3028
3152
  handleDrag(e) {
3029
3153
  const custom = e.customData;
@@ -3054,7 +3178,7 @@ class GridExtension extends WunderbaumExtension {
3054
3178
  /*!
3055
3179
  * Wunderbaum - deferred
3056
3180
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3057
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3181
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
3058
3182
  */
3059
3183
  /**
3060
3184
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -3107,7 +3231,7 @@ class Deferred {
3107
3231
  /*!
3108
3232
  * Wunderbaum - wunderbaum_node
3109
3233
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3110
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3234
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
3111
3235
  */
3112
3236
  /** WunderbaumNode properties that can be passed with source data.
3113
3237
  * (Any other source properties will be stored as `node.data.PROP`.)
@@ -3158,7 +3282,7 @@ NODE_DICT_PROPS.delete("unselectable");
3158
3282
  */
3159
3283
  class WunderbaumNode {
3160
3284
  constructor(tree, parent, data) {
3161
- var _a, _b;
3285
+ var _a;
3162
3286
  /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
3163
3287
  * times within a tree (in this case we have 'clone nodes').
3164
3288
  * @see Use {@link setKey} to modify.
@@ -3188,8 +3312,8 @@ class WunderbaumNode {
3188
3312
  assert(!data.children, "'children' not allowed here");
3189
3313
  this.tree = tree;
3190
3314
  this.parent = parent;
3191
- this.key = "" + ((_a = data.key) !== null && _a !== void 0 ? _a : ++WunderbaumNode.sequence);
3192
- this.title = "" + ((_b = data.title) !== null && _b !== void 0 ? _b : "<" + this.key + ">");
3315
+ this.key = tree._calculateKey(data, parent);
3316
+ this.title = "" + ((_a = data.title) !== null && _a !== void 0 ? _a : "<" + this.key + ">");
3193
3317
  this.expanded = !!data.expanded;
3194
3318
  this.lazy = !!data.lazy;
3195
3319
  // We set the following node properties only if a matching data value is
@@ -3310,8 +3434,14 @@ class WunderbaumNode {
3310
3434
  const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel;
3311
3435
  for (const child of nodeData) {
3312
3436
  const subChildren = child.children;
3437
+ // Remove children property from source data because it should not be
3438
+ // passed to the constructor of WunderbaumNode:
3313
3439
  delete child.children;
3314
3440
  const n = new WunderbaumNode(tree, this, child);
3441
+ // Set `children` property again, so it can be used in `reload()`
3442
+ if (subChildren != null) {
3443
+ child.children = subChildren;
3444
+ }
3315
3445
  if (forceExpand && !n.isUnloaded()) {
3316
3446
  n.expanded = true;
3317
3447
  }
@@ -3727,15 +3857,12 @@ class WunderbaumNode {
3727
3857
  }
3728
3858
  return l;
3729
3859
  }
3730
- /** Return a string representing the hierachical node path, e.g. "a/b/c".
3860
+ /** Return a string representing the hierarchical node path, e.g. "a/b/c".
3731
3861
  * @param includeSelf
3732
3862
  * @param part property name or callback
3733
3863
  * @param separator
3734
3864
  */
3735
3865
  getPath(includeSelf = true, part = "title", separator = "/") {
3736
- // includeSelf = includeSelf !== false;
3737
- // part = part || "title";
3738
- // separator = separator || "/";
3739
3866
  let val;
3740
3867
  const path = [];
3741
3868
  const isFunc = typeof part === "function";
@@ -3750,7 +3877,7 @@ class WunderbaumNode {
3750
3877
  }, includeSelf);
3751
3878
  return path.join(separator);
3752
3879
  }
3753
- /** Return the preceeding node (under the same parent) or null. */
3880
+ /** Return the preceding node (under the same parent) or null. */
3754
3881
  getPrevSibling() {
3755
3882
  const ac = this.parent.children;
3756
3883
  const idx = ac.indexOf(this);
@@ -3779,7 +3906,7 @@ class WunderbaumNode {
3779
3906
  hasClass(className) {
3780
3907
  return this.classes ? this.classes.has(className) : false;
3781
3908
  }
3782
- /** Return true if node ist the currently focused node. @since 0.9.0 */
3909
+ /** Return true if node is the currently focused node. @since 0.9.0 */
3783
3910
  hasFocus() {
3784
3911
  return this.tree.focusNode === this;
3785
3912
  }
@@ -3834,7 +3961,7 @@ class WunderbaumNode {
3834
3961
  * an expand operation is currently possible.
3835
3962
  */
3836
3963
  isExpandable(andCollapsed = false) {
3837
- // `false` is never expandable (unoffical)
3964
+ // `false` is never expandable (unofficial)
3838
3965
  if ((andCollapsed && this.expanded) || this.children === false) {
3839
3966
  return false;
3840
3967
  }
@@ -3889,7 +4016,7 @@ class WunderbaumNode {
3889
4016
  isParentOf(other) {
3890
4017
  return other && other.parent === this;
3891
4018
  }
3892
- /** (experimental) Return true if this node is partially loaded. */
4019
+ /** Return true if this node is partially loaded. @experimental */
3893
4020
  isPartload() {
3894
4021
  return !!this._partload;
3895
4022
  }
@@ -3897,11 +4024,11 @@ class WunderbaumNode {
3897
4024
  isPartsel() {
3898
4025
  return !this.selected && !!this._partsel;
3899
4026
  }
3900
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4027
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3901
4028
  isRadio() {
3902
4029
  return !!this.parent.radiogroup || this.getOption("checkbox") === "radio";
3903
4030
  }
3904
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4031
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3905
4032
  isRendered() {
3906
4033
  return !!this._rowElem;
3907
4034
  }
@@ -4347,10 +4474,11 @@ class WunderbaumNode {
4347
4474
  * @param options
4348
4475
  */
4349
4476
  async navigate(where, options) {
4477
+ var _a;
4350
4478
  // Allow to pass 'ArrowLeft' instead of 'left'
4351
- where = KEY_TO_ACTION_DICT[where] || where;
4479
+ const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where);
4352
4480
  // Otherwise activate or focus the related node
4353
- const node = this.findRelatedNode(where);
4481
+ const node = this.findRelatedNode(navType);
4354
4482
  if (!node) {
4355
4483
  this.logWarn(`Could not find related node '${where}'.`);
4356
4484
  return Promise.resolve(this);
@@ -4447,86 +4575,17 @@ class WunderbaumNode {
4447
4575
  renderColInfosById: renderColInfosById,
4448
4576
  };
4449
4577
  }
4450
- _createIcon(iconMap, parentElem, replaceChild, showLoading) {
4451
- let iconSpan;
4452
- let icon = this.getOption("icon");
4453
- if (this._errorInfo) {
4454
- icon = iconMap.error;
4455
- }
4456
- else if (this._isLoading && showLoading) {
4457
- // Status nodes, or nodes without expander (< minExpandLevel) should
4458
- // display the 'loading' status with the i.wb-icon span
4459
- icon = iconMap.loading;
4460
- }
4461
- if (icon === false) {
4462
- return null; // explicitly disabled: don't try default icons
4463
- }
4464
- if (typeof icon === "string") ;
4465
- else if (this.statusNodeType) {
4466
- icon = iconMap[this.statusNodeType];
4467
- }
4468
- else if (this.expanded) {
4469
- icon = iconMap.folderOpen;
4470
- }
4471
- else if (this.children) {
4472
- icon = iconMap.folder;
4473
- }
4474
- else if (this.lazy) {
4475
- icon = iconMap.folderLazy;
4476
- }
4477
- else {
4478
- icon = iconMap.doc;
4479
- }
4480
- // this.log("_createIcon: " + icon);
4481
- if (!icon) {
4482
- iconSpan = document.createElement("i");
4483
- iconSpan.className = "wb-icon";
4484
- }
4485
- else if (icon.indexOf("<") >= 0) {
4486
- // HTML
4487
- iconSpan = elemFromHtml(icon);
4488
- }
4489
- else if (TEST_IMG.test(icon)) {
4490
- // Image URL
4491
- iconSpan = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
4492
- }
4493
- else {
4494
- // Class name
4495
- iconSpan = document.createElement("i");
4496
- iconSpan.className = "wb-icon " + icon;
4497
- }
4498
- if (replaceChild) {
4499
- parentElem.replaceChild(iconSpan, replaceChild);
4500
- }
4501
- else {
4502
- parentElem.appendChild(iconSpan);
4503
- }
4504
- // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
4505
- const cbRes = this._callEvent("iconBadge", { iconSpan: iconSpan });
4506
- let badge = null;
4507
- if (cbRes != null && cbRes !== false) {
4508
- let classes = "";
4509
- let tooltip = "";
4510
- if (isPlainObject(cbRes)) {
4511
- badge = "" + cbRes.badge;
4512
- classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
4513
- tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
4514
- }
4515
- else if (typeof cbRes === "number") {
4516
- badge = "" + cbRes;
4578
+ _createIcon(parentElem, replaceChild, showLoading) {
4579
+ const iconElem = this.tree._createNodeIcon(this, showLoading, true);
4580
+ if (iconElem) {
4581
+ if (replaceChild) {
4582
+ parentElem.replaceChild(iconElem, replaceChild);
4517
4583
  }
4518
4584
  else {
4519
- badge = cbRes; // string or HTMLSpanElement
4520
- }
4521
- if (typeof badge === "string") {
4522
- badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
4523
- }
4524
- if (badge) {
4525
- iconSpan.append(badge);
4585
+ parentElem.appendChild(iconElem);
4526
4586
  }
4527
4587
  }
4528
- // this.log("_createIcon: ", iconSpan);
4529
- return iconSpan;
4588
+ return iconElem;
4530
4589
  }
4531
4590
  /**
4532
4591
  * Create a whole new `<div class="wb-row">` element.
@@ -4581,7 +4640,7 @@ class WunderbaumNode {
4581
4640
  }
4582
4641
  // Render the icon (show a 'loading' icon if we do not have an expander that
4583
4642
  // we would prefer).
4584
- const iconSpan = this._createIcon(tree.iconMap, nodeElem, null, !expanderSpan);
4643
+ const iconSpan = this._createIcon(nodeElem, null, !expanderSpan);
4585
4644
  if (iconSpan) {
4586
4645
  ofsTitlePx += ICON_WIDTH;
4587
4646
  }
@@ -4724,9 +4783,9 @@ class WunderbaumNode {
4724
4783
  const typeInfo = this.type ? tree.types[this.type] : null;
4725
4784
  const rowDiv = this._rowElem;
4726
4785
  // Row markup already exists
4727
- const nodeElem = rowDiv.querySelector("span.wb-node");
4728
- const expanderSpan = nodeElem.querySelector("i.wb-expander");
4729
- const checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
4786
+ const nodeSpan = rowDiv.querySelector("span.wb-node");
4787
+ const expanderElem = nodeSpan.querySelector("i.wb-expander");
4788
+ const checkboxElem = nodeSpan.querySelector("i.wb-checkbox");
4730
4789
  const rowClasses = ["wb-row"];
4731
4790
  this.expanded ? rowClasses.push("wb-expanded") : 0;
4732
4791
  this.lazy ? rowClasses.push("wb-lazy") : 0;
@@ -4751,7 +4810,7 @@ class WunderbaumNode {
4751
4810
  if (typeInfo && typeInfo.classes) {
4752
4811
  rowDiv.classList.add(...typeInfo.classes);
4753
4812
  }
4754
- if (expanderSpan) {
4813
+ if (expanderElem) {
4755
4814
  let image = null;
4756
4815
  if (this._isLoading) {
4757
4816
  image = iconMap.loading;
@@ -4768,16 +4827,20 @@ class WunderbaumNode {
4768
4827
  image = iconMap.expanderLazy;
4769
4828
  }
4770
4829
  if (image == null) {
4771
- expanderSpan.classList.add("wb-indent");
4830
+ expanderElem.className = "wb-expander";
4831
+ expanderElem.classList.add("wb-indent");
4832
+ }
4833
+ else if (TEST_HTML.test(image)) {
4834
+ expanderElem.replaceWith(elemFromHtml(image));
4772
4835
  }
4773
- else if (TEST_IMG.test(image)) {
4774
- expanderSpan.style.backgroundImage = `url('${image}')`;
4836
+ else if (TEST_FILE_PATH.test(image)) {
4837
+ expanderElem.style.backgroundImage = `url('${image}')`;
4775
4838
  }
4776
4839
  else {
4777
- expanderSpan.className = "wb-expander " + image;
4840
+ expanderElem.className = "wb-expander " + image;
4778
4841
  }
4779
4842
  }
4780
- if (checkboxSpan) {
4843
+ if (checkboxElem) {
4781
4844
  let cbclass = "wb-checkbox ";
4782
4845
  if (this.isRadio()) {
4783
4846
  cbclass += "wb-radio ";
@@ -4801,7 +4864,7 @@ class WunderbaumNode {
4801
4864
  cbclass += iconMap.checkUnchecked;
4802
4865
  }
4803
4866
  }
4804
- checkboxSpan.className = cbclass;
4867
+ checkboxElem.className = cbclass;
4805
4868
  }
4806
4869
  // Fix active cell in cell-nav mode
4807
4870
  if (!opts.isNew) {
@@ -4811,9 +4874,9 @@ class WunderbaumNode {
4811
4874
  colSpan.classList.remove("wb-error", "wb-invalid");
4812
4875
  }
4813
4876
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4814
- const iconSpan = nodeElem.querySelector("i.wb-icon");
4877
+ const iconSpan = nodeSpan.querySelector("i.wb-icon");
4815
4878
  if (iconSpan) {
4816
- this._createIcon(tree.iconMap, nodeElem, iconSpan, !expanderSpan);
4879
+ this._createIcon(nodeSpan, iconSpan, !expanderElem);
4817
4880
  }
4818
4881
  }
4819
4882
  // Adjust column width
@@ -5118,6 +5181,32 @@ class WunderbaumNode {
5118
5181
  setKey(key, refKey) {
5119
5182
  throw new Error("Not yet implemented");
5120
5183
  }
5184
+ // /**
5185
+ // * Calculate a *stable*, unique key for this node from its refKey (or title).
5186
+ // * We also add information from the parent, because a refKey may occur multiple
5187
+ // * times in a tree.
5188
+ // */
5189
+ // calcUniqueKey() {
5190
+ // // Assuming that the parent's key was calculated the same way, we implicitly
5191
+ // // involve the whole refKey-path:
5192
+ // const s = this.key + (this.refKey || this.title);
5193
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5194
+ // // https://security.stackexchange.com/q/209882/207588
5195
+ // const h1 = util.murmurHash3(s, true);
5196
+ // return "id_" + h1 + util.murmurHash3(h1 + s, true);
5197
+ // // const l = [];
5198
+ // // // eslint-disable-next-line @typescript-eslint/no-this-alias
5199
+ // // let node: WunderbaumNode = this;
5200
+ // // while (node.parent) {
5201
+ // // l.unshift(node.refKey || node.key);
5202
+ // // node = node.parent;
5203
+ // // }
5204
+ // // const path = l.join("/");
5205
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5206
+ // // https://security.stackexchange.com/q/209882/207588
5207
+ // // const h1 = util.murmurHash3(path, true);
5208
+ // // return "id_" + h1 + util.murmurHash3(h1 + path, true);
5209
+ // }
5121
5210
  /**
5122
5211
  * Trigger a repaint, typically after a status or data change.
5123
5212
  *
@@ -5149,6 +5238,23 @@ class WunderbaumNode {
5149
5238
  });
5150
5239
  return nodeList;
5151
5240
  }
5241
+ /**
5242
+ * Return an array of refKey values.
5243
+ *
5244
+ * RefKeys are unique identifiers for a node data, and are used to identify
5245
+ * clones.
5246
+ * If more than one node has the same refKey, it is only returned once.
5247
+ * @param selected if true, only return refKeys of selected nodes.
5248
+ */
5249
+ getRefKeys(selected = false) {
5250
+ const refKeys = new Set();
5251
+ this.visit((node) => {
5252
+ if (node.refKey != null && (!selected || node.selected)) {
5253
+ refKeys.add(node.refKey);
5254
+ }
5255
+ });
5256
+ return Array.from(refKeys);
5257
+ }
5152
5258
  /** Toggle the check/uncheck state. */
5153
5259
  toggleSelected(options) {
5154
5260
  let flag = this.isSelected();
@@ -5328,9 +5434,11 @@ class WunderbaumNode {
5328
5434
  if (selectMode === "hier") {
5329
5435
  this.fixSelection3AfterClick();
5330
5436
  }
5331
- else if (selectMode === "single") {
5437
+ else if (selectMode === "single" && flag) {
5332
5438
  tree.visit((n) => {
5333
- n.selected = false;
5439
+ if (n !== this) {
5440
+ n.selected = false;
5441
+ }
5334
5442
  });
5335
5443
  }
5336
5444
  }
@@ -5362,7 +5470,7 @@ class WunderbaumNode {
5362
5470
  assert(data.statusNodeType, "Not a status node");
5363
5471
  assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node");
5364
5472
  statusNode = this.addNode(data, "prependChild");
5365
- statusNode.match = true;
5473
+ statusNode.match = -1; // Mark as 'match' to avoid hiding
5366
5474
  tree.update(ChangeType.structure);
5367
5475
  return statusNode;
5368
5476
  };
@@ -5432,30 +5540,16 @@ class WunderbaumNode {
5432
5540
  this.tooltip = tooltip;
5433
5541
  this.update();
5434
5542
  }
5435
- _sortChildren(cmp, deep) {
5436
- const cl = this.children;
5437
- if (!cl) {
5438
- return;
5439
- }
5440
- cl.sort(cmp);
5441
- if (deep) {
5442
- for (let i = 0, l = cl.length; i < l; i++) {
5443
- if (cl[i].children) {
5444
- cl[i]._sortChildren(cmp, deep);
5445
- }
5446
- }
5447
- }
5448
- }
5449
5543
  /**
5450
5544
  * Sort child list by title or custom criteria.
5451
5545
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
5452
5546
  * (defaults to sorting by title).
5453
5547
  * @param {boolean} deep pass true to sort all descendant nodes recursively
5548
+ * @deprecated use {@link sort}
5454
5549
  */
5455
5550
  sortChildren(cmp = nodeTitleSorter, deep = false) {
5456
- this._sortChildren(cmp || nodeTitleSorter, deep);
5457
- this.tree.update(ChangeType.structure);
5458
- // this.triggerModify("sort"); // TODO
5551
+ this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" });
5552
+ return this.sort({ cmp: cmp ? cmp : undefined, deep: deep });
5459
5553
  }
5460
5554
  /**
5461
5555
  * Renumber nodes `_nativeIndex`. This is useful to allow to restore the
@@ -5477,74 +5571,142 @@ class WunderbaumNode {
5477
5571
  /**
5478
5572
  * Convenience method to implement column sorting.
5479
5573
  * @since 0.11.0
5574
+ * @deprecated use {@link sort}
5480
5575
  */
5481
5576
  sortByProperty(options) {
5482
- var _a, _b, _c;
5483
- const { caseInsensitive = true, deep = true, nativeOrderPropName = "_nativeIndex", updateColInfo = false, } = options;
5484
- let order;
5485
- let colDef;
5577
+ this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" });
5578
+ return this.sort(options);
5579
+ }
5580
+ /**
5581
+ * Implement column sorting.
5582
+ * @since 0.14.0
5583
+ */
5584
+ sort(options) {
5585
+ const tree = this.tree;
5586
+ let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined,
5587
+ // Support click on column sort header:
5588
+ updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options;
5589
+ propName !== null && propName !== void 0 ? propName : (propName = colId);
5590
+ if (propName === "*") {
5591
+ propName = "title";
5592
+ }
5593
+ const isFolder = tree.options.sortFoldersFirst === true
5594
+ ? (node) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER
5595
+ : tree.options.sortFoldersFirst;
5486
5596
  if (updateColInfo) {
5487
- colDef = this.tree["_columnsById"][options.colId];
5597
+ const colDef = this.tree["_columnsById"][options.colId];
5488
5598
  assert(colDef, `Invalid colId specified: ${options.colId}`);
5489
- order =
5490
- (_a = options.order) !== null && _a !== void 0 ? _a : rotate(colDef.sortOrder, ["asc", "desc", undefined]);
5599
+ order !== null && order !== void 0 ? order : (order = rotate(colDef.sortOrder, ["asc", "desc", undefined]));
5491
5600
  for (const col of this.tree.columns) {
5492
5601
  col.sortOrder = col === colDef ? order : undefined;
5493
5602
  }
5603
+ if (order === undefined) {
5604
+ propName = nativeOrderPropName;
5605
+ order = "asc";
5606
+ }
5494
5607
  this.tree.update(ChangeType.colStructure);
5495
5608
  }
5496
5609
  else {
5497
- order = (_b = options.order) !== null && _b !== void 0 ? _b : "asc";
5610
+ propName !== null && propName !== void 0 ? propName : (propName = "title");
5611
+ order !== null && order !== void 0 ? order : (order = "asc");
5612
+ }
5613
+ this.logDebug(`sort(), propName=${propName}, ${order}`, options);
5614
+ assert(propName || cmp || key, "No `propName` or `key` specified");
5615
+ // Define a key callback from the parameters we have
5616
+ if (key == null && cmp == null) {
5617
+ key = (node) => {
5618
+ let val;
5619
+ if (NODE_DICT_PROPS.has(propName)) {
5620
+ val = node[propName];
5621
+ }
5622
+ else {
5623
+ val = node.data[propName];
5624
+ }
5625
+ if (caseInsensitive && typeof val === "string") {
5626
+ val = val.toLowerCase();
5627
+ }
5628
+ return val;
5629
+ };
5498
5630
  }
5499
- let propName = (_c = options.propName) !== null && _c !== void 0 ? _c : (options.colId || "");
5500
- if (propName === "*") {
5501
- propName = "title";
5631
+ // Define a compare callback that uses the key callback
5632
+ if (cmp) {
5633
+ assert(!key, "`key` and `cmp` are mutually exclusive");
5634
+ tree.logDeprecate("SortOptions.cmp", {
5635
+ since: "0.14.0",
5636
+ hint: "use the `key` callback instead",
5637
+ });
5502
5638
  }
5503
- if (order == null) {
5504
- propName = nativeOrderPropName;
5505
- order = "asc";
5639
+ else {
5640
+ if (options.propName || options.caseInsensitive) {
5641
+ tree.logWarn("sort(): ignoring propName, caseInsensitive");
5642
+ }
5643
+ cmp = (a, b) => {
5644
+ if (isFolder) {
5645
+ const isFolderA = isFolder(a);
5646
+ if (isFolderA !== isFolder(b)) {
5647
+ return isFolderA ? -1 : 1;
5648
+ }
5649
+ }
5650
+ let x = key(a);
5651
+ let y = key(b);
5652
+ // Assure we have reasonable comparisons with null values:
5653
+ if (x == null) {
5654
+ x = typeof y === "string" ? "" : 0;
5655
+ }
5656
+ else if (typeof x === "boolean") {
5657
+ x = x ? 1 : 0;
5658
+ }
5659
+ if (y == null) {
5660
+ y = typeof x === "string" ? "" : 0;
5661
+ }
5662
+ else if (typeof y === "boolean") {
5663
+ y = y ? 1 : 0;
5664
+ }
5665
+ if (order === "desc") {
5666
+ return x === y ? 0 : x > y ? -1 : 1;
5667
+ }
5668
+ return x === y ? 0 : x > y ? 1 : -1;
5669
+ };
5506
5670
  }
5507
- this.logDebug(`sortByProperty(), propName=${propName}, ${order}`, options);
5508
- assert(propName, "No property name specified");
5509
- const cmp = (a, b) => {
5510
- let av, bv;
5511
- if (NODE_DICT_PROPS.has(propName)) {
5512
- av = a[propName];
5513
- bv = b[propName];
5514
- }
5515
- else {
5516
- av = a.data[propName];
5517
- bv = b.data[propName];
5518
- }
5519
- if (av == null && bv == null) {
5520
- return 0;
5521
- }
5522
- if (av == null) {
5523
- av = typeof bv === "string" ? "" : 0;
5524
- }
5525
- else if (typeof av === "boolean") {
5526
- av = av ? 1 : 0;
5527
- }
5528
- if (bv == null) {
5529
- bv = typeof av === "string" ? "" : 0;
5530
- }
5531
- else if (typeof bv === "boolean") {
5532
- bv = bv ? 1 : 0;
5671
+ function _sortChildren(cl) {
5672
+ if (!cl) {
5673
+ return;
5533
5674
  }
5534
- if (caseInsensitive) {
5535
- if (typeof av === "string") {
5536
- av = av.toLowerCase();
5537
- }
5538
- if (typeof bv === "string") {
5539
- bv = bv.toLowerCase();
5675
+ cl.sort(cmp);
5676
+ if (deep) {
5677
+ for (let i = 0, l = cl.length; i < l; i++) {
5678
+ if (cl[i].children) {
5679
+ _sortChildren(cl[i].children);
5680
+ }
5540
5681
  }
5541
5682
  }
5542
- if (order === "desc") {
5543
- return av === bv ? 0 : av > bv ? -1 : 1;
5683
+ }
5684
+ if (this.children) {
5685
+ _sortChildren(this.children);
5686
+ }
5687
+ this.tree.update(ChangeType.structure);
5688
+ // this.triggerModify("sort"); // TODO
5689
+ }
5690
+ /**
5691
+ * Re-apply current sorting if any (use after lazy load).
5692
+ * Example:
5693
+ * ```js
5694
+ * load: function (e) {
5695
+ * // Whe loading a lazy branch, apply current sort order if any
5696
+ * e.node.resort();
5697
+ * },
5698
+ * ```
5699
+ * @since 0.14.0
5700
+ */
5701
+ resort(options = {}) {
5702
+ for (const colDef of this.tree.columns) {
5703
+ if (colDef.sortOrder) {
5704
+ options.colId = colDef.id;
5705
+ options.order = colDef.sortOrder;
5706
+ this.sort(options);
5707
+ break;
5544
5708
  }
5545
- return av === bv ? 0 : av > bv ? 1 : -1;
5546
- };
5547
- return this.sortChildren(cmp, deep);
5709
+ }
5548
5710
  }
5549
5711
  /**
5550
5712
  * Trigger `modifyChild` event on a parent to signal that a child was modified.
@@ -5581,7 +5743,8 @@ class WunderbaumNode {
5581
5743
  * @param {function} callback the callback function.
5582
5744
  * Return false to stop iteration, return "skip" to skip this node and
5583
5745
  * its children only.
5584
- * @see {@link IterableIterator<WunderbaumNode>}, {@link Wunderbaum.visit}.
5746
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
5747
+ * @see {@link Wunderbaum.visit}.
5585
5748
  */
5586
5749
  visit(callback, includeSelf = false) {
5587
5750
  let res = true;
@@ -5654,7 +5817,7 @@ WunderbaumNode.sequence = 0;
5654
5817
  /*!
5655
5818
  * Wunderbaum - ext-edit
5656
5819
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
5657
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
5820
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
5658
5821
  */
5659
5822
  // const START_MARKER = "\uFFF7";
5660
5823
  class EditExtension extends WunderbaumExtension {
@@ -5889,7 +6052,7 @@ class EditExtension extends WunderbaumExtension {
5889
6052
  newValue = newValue.trim();
5890
6053
  }
5891
6054
  if (!node) {
5892
- this.tree.logDebug("stopEditTitle: not in edit mode.");
6055
+ // this.tree.logDebug("stopEditTitle: not in edit mode.");
5893
6056
  return;
5894
6057
  }
5895
6058
  node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue);
@@ -5973,7 +6136,7 @@ class EditExtension extends WunderbaumExtension {
5973
6136
  newNode.setClass("wb-edit-new");
5974
6137
  this.relatedNode = node;
5975
6138
  // Don't filter new nodes:
5976
- newNode.match = true;
6139
+ newNode.match = -1;
5977
6140
  newNode.makeVisible({ noAnimation: true }).then(() => {
5978
6141
  this.startEditTitle(newNode);
5979
6142
  });
@@ -5989,8 +6152,8 @@ class EditExtension extends WunderbaumExtension {
5989
6152
  * https://github.com/mar10/wunderbaum
5990
6153
  *
5991
6154
  * Released under the MIT license.
5992
- * @version v0.12.1
5993
- * @date Sat, 22 Feb 2025 22:59:20 GMT
6155
+ * @version v0.14.0
6156
+ * @date Fri, 20 Mar 2026 16:58:31 GMT
5994
6157
  */
5995
6158
  // import "./wunderbaum.scss";
5996
6159
  class WbSystemRoot extends WunderbaumNode {
@@ -6039,18 +6202,21 @@ class Wunderbaum {
6039
6202
  this._disableUpdateIgnoreCount = 0;
6040
6203
  this._activeNode = null;
6041
6204
  this._focusNode = null;
6205
+ this._initialSource = null;
6042
6206
  /** Shared properties, referenced by `node.type`. */
6043
6207
  this.types = {};
6044
6208
  /** List of column definitions. */
6045
- this.columns = []; // any[] = [];
6209
+ this.columns = [];
6046
6210
  this._columnsById = {};
6047
6211
  // Modification Status
6048
6212
  this.pendingChangeTypes = new Set();
6049
6213
  /** Expose some useful methods of the util.ts module as `tree._util`. */
6050
6214
  this._util = util;
6051
6215
  // --- SELECT ---
6052
- // /** @internal */
6053
6216
  // public selectRangeAnchor: WunderbaumNode | null = null;
6217
+ // --- BREADCRUMB ---
6218
+ /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6219
+ this.breadcrumb = null;
6054
6220
  // --- FILTER ---
6055
6221
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6056
6222
  this.filterMode = null;
@@ -6065,45 +6231,57 @@ class Wunderbaum {
6065
6231
  this.lastQuicksearchTerm = "";
6066
6232
  // --- EDIT ---
6067
6233
  this.lastClickTime = 0;
6068
- const opts = (this.options = extend({
6069
- id: null,
6070
- source: null, // URL for GET/PUT, Ajax options, or callback
6071
- element: null, // <div class="wunderbaum">
6234
+ // Set default options and merge with user options
6235
+ const initOptions = Object.assign({
6236
+ id: undefined,
6237
+ source: [], // URL for GET/PUT, Ajax options, or callback
6238
+ element: unsafeCast(null),
6072
6239
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
6073
6240
  header: null, // Show/hide header (pass bool or string)
6074
- // headerHeightPx: ROW_HEIGHT,
6075
6241
  rowHeightPx: DEFAULT_ROW_HEIGHT,
6076
6242
  iconMap: "bootstrap",
6077
- columns: null,
6078
- types: null,
6079
- // escapeTitles: true,
6243
+ columns: [], //util.unsafeCast<ColumnDefinitionList>(null),
6244
+ types: {},
6080
6245
  enabled: true,
6081
6246
  fixedCol: false,
6082
6247
  showSpinner: false,
6083
6248
  checkbox: false,
6084
6249
  minExpandLevel: 0,
6085
6250
  emptyChildListExpandable: false,
6086
- // updateThrottleWait: 200,
6087
6251
  skeleton: false,
6088
- connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
6252
+ autoCollapse: false,
6253
+ adjustHeight: true,
6254
+ connectTopBreadcrumb: null,
6255
+ columnsFilterable: false,
6256
+ columnsMenu: false,
6257
+ columnsResizable: false,
6258
+ columnsSortable: false,
6089
6259
  selectMode: "multi", // SelectModeType
6260
+ scrollIntoViewOnExpandClick: true,
6261
+ // --- Extensions (actually set by exensions on init)
6262
+ dnd: unsafeCast(null),
6263
+ edit: unsafeCast(null),
6264
+ filter: unsafeCast(null),
6090
6265
  // --- KeyNav ---
6091
- navigationModeOption: null, // NavModeEnum.startRow,
6266
+ navigationModeOption: unsafeCast(null),
6092
6267
  quicksearch: true,
6093
6268
  // --- Events ---
6094
- iconBadge: null,
6095
- change: null,
6096
- // enhanceTitle: null,
6097
- error: null,
6098
- receive: null,
6269
+ // iconBadge: null,
6270
+ // change: null,
6271
+ // ...
6099
6272
  // --- Strings ---
6100
6273
  strings: {
6101
6274
  loadError: "Error",
6102
6275
  loading: "Loading...",
6103
- // loading: "Loading&hellip;",
6104
6276
  noData: "No data",
6277
+ breadcrumbDelimiter: " » ",
6278
+ queryResult: "Found ${matches} of ${count}",
6279
+ noMatch: "No results",
6280
+ matchIndex: "${match} of ${matches}",
6105
6281
  },
6106
- }, options));
6282
+ }, options);
6283
+ const opts = initOptions;
6284
+ this.options = opts;
6107
6285
  const readyDeferred = new Deferred();
6108
6286
  this.ready = readyDeferred.promise();
6109
6287
  let readyOk = false;
@@ -6130,7 +6308,8 @@ class Wunderbaum {
6130
6308
  this._callEvent("init", { error: err });
6131
6309
  }
6132
6310
  });
6133
- this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
6311
+ this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence;
6312
+ delete initOptions.id;
6134
6313
  this.root = new WbSystemRoot(this);
6135
6314
  this._registerExtension(new KeynavExtension(this));
6136
6315
  this._registerExtension(new EditExtension(this));
@@ -6140,19 +6319,20 @@ class Wunderbaum {
6140
6319
  this._registerExtension(new LoggerExtension(this));
6141
6320
  this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {});
6142
6321
  // --- Evaluate options
6143
- this.columns = opts.columns;
6144
- delete opts.columns;
6322
+ this.columns = initOptions.columns || [];
6323
+ delete initOptions.columns;
6145
6324
  if (!this.columns || !this.columns.length) {
6146
6325
  const title = typeof opts.header === "string" ? opts.header : this.id;
6147
6326
  this.columns = [{ id: "*", title: title, width: "*" }];
6148
6327
  }
6149
- if (opts.types) {
6150
- this.setTypes(opts.types, true);
6328
+ if (initOptions.types) {
6329
+ this.setTypes(initOptions.types, true);
6151
6330
  }
6152
- delete opts.types;
6331
+ delete initOptions.types;
6153
6332
  // --- Create Markup
6154
- this.element = elemFromSelector(opts.element);
6155
- assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
6333
+ this.element = elemFromSelector(initOptions.element);
6334
+ assert(!!this.element, `Invalid 'element' option: ${initOptions.element}`);
6335
+ delete initOptions.element;
6156
6336
  this.element.classList.add("wunderbaum");
6157
6337
  if (!this.element.getAttribute("tabindex")) {
6158
6338
  this.element.tabIndex = 0;
@@ -6207,6 +6387,19 @@ class Wunderbaum {
6207
6387
  this.headerElement =
6208
6388
  this.element.querySelector("div.wb-header");
6209
6389
  this.element.classList.toggle("wb-grid", this.columns.length > 1);
6390
+ if (this.options.connectTopBreadcrumb) {
6391
+ this.breadcrumb = elemFromSelector(this.options.connectTopBreadcrumb);
6392
+ assert(!this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`);
6393
+ this.breadcrumb.addEventListener("click", (e) => {
6394
+ // const node = Wunderbaum.getNode(e)!;
6395
+ const elem = e.target;
6396
+ if (elem && elem.matches("a.wb-breadcrumb")) {
6397
+ const node = this.keyMap.get(elem.dataset.key);
6398
+ node === null || node === void 0 ? void 0 : node.setActive();
6399
+ e.preventDefault();
6400
+ }
6401
+ });
6402
+ }
6210
6403
  this._initExtensions();
6211
6404
  // --- apply initial options
6212
6405
  ["enabled", "fixedCol"].forEach((optName) => {
@@ -6215,11 +6408,11 @@ class Wunderbaum {
6215
6408
  }
6216
6409
  });
6217
6410
  // --- Load initial data
6218
- if (opts.source) {
6411
+ if (initOptions.source) {
6219
6412
  if (opts.showSpinner) {
6220
6413
  this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
6221
6414
  }
6222
- this.load(opts.source)
6415
+ this.load(initOptions.source)
6223
6416
  .then(() => {
6224
6417
  // The source may have defined columns, so we may adjust the nav mode
6225
6418
  if (opts.navigationModeOption == null) {
@@ -6252,15 +6445,18 @@ class Wunderbaum {
6252
6445
  // has a wrong value at start???
6253
6446
  this.update(ChangeType.any);
6254
6447
  // --- Bind listeners
6255
- this.element.addEventListener("scroll", (e) => {
6256
- // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6257
- this.update(ChangeType.scroll);
6258
- });
6448
+ this._registerEventHandlers();
6259
6449
  this.resizeObserver = new ResizeObserver((entries) => {
6260
6450
  // this.log("ResizeObserver: Size changed", entries);
6261
6451
  this.update(ChangeType.resize);
6262
6452
  });
6263
6453
  this.resizeObserver.observe(this.element);
6454
+ }
6455
+ _registerEventHandlers() {
6456
+ this.element.addEventListener("scroll", (e) => {
6457
+ // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6458
+ this.update(ChangeType.scroll);
6459
+ });
6264
6460
  onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
6265
6461
  var _a, _b;
6266
6462
  const info = Wunderbaum.getEventInfo(e);
@@ -6276,9 +6472,6 @@ class Wunderbaum {
6276
6472
  const node = info.node;
6277
6473
  const mouseEvent = e;
6278
6474
  // this.log("click", info);
6279
- // if (this._selectRange(info) === false) {
6280
- // return;
6281
- // }
6282
6475
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
6283
6476
  this.lastClickTime = Date.now();
6284
6477
  return false;
@@ -6297,20 +6490,22 @@ class Wunderbaum {
6297
6490
  (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) {
6298
6491
  node.startEditTitle();
6299
6492
  }
6300
- if (info.colIdx >= 0) {
6301
- node.setActive(true, { colIdx: info.colIdx, event: e });
6302
- }
6303
- else {
6304
- node.setActive(true, { event: e });
6305
- }
6306
6493
  if (info.region === NodeRegion.expander) {
6307
6494
  node.setExpanded(!node.isExpanded(), {
6308
- scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
6495
+ scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
6309
6496
  });
6310
6497
  }
6311
6498
  else if (info.region === NodeRegion.checkbox) {
6312
6499
  node.toggleSelected();
6313
6500
  }
6501
+ else {
6502
+ if (info.colIdx >= 0) {
6503
+ node.setActive(true, { colIdx: info.colIdx, event: e });
6504
+ }
6505
+ else {
6506
+ node.setActive(true, { event: e });
6507
+ }
6508
+ }
6314
6509
  }
6315
6510
  this.lastClickTime = Date.now();
6316
6511
  });
@@ -6346,7 +6541,7 @@ class Wunderbaum {
6346
6541
  const targetNode = Wunderbaum.getNode(e);
6347
6542
  this._callEvent("focus", { flag: flag, event: e });
6348
6543
  if (flag && this.isRowNav() && !this.isEditingTitle()) {
6349
- if (opts.navigationModeOption === NavModeEnum.row) {
6544
+ if (this.options.navigationModeOption === NavModeEnum.row) {
6350
6545
  targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive();
6351
6546
  }
6352
6547
  else {
@@ -6413,11 +6608,12 @@ class Wunderbaum {
6413
6608
  }
6414
6609
  /**
6415
6610
  * Return the icon-function -> icon-definition mapping.
6611
+ * @deprecated Use {@link Wunderbaum.iconMaps}
6416
6612
  */
6417
6613
  get iconMap() {
6418
6614
  const map = this.options.iconMap;
6419
6615
  if (typeof map === "string") {
6420
- return iconMaps[map];
6616
+ return defaultIconMaps[map];
6421
6617
  }
6422
6618
  return map;
6423
6619
  }
@@ -6470,7 +6666,38 @@ class Wunderbaum {
6470
6666
  ext.init();
6471
6667
  }
6472
6668
  }
6473
- /** Add node to tree's bookkeeping data structures. */
6669
+ /**
6670
+ * Calculate a *stable*, unique key for a node from its refKey (or title).
6671
+ * We also add information from the parent, because a refKey may occur multiple
6672
+ * times in a tree (but not as child of the same parent).
6673
+ * @internal
6674
+ */
6675
+ _calculateKey(data, parent) {
6676
+ if (data.key) {
6677
+ // Always use an explicitly passed key
6678
+ return data.key;
6679
+ }
6680
+ // Auto-keys are optional, use a monotonic counter by default:
6681
+ if (!this.options.autoKeys) {
6682
+ return "" + ++WunderbaumNode.sequence;
6683
+ }
6684
+ // Add the parent's key to the hash. Assuming this was generated by the
6685
+ // same algorithm, this should incorporate the whole path:
6686
+ const s = (parent ? parent.key : "") + (data.refKey || data.title);
6687
+ // 32-bit has a high probability of collisions, so we pump up to 64-bit
6688
+ // https://security.stackexchange.com/q/209882/207588
6689
+ const h1 = murmurHash3(s, true);
6690
+ let key = "id_" + h1 + murmurHash3(h1 + s, true);
6691
+ // Check for collisions
6692
+ // (Most likely if the same title occurs multiple in the same parent).
6693
+ const existingNode = this.keyMap.get(key);
6694
+ if (existingNode) {
6695
+ key += "." + ++Wunderbaum.sequence;
6696
+ this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data);
6697
+ }
6698
+ return key;
6699
+ }
6700
+ /** Add node to tree's bookkeeping data structures. @internal */
6474
6701
  _registerNode(node) {
6475
6702
  const key = node.key;
6476
6703
  assert(key != null, `Missing key: '${node}'.`);
@@ -6487,7 +6714,7 @@ class Wunderbaum {
6487
6714
  }
6488
6715
  }
6489
6716
  }
6490
- /** Remove node from tree's bookkeeping data structures. */
6717
+ /** Remove node from tree's bookkeeping data structures. @internal */
6491
6718
  _unregisterNode(node) {
6492
6719
  // Remove refKey reference from map (if any)
6493
6720
  const rk = node.refKey;
@@ -6570,7 +6797,10 @@ class Wunderbaum {
6570
6797
  });
6571
6798
  return node;
6572
6799
  }
6573
- /** Return the topmost visible node in the viewport. */
6800
+ /** Return the topmost visible node in the viewport.
6801
+ * @param complete If `false`, the node is considered visible if at least one
6802
+ * pixel is visible.
6803
+ */
6574
6804
  getTopmostVpNode(complete = true) {
6575
6805
  const rowHeight = this.options.rowHeightPx;
6576
6806
  const gracePx = 1; // ignore subpixel scrolling
@@ -6603,7 +6833,7 @@ class Wunderbaum {
6603
6833
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6604
6834
  return this._getNodeByRowIdx(bottomIdx);
6605
6835
  }
6606
- /** Return preceeding visible node in the viewport. */
6836
+ /** Return preceding visible node in the viewport. */
6607
6837
  _getPrevNodeInView(node, ofs = 1) {
6608
6838
  this.visitRows((n) => {
6609
6839
  node = n;
@@ -6614,13 +6844,18 @@ class Wunderbaum {
6614
6844
  return node;
6615
6845
  }
6616
6846
  /** Return following visible node in the viewport. */
6617
- _getNextNodeInView(node, ofs = 1) {
6847
+ _getNextNodeInView(node, options) {
6848
+ let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
6849
+ const reverse = !!(options === null || options === void 0 ? void 0 : options.reverse);
6618
6850
  this.visitRows((n) => {
6619
6851
  node = n;
6852
+ if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) {
6853
+ return false;
6854
+ }
6620
6855
  if (ofs-- <= 0) {
6621
6856
  return false;
6622
6857
  }
6623
- }, { reverse: false, start: node || this.getActiveNode() });
6858
+ }, { reverse: reverse, start: node || this.getActiveNode() });
6624
6859
  return node;
6625
6860
  }
6626
6861
  /**
@@ -6740,9 +6975,11 @@ class Wunderbaum {
6740
6975
  case "first":
6741
6976
  case "last":
6742
6977
  case "left":
6978
+ case "nextMatch":
6743
6979
  case "pageDown":
6744
6980
  case "pageUp":
6745
6981
  case "parent":
6982
+ case "prevMatch":
6746
6983
  case "right":
6747
6984
  case "up":
6748
6985
  return node.navigate(cmd);
@@ -6857,22 +7094,39 @@ class Wunderbaum {
6857
7094
  /** Run code, but defer rendering of viewport until done.
6858
7095
  *
6859
7096
  * ```js
6860
- * tree.runWithDeferredUpdate(() => {
6861
- * return someFuncThatWouldUpdateManyNodes();
7097
+ * const res = tree.runWithDeferredUpdate(() => {
7098
+ * return someFunctionThatWouldUpdateManyNodes();
6862
7099
  * });
6863
7100
  * ```
6864
7101
  */
6865
- runWithDeferredUpdate(func, hint = null) {
7102
+ runWithDeferredUpdate(func) {
6866
7103
  try {
6867
7104
  this.enableUpdate(false);
6868
7105
  const res = func();
6869
- assert(!(res instanceof Promise), `Promise return not allowed: ${res}`);
7106
+ assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`);
6870
7107
  return res;
6871
7108
  }
6872
7109
  finally {
6873
7110
  this.enableUpdate(true);
6874
7111
  }
6875
7112
  }
7113
+ /** Run code, but defer rendering of viewport until done.
7114
+ *
7115
+ * ```js
7116
+ * const res = await tree.runWithDeferredUpdate(async () => {
7117
+ * return someAsyncFunctionThatWouldUpdateManyNodes();
7118
+ * });
7119
+ * ```
7120
+ */
7121
+ async runWithDeferredUpdateAsync(func) {
7122
+ try {
7123
+ this.enableUpdate(false);
7124
+ return await func();
7125
+ }
7126
+ finally {
7127
+ this.enableUpdate(true);
7128
+ }
7129
+ }
6876
7130
  /** Recursively expand all expandable nodes (triggers lazy load if needed). */
6877
7131
  async expandAll(flag = true, options) {
6878
7132
  await this.root.expandAll(flag, options);
@@ -6892,6 +7146,17 @@ class Wunderbaum {
6892
7146
  getSelectedNodes(stopOnParents = false) {
6893
7147
  return this.root.getSelectedNodes(stopOnParents);
6894
7148
  }
7149
+ /**
7150
+ * Return an array of refKey values.
7151
+ *
7152
+ * RefKeys are unique identifiers for a node data, and are used to identify
7153
+ * clones.
7154
+ * If more than one node has the same refKey, it is only returned once.
7155
+ * @param selected if true, only return refKeys of selected nodes.
7156
+ */
7157
+ getRefKeys(selected = false) {
7158
+ return this.root.getRefKeys(selected);
7159
+ }
6895
7160
  /*
6896
7161
  * Return an array of selected nodes.
6897
7162
  */
@@ -6934,6 +7199,11 @@ class Wunderbaum {
6934
7199
  count(visible = false) {
6935
7200
  return visible ? this.treeRowCount : this.keyMap.size;
6936
7201
  }
7202
+ /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`.
7203
+ */
7204
+ countUnique() {
7205
+ return this.refKeyMap.size;
7206
+ }
6937
7207
  /** @internal sanity check. */
6938
7208
  _check() {
6939
7209
  let i = 0;
@@ -6992,12 +7262,14 @@ class Wunderbaum {
6992
7262
  * and wrap-around at the end.
6993
7263
  * Used by quicksearch and keyboard navigation.
6994
7264
  */
6995
- findNextNode(match, startNode) {
7265
+ findNextNode(match, startNode, reverse = false) {
6996
7266
  //, visibleOnly) {
6997
7267
  let res = null;
6998
7268
  const firstNode = this.getFirstChild();
7269
+ // Last visible node (calculation is expensive, so do only if we need it):
7270
+ const lastNode = reverse ? this.findRelatedNode(firstNode, "last") : null;
6999
7271
  const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
7000
- startNode = startNode || firstNode;
7272
+ startNode = startNode || (reverse ? lastNode : firstNode);
7001
7273
  function _checkNode(n) {
7002
7274
  // console.log("_check " + n)
7003
7275
  if (matcher(n)) {
@@ -7010,12 +7282,14 @@ class Wunderbaum {
7010
7282
  this.visitRows(_checkNode, {
7011
7283
  start: startNode,
7012
7284
  includeSelf: false,
7285
+ reverse: reverse,
7013
7286
  });
7014
7287
  // Wrap around search
7015
7288
  if (!res && startNode !== firstNode) {
7016
7289
  this.visitRows(_checkNode, {
7017
- start: firstNode,
7290
+ start: reverse ? lastNode : firstNode,
7018
7291
  includeSelf: true,
7292
+ reverse: reverse,
7019
7293
  });
7020
7294
  }
7021
7295
  return res;
@@ -7082,7 +7356,7 @@ class Wunderbaum {
7082
7356
  // }
7083
7357
  break;
7084
7358
  case "up":
7085
- res = this._getPrevNodeInView(node);
7359
+ res = this._getNextNodeInView(node, { reverse: true });
7086
7360
  break;
7087
7361
  case "down":
7088
7362
  res = this._getNextNodeInView(node);
@@ -7095,7 +7369,10 @@ class Wunderbaum {
7095
7369
  res = bottomNode;
7096
7370
  }
7097
7371
  else {
7098
- res = this._getNextNodeInView(node, pageSize);
7372
+ res = this._getNextNodeInView(node, {
7373
+ reverse: false,
7374
+ ofs: pageSize,
7375
+ });
7099
7376
  }
7100
7377
  }
7101
7378
  break;
@@ -7110,10 +7387,23 @@ class Wunderbaum {
7110
7387
  res = topNode;
7111
7388
  }
7112
7389
  else {
7113
- res = this._getPrevNodeInView(node, pageSize);
7390
+ res = this._getNextNodeInView(node, {
7391
+ reverse: true,
7392
+ ofs: pageSize,
7393
+ });
7114
7394
  }
7115
7395
  }
7116
7396
  break;
7397
+ case "prevMatch":
7398
+ // fallthrough
7399
+ case "nextMatch":
7400
+ if (!this.isFilterActive) {
7401
+ this.logWarn(`${where}: Filter is not active.`);
7402
+ break;
7403
+ }
7404
+ res = this.findNextNode((n) => n.isMatched(), node, where === "prevMatch");
7405
+ res === null || res === void 0 ? void 0 : res.setActive();
7406
+ break;
7117
7407
  default:
7118
7408
  this.logWarn("Unknown relation '" + where + "'.");
7119
7409
  }
@@ -7148,6 +7438,18 @@ class Wunderbaum {
7148
7438
  format(name_cb, connectors) {
7149
7439
  return this.root.format(name_cb, connectors);
7150
7440
  }
7441
+ /**
7442
+ * Always returns null (so a tree instance behaves as `tree.root`).
7443
+ */
7444
+ get parent() {
7445
+ return null;
7446
+ }
7447
+ /**
7448
+ * Return a list of top-level nodes.
7449
+ */
7450
+ get children() {
7451
+ return this.root.children || [];
7452
+ }
7151
7453
  /**
7152
7454
  * Return the active cell (`span.wb-col`) of the currently active node or null.
7153
7455
  */
@@ -7175,6 +7477,12 @@ class Wunderbaum {
7175
7477
  getFirstChild() {
7176
7478
  return this.root.getFirstChild();
7177
7479
  }
7480
+ /**
7481
+ * Return the last top level node if any (not the invisible root node).
7482
+ */
7483
+ getLastChild() {
7484
+ return this.root.getLastChild();
7485
+ }
7178
7486
  /**
7179
7487
  * Return the node that currently has keyboard focus or null.
7180
7488
  * Alias for {@link Wunderbaum.focusNode}.
@@ -7260,7 +7568,7 @@ class Wunderbaum {
7260
7568
  }
7261
7569
  /** Return true if any node title or grid cell is currently beeing edited.
7262
7570
  *
7263
- * See also {@link Wunderbaum.isEditingTitle}.
7571
+ * See also {@link isEditingTitle}.
7264
7572
  */
7265
7573
  isEditing() {
7266
7574
  const focusElem = this.nodeListElement.querySelector("input:focus,select:focus");
@@ -7268,7 +7576,7 @@ class Wunderbaum {
7268
7576
  }
7269
7577
  /** Return true if any node is currently in edit-title mode.
7270
7578
  *
7271
- * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
7579
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
7272
7580
  */
7273
7581
  isEditingTitle() {
7274
7582
  return this._callMethod("edit.isEditingTitle");
@@ -7288,7 +7596,7 @@ class Wunderbaum {
7288
7596
  return res;
7289
7597
  }
7290
7598
  /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
7291
- * @see {@link Wunderbaum.logDebug}
7599
+ * @see {@link logDebug}
7292
7600
  */
7293
7601
  log(...args) {
7294
7602
  if (this.options.debugLevel >= 4) {
@@ -7297,7 +7605,7 @@ class Wunderbaum {
7297
7605
  }
7298
7606
  /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
7299
7607
  * and browser console level includes debug/verbose messages.
7300
- * @see {@link Wunderbaum.log}
7608
+ * @see {@link log}
7301
7609
  */
7302
7610
  logDebug(...args) {
7303
7611
  if (this.options.debugLevel >= 4) {
@@ -7335,6 +7643,19 @@ class Wunderbaum {
7335
7643
  console.warn(this.toString(), ...args); // eslint-disable-line no-console
7336
7644
  }
7337
7645
  }
7646
+ /** Emit a warning for deprecated methods. @internal */
7647
+ logDeprecate(method, options) {
7648
+ if (this.options.debugLevel >= 2) {
7649
+ let msg = `${this}: ${method} is deprecated`;
7650
+ if (options === null || options === void 0 ? void 0 : options.since) {
7651
+ msg += ` since ${options.since}`;
7652
+ }
7653
+ if (options === null || options === void 0 ? void 0 : options.hint) {
7654
+ msg += ` (${options.since})`;
7655
+ }
7656
+ console.warn(msg + "."); // eslint-disable-line no-console
7657
+ }
7658
+ }
7338
7659
  /** Reset column widths to default. @since 0.10.0 */
7339
7660
  resetColumns() {
7340
7661
  this.columns.forEach((col) => {
@@ -7502,6 +7823,69 @@ class Wunderbaum {
7502
7823
  _setFocusNode(node) {
7503
7824
  this._focusNode = node;
7504
7825
  }
7826
+ /** Return the current selection/expansion/activation status. @experimental */
7827
+ getState(options = {}) {
7828
+ var _a, _b;
7829
+ const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options;
7830
+ const expandSet = new Set();
7831
+ if (expandedKeys) {
7832
+ for (const node of this) {
7833
+ if (node.isExpanded() && node.hasChildren()) {
7834
+ expandSet.add(node.key);
7835
+ }
7836
+ }
7837
+ }
7838
+ // Parents of active node are always expanded
7839
+ if (activeKey && this.activeNode) {
7840
+ this.activeNode.visitParents((n) => {
7841
+ if (n.parent) {
7842
+ expandSet.add(n.key);
7843
+ }
7844
+ }, false);
7845
+ }
7846
+ const state = {
7847
+ expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
7848
+ activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
7849
+ activeColIdx: this.activeColIdx,
7850
+ selectedKeys: selectedKeys
7851
+ ? this.getSelectedNodes().flatMap((n) => n.key)
7852
+ : undefined,
7853
+ };
7854
+ return state;
7855
+ }
7856
+ /** Apply selection/expansion/activation status. @experimental */
7857
+ async setState(state, options = {}) {
7858
+ const { expandLazy = true } = options;
7859
+ return this.runWithDeferredUpdateAsync(async () => {
7860
+ var _a, _b;
7861
+ if (state.expandedKeys && state.expandedKeys.length) {
7862
+ if (expandLazy) {
7863
+ // Expand all keys recursively, even if they are not in the tree yet
7864
+ await this._loadLazyNodes(state.expandedKeys, {
7865
+ expand: true,
7866
+ noEvents: true,
7867
+ });
7868
+ }
7869
+ else {
7870
+ for (const key of state.expandedKeys) {
7871
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true);
7872
+ }
7873
+ }
7874
+ }
7875
+ if (state.activeKey) {
7876
+ this.setActiveNode(state.activeKey);
7877
+ }
7878
+ if (state.selectedKeys) {
7879
+ this.selectAll(false);
7880
+ for (const key of state.selectedKeys) {
7881
+ (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true);
7882
+ }
7883
+ }
7884
+ if (this.isCellNav() && state.activeColIdx != null) {
7885
+ this.setColumn(state.activeColIdx);
7886
+ }
7887
+ });
7888
+ }
7505
7889
  update(change, node, options) {
7506
7890
  // this.log(`update(${change}) node=${node}`);
7507
7891
  if (!(node instanceof WunderbaumNode)) {
@@ -7658,18 +8042,33 @@ class Wunderbaum {
7658
8042
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
7659
8043
  * (defaults to sorting by title).
7660
8044
  * @param {boolean} deep pass true to sort all descendant nodes recursively
8045
+ * @deprecated use {@link sort}
7661
8046
  */
7662
8047
  sortChildren(cmp = nodeTitleSorter, deep = false) {
7663
- this.root.sortChildren(cmp, deep);
8048
+ this.logDeprecate("sortChildren()", { since: "0.14.0" });
8049
+ return this.sort({
8050
+ cmp: cmp ? cmp : undefined,
8051
+ deep: deep,
8052
+ propName: "title",
8053
+ });
7664
8054
  }
7665
8055
  /**
7666
8056
  * Convenience method to implement column sorting.
7667
8057
  * @see {@link WunderbaumNode.sortByProperty}.
7668
8058
  * @since 0.11.0
8059
+ * @deprecated use {@link sort}
7669
8060
  */
7670
8061
  sortByProperty(options) {
8062
+ this.logDeprecate("sortByProperty()", { since: "0.14.0" });
7671
8063
  this.root.sortByProperty(options);
7672
8064
  }
8065
+ /**
8066
+ * Sort nodes list by title or custom criteria.
8067
+ * @since 0.14.0
8068
+ */
8069
+ sort(options) {
8070
+ this.root.sort(options);
8071
+ }
7673
8072
  /** Convert tree to an array of plain objects.
7674
8073
  *
7675
8074
  * @param callback is called for every node, in order to allow
@@ -7783,11 +8182,11 @@ class Wunderbaum {
7783
8182
  // }
7784
8183
  return modified;
7785
8184
  }
7786
- _insertIcon(icon, elem) {
7787
- const iconElem = document.createElement("i");
7788
- iconElem.className = icon;
7789
- elem.appendChild(iconElem);
7790
- }
8185
+ // protected _insertIcon(icon: string, elem: HTMLElement) {
8186
+ // const iconElem = document.createElement("i");
8187
+ // iconElem.className = icon;
8188
+ // elem.appendChild(iconElem);
8189
+ // }
7791
8190
  /** Create/update header markup from `this.columns` definition.
7792
8191
  * @internal
7793
8192
  */
@@ -7885,6 +8284,102 @@ class Wunderbaum {
7885
8284
  this._updateViewportImmediately();
7886
8285
  }
7887
8286
  }
8287
+ /** @internal */
8288
+ _createNodeIcon(node, showLoading, showBadge) {
8289
+ const iconMap = this.iconMap;
8290
+ let iconElem;
8291
+ let icon = node.getOption("icon");
8292
+ if (node._errorInfo) {
8293
+ icon = iconMap.error;
8294
+ }
8295
+ else if (node._isLoading && showLoading) {
8296
+ // Status nodes, or nodes without expander (< minExpandLevel) should
8297
+ // display the 'loading' status with the i.wb-icon span
8298
+ icon = iconMap.loading;
8299
+ }
8300
+ if (icon === false) {
8301
+ return null; // explicitly disabled: don't try default icons
8302
+ }
8303
+ if (typeof icon === "string") ;
8304
+ else if (node.statusNodeType) {
8305
+ icon = iconMap[node.statusNodeType];
8306
+ }
8307
+ else if (node.expanded) {
8308
+ icon = iconMap.folderOpen;
8309
+ }
8310
+ else if (node.children) {
8311
+ icon = iconMap.folder;
8312
+ }
8313
+ else if (node.lazy) {
8314
+ icon = iconMap.folderLazy;
8315
+ }
8316
+ else {
8317
+ icon = iconMap.doc;
8318
+ }
8319
+ if (!icon) {
8320
+ iconElem = document.createElement("i");
8321
+ iconElem.className = "wb-icon";
8322
+ }
8323
+ else if (TEST_HTML.test(icon)) {
8324
+ iconElem = elemFromHtml(icon);
8325
+ }
8326
+ else if (TEST_FILE_PATH.test(icon)) {
8327
+ iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
8328
+ }
8329
+ else {
8330
+ // Class name
8331
+ iconElem = document.createElement("i");
8332
+ iconElem.className = "wb-icon " + icon;
8333
+ }
8334
+ // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
8335
+ const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem });
8336
+ let badge = null;
8337
+ if (cbRes != null && cbRes !== false) {
8338
+ let classes = "";
8339
+ let tooltip = "";
8340
+ if (isPlainObject(cbRes)) {
8341
+ badge = "" + cbRes.badge;
8342
+ classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
8343
+ tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
8344
+ }
8345
+ else if (typeof cbRes === "number") {
8346
+ badge = "" + cbRes;
8347
+ }
8348
+ else {
8349
+ badge = cbRes; // string or HTMLSpanElement
8350
+ }
8351
+ if (typeof badge === "string") {
8352
+ badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
8353
+ }
8354
+ if (badge) {
8355
+ iconElem.append(badge);
8356
+ }
8357
+ }
8358
+ return iconElem;
8359
+ }
8360
+ _updateTopBreadcrumb() {
8361
+ const breadcrumb = this.breadcrumb;
8362
+ const topmost = this.getTopmostVpNode(true);
8363
+ const parentList = topmost === null || topmost === void 0 ? void 0 : topmost.getParentList(false, false);
8364
+ if (parentList === null || parentList === void 0 ? void 0 : parentList.length) {
8365
+ breadcrumb.innerHTML = "";
8366
+ for (const n of topmost.getParentList(false, false)) {
8367
+ const icon = this._createNodeIcon(n, false, false);
8368
+ if (icon) {
8369
+ breadcrumb.append(icon, " ");
8370
+ }
8371
+ const part = document.createElement("a");
8372
+ part.textContent = n.title;
8373
+ part.href = "#";
8374
+ part.classList.add("wb-breadcrumb");
8375
+ part.dataset.key = n.key;
8376
+ breadcrumb.append(part, this.options.strings.breadcrumbDelimiter);
8377
+ }
8378
+ }
8379
+ else {
8380
+ breadcrumb.innerHTML = "&nbsp;";
8381
+ }
8382
+ }
7888
8383
  /**
7889
8384
  * This is the actual update method, which is wrapped inside a throttle method.
7890
8385
  * It calls `updateColumns()` and `_updateRows()`.
@@ -7895,7 +8390,6 @@ class Wunderbaum {
7895
8390
  * @internal
7896
8391
  */
7897
8392
  _updateViewportImmediately() {
7898
- var _a;
7899
8393
  if (this._disableUpdateCount) {
7900
8394
  this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`);
7901
8395
  this._disableUpdateIgnoreCount++;
@@ -7942,11 +8436,8 @@ class Wunderbaum {
7942
8436
  this._updateRows();
7943
8437
  // console.profileEnd(`_updateViewportImmediately()`)
7944
8438
  }
7945
- if (this.options.connectTopBreadcrumb) {
7946
- assert(this.options.connectTopBreadcrumb.textContent != null, `Invalid 'connectTopBreadcrumb' option (input element expected).`);
7947
- let path = (_a = this.getTopmostVpNode(true)) === null || _a === void 0 ? void 0 : _a.getPath(false, "title", " > ");
7948
- path = path ? path + " >" : "";
7949
- this.options.connectTopBreadcrumb.textContent = path;
8439
+ if (this.breadcrumb) {
8440
+ this._updateTopBreadcrumb();
7950
8441
  }
7951
8442
  this._callEvent("update");
7952
8443
  }
@@ -8070,7 +8561,8 @@ class Wunderbaum {
8070
8561
  }
8071
8562
  /**
8072
8563
  * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
8073
- * @see {@link IterableIterator<WunderbaumNode>}, {@link WunderbaumNode.visit}.
8564
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
8565
+ * @see {@link WunderbaumNode.visit}.
8074
8566
  *
8075
8567
  * @param {function} callback the callback function.
8076
8568
  * Return false to stop iteration, return "skip" to skip this node and
@@ -8213,11 +8705,71 @@ class Wunderbaum {
8213
8705
  *
8214
8706
  * Previous data is cleared. Note that also column- and type defintions may
8215
8707
  * be passed with the `source` object.
8708
+ * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request
8709
+ * and restore the previous state.
8216
8710
  */
8217
- load(source) {
8711
+ async load(source) {
8218
8712
  this.clear();
8713
+ this._initialSource = source;
8219
8714
  return this.root.load(source);
8220
8715
  }
8716
+ /** Reload the tree and optionally restore state.
8717
+ * Source defaults to last ajax url if any.
8718
+ * Restoring the active node requires stable keys
8719
+ * @see {@link WunderbaumOptions.autoKeys}
8720
+ * @see {@link Wunderbaum.load}
8721
+ * @experimental
8722
+ */
8723
+ async reload(options = {}) {
8724
+ const { source = this._initialSource, reactivate = true } = options;
8725
+ if (!source) {
8726
+ this.logWarn("No previous ajax source to reload.");
8727
+ return;
8728
+ }
8729
+ if (!reactivate) {
8730
+ return this.load(source);
8731
+ }
8732
+ const state = this.getState();
8733
+ await this.load(source);
8734
+ return this.setState(state);
8735
+ }
8736
+ /**
8737
+ * Make sure that all nodes in the given keyList are accessible.
8738
+ * This may include loading lazy parent nodes.
8739
+ * Recursively load (and optionally expand) all requested node paths.
8740
+ */
8741
+ async _loadLazyNodes(keyList, options = {}) {
8742
+ const { expand = true } = options;
8743
+ const keySet = new Set(keyList);
8744
+ // Make sure that all parent nodes are loaded (and expand if requested)
8745
+ while (keySet.size > 0) {
8746
+ const pendingNodes = [];
8747
+ const curSet = new Set(keySet);
8748
+ for (const key of curSet) {
8749
+ const node = this.findKey(key);
8750
+ if (!node) {
8751
+ continue; // key not yet found (need to load lazy parent?)
8752
+ }
8753
+ keySet.delete(key);
8754
+ if (expand) {
8755
+ pendingNodes.push(node.setExpanded(true));
8756
+ }
8757
+ else if (node.isUnloaded()) {
8758
+ pendingNodes.push(node.loadLazy());
8759
+ }
8760
+ if (node._rowElem) {
8761
+ node._render(); // show spinner even is update is suppressed
8762
+ }
8763
+ }
8764
+ if (pendingNodes.length === 0) {
8765
+ // will not load any more nodes, so if if there are still keys
8766
+ // left in the set, we will never find them
8767
+ this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet);
8768
+ break;
8769
+ }
8770
+ await Promise.allSettled(pendingNodes);
8771
+ }
8772
+ }
8221
8773
  /**
8222
8774
  * Disable render requests during operations that would trigger many updates.
8223
8775
  *
@@ -8315,8 +8867,20 @@ class Wunderbaum {
8315
8867
  }
8316
8868
  Wunderbaum.sequence = 0;
8317
8869
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8318
- Wunderbaum.version = "v0.12.1"; // Set to semver by 'grunt release'
8870
+ Wunderbaum.version = "v0.14.0"; // Set to semver by 'grunt release'
8319
8871
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8320
8872
  Wunderbaum.util = util;
8873
+ /** A map of default iconMaps.
8874
+ * May be used as default, when passing partial icon definition maps:
8875
+ * ```js
8876
+ * const tree = new mar10.Wunderbaum({
8877
+ * ...
8878
+ * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
8879
+ * folder: "bi bi-archive",
8880
+ * }),
8881
+ * });
8882
+ * ```
8883
+ */
8884
+ Wunderbaum.iconMaps = defaultIconMaps;
8321
8885
 
8322
8886
  export { Wunderbaum };