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.
@@ -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.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
10
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
11
11
  */
12
12
  /*
13
13
  * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21
@@ -299,7 +299,7 @@
299
299
  /*!
300
300
  * Wunderbaum - util
301
301
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
302
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
302
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
303
303
  */
304
304
  /** @module util */
305
305
  /** Readable names for `MouseEvent.button` */
@@ -449,7 +449,7 @@
449
449
  }
450
450
  return obj;
451
451
  }
452
- /** Shortcut for `throw new Error(msg)`.*/
452
+ /** Shortcut for `throw new Error(msg)`. */
453
453
  function error(msg) {
454
454
  throw new Error(msg);
455
455
  }
@@ -682,18 +682,6 @@
682
682
  }
683
683
  return obj;
684
684
  }
685
- // /** Return a EventTarget from selector or cast an existing element. */
686
- // export function eventTargetFromSelector(
687
- // obj: string | EventTarget
688
- // ): EventTarget | null {
689
- // if (!obj) {
690
- // return null;
691
- // }
692
- // if (typeof obj === "string") {
693
- // return document.querySelector(obj) as EventTarget;
694
- // }
695
- // return obj as EventTarget;
696
- // }
697
685
  /**
698
686
  * Return a canonical descriptive string for a keyboard or mouse event.
699
687
  *
@@ -976,6 +964,10 @@
976
964
  }
977
965
  throw new Error(`Expected a string like '123px': ${defaults}`);
978
966
  }
967
+ /** Cast any value to <T>. */
968
+ function unsafeCast(value) {
969
+ return value;
970
+ }
979
971
  /** Return the the boolean value of the first non-null element.
980
972
  * Example:
981
973
  * ```js
@@ -1049,7 +1041,7 @@
1049
1041
  const throttledFn = (...args) => {
1050
1042
  if (waiting) {
1051
1043
  pendingArgs = args;
1052
- // console.log(`adaptiveThrottle() queing request #${waiting}...`, args);
1044
+ // console.log(`adaptiveThrottle() queueing request #${waiting}...`, args);
1053
1045
  waiting += 1;
1054
1046
  }
1055
1047
  else {
@@ -1104,6 +1096,60 @@
1104
1096
  };
1105
1097
  return throttledFn;
1106
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
+ }
1107
1153
 
1108
1154
  var util = /*#__PURE__*/Object.freeze({
1109
1155
  __proto__: null,
@@ -1134,6 +1180,7 @@
1134
1180
  isFunction: isFunction,
1135
1181
  isMac: isMac,
1136
1182
  isPlainObject: isPlainObject,
1183
+ murmurHash3: murmurHash3,
1137
1184
  noop: noop,
1138
1185
  onEvent: onEvent,
1139
1186
  overrideMethod: overrideMethod,
@@ -1147,13 +1194,14 @@
1147
1194
  toPixel: toPixel,
1148
1195
  toSet: toSet,
1149
1196
  toggleCheckbox: toggleCheckbox,
1150
- type: type
1197
+ type: type,
1198
+ unsafeCast: unsafeCast
1151
1199
  });
1152
1200
 
1153
1201
  /*!
1154
1202
  * Wunderbaum - types
1155
1203
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1156
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1204
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1157
1205
  */
1158
1206
  /**
1159
1207
  * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
@@ -1221,7 +1269,7 @@
1221
1269
  /*!
1222
1270
  * Wunderbaum - wb_extension_base
1223
1271
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1224
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1272
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1225
1273
  */
1226
1274
  class WunderbaumExtension {
1227
1275
  constructor(tree, id, defaults) {
@@ -1280,7 +1328,7 @@
1280
1328
  /*!
1281
1329
  * Wunderbaum - ext-filter
1282
1330
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1283
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1331
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1284
1332
  */
1285
1333
  const START_MARKER = "\uFFF7";
1286
1334
  const END_MARKER = "\uFFF8";
@@ -1292,7 +1340,7 @@
1292
1340
  autoApply: true, // Re-apply last filter if lazy data is loaded
1293
1341
  autoExpand: false, // Expand all branches that contain matches while filtered
1294
1342
  matchBranch: false, // Whether to implicitly match all children of matched nodes
1295
- connectInput: null, // Element or selector of an input control for filter query strings
1343
+ connect: null, // Element or selector of an input control for filter query strings
1296
1344
  fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
1297
1345
  hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
1298
1346
  highlight: true, // Highlight matches by wrapping inside <mark> tags
@@ -1300,36 +1348,117 @@
1300
1348
  mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
1301
1349
  noData: true, // Display a 'no data' status node if result is empty
1302
1350
  });
1351
+ this.queryInput = null;
1352
+ this.prevButton = null;
1353
+ this.nextButton = null;
1354
+ this.modeButton = null;
1355
+ this.matchInfoElem = null;
1303
1356
  this.lastFilterArgs = null;
1304
1357
  }
1305
1358
  init() {
1306
1359
  super.init();
1307
- const connectInput = this.getPluginOption("connectInput");
1308
- if (connectInput) {
1309
- this.queryInput = elemFromSelector(connectInput);
1310
- assert(this.queryInput, `Invalid 'filter.connectInput' option: ${connectInput}.`);
1311
- onEvent(this.queryInput, "input", debounce((e) => {
1312
- // this.tree.log("query", e);
1313
- this.filterNodes(this.queryInput.value.trim(), {});
1314
- }, 700));
1360
+ const connect = this.getPluginOption("connect");
1361
+ if (connect) {
1362
+ this._connectControls();
1315
1363
  }
1316
1364
  }
1317
1365
  setPluginOption(name, value) {
1318
- // alert("filter opt=" + name + ", " + value)
1319
1366
  super.setPluginOption(name, value);
1320
1367
  switch (name) {
1321
1368
  case "mode":
1322
- this.tree.filterMode = value === "hide" ? "hide" : "dim";
1369
+ this.tree.filterMode =
1370
+ value === "hide" ? "hide" : value === "mark" ? "mark" : "dim";
1323
1371
  this.tree.updateFilter();
1324
1372
  break;
1325
1373
  }
1326
1374
  }
1375
+ _updatedConnectedControls() {
1376
+ var _a;
1377
+ const filterActive = this.tree.filterMode !== null;
1378
+ const activeNode = this.tree.getActiveNode();
1379
+ const matchCount = filterActive ? this.countMatches() : 0;
1380
+ const strings = this.treeOpts.strings;
1381
+ let matchIdx = "?";
1382
+ if (this.matchInfoElem) {
1383
+ if (filterActive) {
1384
+ let info;
1385
+ if (matchCount === 0) {
1386
+ info = strings.noMatch;
1387
+ }
1388
+ else if (activeNode && activeNode.match >= 1) {
1389
+ matchIdx = (_a = activeNode.match) !== null && _a !== void 0 ? _a : "?";
1390
+ info = strings.matchIndex;
1391
+ }
1392
+ else {
1393
+ info = strings.queryResult;
1394
+ }
1395
+ info = info
1396
+ .replace("${count}", this.tree.count().toLocaleString())
1397
+ .replace("${match}", "" + matchIdx)
1398
+ .replace("${matches}", matchCount.toLocaleString());
1399
+ this.matchInfoElem.textContent = info;
1400
+ }
1401
+ else {
1402
+ this.matchInfoElem.textContent = "";
1403
+ }
1404
+ }
1405
+ if (this.nextButton instanceof HTMLButtonElement) {
1406
+ this.nextButton.disabled = !matchCount;
1407
+ }
1408
+ if (this.prevButton instanceof HTMLButtonElement) {
1409
+ this.prevButton.disabled = !matchCount;
1410
+ }
1411
+ if (this.modeButton) {
1412
+ this.modeButton.disabled = !filterActive;
1413
+ this.modeButton.classList.toggle("wb-filter-hide", this.tree.filterMode === "hide");
1414
+ }
1415
+ }
1416
+ _connectControls() {
1417
+ const tree = this.tree;
1418
+ const connect = this.getPluginOption("connect");
1419
+ if (!connect) {
1420
+ return;
1421
+ }
1422
+ this.queryInput = elemFromSelector(connect.inputElem);
1423
+ if (!this.queryInput) {
1424
+ throw new Error(`Invalid 'filter.connect' option: ${connect.inputElem}.`);
1425
+ }
1426
+ this.prevButton = elemFromSelector(connect.prevButton);
1427
+ this.nextButton = elemFromSelector(connect.nextButton);
1428
+ this.modeButton = elemFromSelector(connect.modeButton);
1429
+ this.matchInfoElem = elemFromSelector(connect.matchInfoElem);
1430
+ if (this.prevButton) {
1431
+ onEvent(this.prevButton, "click", () => {
1432
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "prevMatch");
1433
+ this._updatedConnectedControls();
1434
+ });
1435
+ }
1436
+ if (this.nextButton) {
1437
+ onEvent(this.nextButton, "click", () => {
1438
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "nextMatch");
1439
+ this._updatedConnectedControls();
1440
+ });
1441
+ }
1442
+ if (this.modeButton) {
1443
+ onEvent(this.modeButton, "click", (e) => {
1444
+ if (!this.tree.filterMode) {
1445
+ return;
1446
+ }
1447
+ this.setPluginOption("mode", tree.filterMode === "dim" ? "hide" : "dim");
1448
+ });
1449
+ }
1450
+ onEvent(this.queryInput, "input", debounce((e) => {
1451
+ this.filterNodes(this.queryInput.value.trim(), {});
1452
+ }, 700));
1453
+ this._updatedConnectedControls();
1454
+ }
1327
1455
  _applyFilterNoUpdate(filter, _opts) {
1328
1456
  return this.tree.runWithDeferredUpdate(() => {
1329
1457
  return this._applyFilterImpl(filter, _opts);
1330
1458
  });
1331
1459
  }
1332
1460
  _applyFilterImpl(filter, _opts) {
1461
+ var _a;
1333
1462
  let //temp,
1334
1463
  count = 0;
1335
1464
  const start = Date.now();
@@ -1411,11 +1540,11 @@
1411
1540
  return !!res;
1412
1541
  };
1413
1542
  }
1414
- tree.filterMode = opts.mode;
1543
+ tree.filterMode = (_a = opts.mode) !== null && _a !== void 0 ? _a : "dim";
1415
1544
  // eslint-disable-next-line prefer-rest-params
1416
1545
  this.lastFilterArgs = arguments;
1417
1546
  tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
1418
- tree.element.classList.toggle("wb-ext-filter-dim", !hideMode);
1547
+ tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim");
1419
1548
  tree.element.classList.toggle("wb-ext-filter-hide-expanders", !!opts.hideExpanders);
1420
1549
  // Reset current filter
1421
1550
  tree.root.subMatchCount = 0;
@@ -1424,10 +1553,6 @@
1424
1553
  delete node.titleWithHighlight;
1425
1554
  node.subMatchCount = 0;
1426
1555
  });
1427
- // statusNode = tree.root.findDirectChild(KEY_NODATA);
1428
- // if (statusNode) {
1429
- // statusNode.remove();
1430
- // }
1431
1556
  tree.setStatus(NodeStatusType.ok);
1432
1557
  // Adjust node.hide, .match, and .subMatchCount properties
1433
1558
  treeOpts.autoCollapse = false; // #528
@@ -1438,7 +1563,7 @@
1438
1563
  let res = filter(node);
1439
1564
  if (res === "skip") {
1440
1565
  node.visit(function (c) {
1441
- c.match = false;
1566
+ c.match = undefined;
1442
1567
  }, true);
1443
1568
  return "skip";
1444
1569
  }
@@ -1449,7 +1574,7 @@
1449
1574
  }
1450
1575
  if (res) {
1451
1576
  count++;
1452
- node.match = true;
1577
+ node.match = count;
1453
1578
  node.visitParents((p) => {
1454
1579
  if (p !== node) {
1455
1580
  p.subMatchCount += 1;
@@ -1476,6 +1601,7 @@
1476
1601
  }
1477
1602
  // Redraw whole tree
1478
1603
  tree.logDebug(`Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.`);
1604
+ this._updatedConnectedControls();
1479
1605
  return count;
1480
1606
  }
1481
1607
  /**
@@ -1490,6 +1616,10 @@
1490
1616
  */
1491
1617
  filterBranches(filter, options) {
1492
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
+ });
1493
1623
  options.matchBranch = true;
1494
1624
  return this._applyFilterNoUpdate(filter, options);
1495
1625
  }
@@ -1520,34 +1650,22 @@
1520
1650
  else {
1521
1651
  tree.logWarn("updateFilter(): no filter active.");
1522
1652
  }
1653
+ this._updatedConnectedControls();
1523
1654
  }
1524
1655
  /**
1525
1656
  * [ext-filter] Reset the filter.
1526
1657
  */
1527
1658
  clearFilter() {
1528
1659
  const tree = this.tree;
1529
- // statusNode = tree.root.findDirectChild(KEY_NODATA),
1530
- // escapeTitles = tree.options.escapeTitles;
1531
1660
  tree.enableUpdate(false);
1532
- // if (statusNode) {
1533
- // statusNode.remove();
1534
- // }
1535
1661
  tree.setStatus(NodeStatusType.ok);
1536
1662
  // we also counted root node's subMatchCount
1537
1663
  delete tree.root.match;
1538
1664
  delete tree.root.subMatchCount;
1539
1665
  tree.visit((node) => {
1540
- // if (node.match && node._rowElem) {
1541
- // let titleElem = node._rowElem.querySelector("span.wb-title")!;
1542
- // node._callEvent("enhanceTitle", { titleElem: titleElem });
1543
- // }
1544
1666
  delete node.match;
1545
1667
  delete node.subMatchCount;
1546
1668
  delete node.titleWithHighlight;
1547
- // if (node.subMatchBadge) {
1548
- // node.subMatchBadge.remove();
1549
- // delete node.subMatchBadge;
1550
- // }
1551
1669
  if (node._filterAutoExpanded && node.expanded) {
1552
1670
  node.setExpanded(false, {
1553
1671
  noAnimation: true,
@@ -1561,12 +1679,12 @@
1561
1679
  tree.element.classList.remove(
1562
1680
  // "wb-ext-filter",
1563
1681
  "wb-ext-filter-dim", "wb-ext-filter-hide");
1564
- // tree._callHook("treeStructureChanged", this, "clearFilter");
1682
+ this._updatedConnectedControls();
1565
1683
  tree.enableUpdate(true);
1566
1684
  }
1567
1685
  }
1568
1686
  /**
1569
- * @description Marks the matching charecters of `text` either by `mark` or
1687
+ * @description Marks the matching characters of `text` either by `mark` or
1570
1688
  * by exotic*Chars (if `escapeTitles` is `true`) based on `matches`
1571
1689
  * which is an array of matching groups.
1572
1690
  * @param {string} text
@@ -1605,7 +1723,7 @@
1605
1723
  /*!
1606
1724
  * Wunderbaum - ext-keynav
1607
1725
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1608
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1726
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1609
1727
  */
1610
1728
  const QUICKSEARCH_DELAY = 500;
1611
1729
  class KeynavExtension extends WunderbaumExtension {
@@ -1969,7 +2087,7 @@
1969
2087
  /*!
1970
2088
  * Wunderbaum - ext-logger
1971
2089
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1972
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2090
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
1973
2091
  */
1974
2092
  class LoggerExtension extends WunderbaumExtension {
1975
2093
  constructor(tree) {
@@ -2011,7 +2129,7 @@
2011
2129
  /*!
2012
2130
  * Wunderbaum - ext-dnd
2013
2131
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2014
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2132
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2015
2133
  */
2016
2134
  const nodeMimeType = "application/x-wunderbaum-node";
2017
2135
  class DndExtension extends WunderbaumExtension {
@@ -2247,7 +2365,6 @@
2247
2365
  */
2248
2366
  onDragEvent(e) {
2249
2367
  var _a;
2250
- // const tree = this.tree;
2251
2368
  const dndOpts = this.treeOpts.dnd;
2252
2369
  const srcNode = Wunderbaum.getNode(e);
2253
2370
  if (!srcNode) {
@@ -2273,7 +2390,7 @@
2273
2390
  return false;
2274
2391
  }
2275
2392
  const nodeData = srcNode.toDict(true, (n) => {
2276
- // We don't want to re-use the key on drop:
2393
+ // We don't want to reuse the key on drop:
2277
2394
  n._orgKey = n.key;
2278
2395
  delete n.key;
2279
2396
  });
@@ -2335,6 +2452,7 @@
2335
2452
  };
2336
2453
  if (!targetNode) {
2337
2454
  this._leaveNode();
2455
+ e.preventDefault(); // Don't open file in browser when dropped in empty area
2338
2456
  return;
2339
2457
  }
2340
2458
  if (["drop"].includes(e.type)) {
@@ -2440,19 +2558,20 @@
2440
2558
  nodeData = nodeData ? JSON.parse(nodeData) : null;
2441
2559
  const srcNode = this.srcNode;
2442
2560
  const lastDropEffect = this.lastDropEffect;
2443
- setTimeout(() => {
2444
- // Decouple this call, because drop actions may prevent the dragend event
2445
- // from being fired on some browsers
2446
- targetNode._callEvent("dnd.drop", {
2447
- event: e,
2448
- region: region,
2449
- suggestedDropMode: region === "over" ? "appendChild" : region,
2450
- suggestedDropEffect: lastDropEffect,
2451
- // suggestedDropEffect: e.dataTransfer?.dropEffect,
2452
- sourceNode: srcNode,
2453
- sourceNodeData: nodeData,
2454
- });
2455
- }, 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
+ });
2456
2575
  }
2457
2576
  return false;
2458
2577
  }
@@ -2461,7 +2580,7 @@
2461
2580
  /*!
2462
2581
  * Wunderbaum - drag_observer
2463
2582
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2464
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2583
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2465
2584
  */
2466
2585
  /**
2467
2586
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2610,7 +2729,7 @@
2610
2729
  /*!
2611
2730
  * Wunderbaum - common
2612
2731
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2613
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2732
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2614
2733
  */
2615
2734
  const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
2616
2735
  /**
@@ -2630,12 +2749,18 @@
2630
2749
  const RENDER_MAX_PREFETCH = 5;
2631
2750
  /** Minimum column width if not set otherwise. */
2632
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";
2633
2757
  /** Regular expression to detect if a string describes an image URL (in contrast
2634
2758
  * to a class name). Strings are considered image urls if they contain '.' or '/'.
2759
+ * `<` is ignored, because it is probably an html tag.
2635
2760
  */
2636
- const TEST_IMG = new RegExp(/\.|\//);
2637
- // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
2638
- // 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 = /</;
2639
2764
  /**
2640
2765
  * Default node icons for icon libraries
2641
2766
  *
@@ -2643,7 +2768,7 @@
2643
2768
  * - 'fontawesome6' {@link https://fontawesome.com/icons}
2644
2769
  *
2645
2770
  */
2646
- const iconMaps = {
2771
+ const defaultIconMaps = {
2647
2772
  bootstrap: {
2648
2773
  error: "bi bi-exclamation-triangle",
2649
2774
  // loading: "bi bi-hourglass-split wb-busy",
@@ -2691,7 +2816,7 @@
2691
2816
  radioChecked: "fa-solid fa-circle",
2692
2817
  radioUnchecked: "fa-regular fa-circle",
2693
2818
  radioUnknown: "fa-regular fa-circle-question",
2694
- folder: "fa-solid fa-folder-closed",
2819
+ folder: "fa-regular fa-folder-closed",
2695
2820
  folderOpen: "fa-regular fa-folder-open",
2696
2821
  folderLazy: "fa-solid fa-folder-plus",
2697
2822
  doc: "fa-regular fa-file",
@@ -2723,29 +2848,20 @@
2723
2848
  // "Escape",
2724
2849
  // ]);
2725
2850
  /** Map `KeyEvent.key` to navigation action. */
2726
- const KEY_TO_ACTION_DICT = {
2727
- " ": "toggleSelect",
2728
- "+": "expand",
2729
- Add: "expand",
2851
+ const KEY_TO_NAVIGATION_MAP = {
2730
2852
  ArrowDown: "down",
2731
2853
  ArrowLeft: "left",
2732
2854
  ArrowRight: "right",
2733
2855
  ArrowUp: "up",
2734
2856
  Backspace: "parent",
2735
- "/": "collapseAll",
2736
- Divide: "collapseAll",
2737
2857
  End: "lastCol",
2738
2858
  Home: "firstCol",
2739
2859
  "Control+End": "last",
2740
2860
  "Control+Home": "first",
2741
2861
  "Meta+ArrowDown": "last", // macOs
2742
2862
  "Meta+ArrowUp": "first", // macOs
2743
- "*": "expandAll",
2744
- Multiply: "expandAll",
2745
2863
  PageDown: "pageDown",
2746
2864
  PageUp: "pageUp",
2747
- "-": "collapse",
2748
- Subtract: "collapse",
2749
2865
  };
2750
2866
  /** Return a callback that returns true if the node title matches the string
2751
2867
  * or regular expression.
@@ -2773,12 +2889,20 @@
2773
2889
  return reMatch.test(node.title);
2774
2890
  };
2775
2891
  }
2776
- /** 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
+ */
2777
2895
  function nodeTitleSorter(a, b) {
2778
2896
  const x = a.title.toLowerCase();
2779
2897
  const y = b.title.toLowerCase();
2780
2898
  return x === y ? 0 : x > y ? 1 : -1;
2781
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
+ // }
2782
2906
  /**
2783
2907
  * Convert 'flat' to 'nested' format.
2784
2908
  *
@@ -2969,7 +3093,7 @@
2969
3093
  /*!
2970
3094
  * Wunderbaum - ext-grid
2971
3095
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2972
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3096
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
2973
3097
  */
2974
3098
  class GridExtension extends WunderbaumExtension {
2975
3099
  constructor(tree) {
@@ -3029,7 +3153,7 @@
3029
3153
  super.init();
3030
3154
  }
3031
3155
  /**
3032
- * Hanldes drag and sragstop events for column resizing.
3156
+ * Handles drag and sragstop events for column resizing.
3033
3157
  */
3034
3158
  handleDrag(e) {
3035
3159
  const custom = e.customData;
@@ -3060,7 +3184,7 @@
3060
3184
  /*!
3061
3185
  * Wunderbaum - deferred
3062
3186
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3063
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3187
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
3064
3188
  */
3065
3189
  /**
3066
3190
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -3113,7 +3237,7 @@
3113
3237
  /*!
3114
3238
  * Wunderbaum - wunderbaum_node
3115
3239
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3116
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3240
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
3117
3241
  */
3118
3242
  /** WunderbaumNode properties that can be passed with source data.
3119
3243
  * (Any other source properties will be stored as `node.data.PROP`.)
@@ -3164,7 +3288,7 @@
3164
3288
  */
3165
3289
  class WunderbaumNode {
3166
3290
  constructor(tree, parent, data) {
3167
- var _a, _b;
3291
+ var _a;
3168
3292
  /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
3169
3293
  * times within a tree (in this case we have 'clone nodes').
3170
3294
  * @see Use {@link setKey} to modify.
@@ -3194,8 +3318,8 @@
3194
3318
  assert(!data.children, "'children' not allowed here");
3195
3319
  this.tree = tree;
3196
3320
  this.parent = parent;
3197
- this.key = "" + ((_a = data.key) !== null && _a !== void 0 ? _a : ++WunderbaumNode.sequence);
3198
- 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 + ">");
3199
3323
  this.expanded = !!data.expanded;
3200
3324
  this.lazy = !!data.lazy;
3201
3325
  // We set the following node properties only if a matching data value is
@@ -3316,8 +3440,14 @@
3316
3440
  const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel;
3317
3441
  for (const child of nodeData) {
3318
3442
  const subChildren = child.children;
3443
+ // Remove children property from source data because it should not be
3444
+ // passed to the constructor of WunderbaumNode:
3319
3445
  delete child.children;
3320
3446
  const n = new WunderbaumNode(tree, this, child);
3447
+ // Set `children` property again, so it can be used in `reload()`
3448
+ if (subChildren != null) {
3449
+ child.children = subChildren;
3450
+ }
3321
3451
  if (forceExpand && !n.isUnloaded()) {
3322
3452
  n.expanded = true;
3323
3453
  }
@@ -3733,15 +3863,12 @@
3733
3863
  }
3734
3864
  return l;
3735
3865
  }
3736
- /** Return a string representing the hierachical node path, e.g. "a/b/c".
3866
+ /** Return a string representing the hierarchical node path, e.g. "a/b/c".
3737
3867
  * @param includeSelf
3738
3868
  * @param part property name or callback
3739
3869
  * @param separator
3740
3870
  */
3741
3871
  getPath(includeSelf = true, part = "title", separator = "/") {
3742
- // includeSelf = includeSelf !== false;
3743
- // part = part || "title";
3744
- // separator = separator || "/";
3745
3872
  let val;
3746
3873
  const path = [];
3747
3874
  const isFunc = typeof part === "function";
@@ -3756,7 +3883,7 @@
3756
3883
  }, includeSelf);
3757
3884
  return path.join(separator);
3758
3885
  }
3759
- /** Return the preceeding node (under the same parent) or null. */
3886
+ /** Return the preceding node (under the same parent) or null. */
3760
3887
  getPrevSibling() {
3761
3888
  const ac = this.parent.children;
3762
3889
  const idx = ac.indexOf(this);
@@ -3785,7 +3912,7 @@
3785
3912
  hasClass(className) {
3786
3913
  return this.classes ? this.classes.has(className) : false;
3787
3914
  }
3788
- /** Return true if node ist the currently focused node. @since 0.9.0 */
3915
+ /** Return true if node is the currently focused node. @since 0.9.0 */
3789
3916
  hasFocus() {
3790
3917
  return this.tree.focusNode === this;
3791
3918
  }
@@ -3840,7 +3967,7 @@
3840
3967
  * an expand operation is currently possible.
3841
3968
  */
3842
3969
  isExpandable(andCollapsed = false) {
3843
- // `false` is never expandable (unoffical)
3970
+ // `false` is never expandable (unofficial)
3844
3971
  if ((andCollapsed && this.expanded) || this.children === false) {
3845
3972
  return false;
3846
3973
  }
@@ -3895,7 +4022,7 @@
3895
4022
  isParentOf(other) {
3896
4023
  return other && other.parent === this;
3897
4024
  }
3898
- /** (experimental) Return true if this node is partially loaded. */
4025
+ /** Return true if this node is partially loaded. @experimental */
3899
4026
  isPartload() {
3900
4027
  return !!this._partload;
3901
4028
  }
@@ -3903,11 +4030,11 @@
3903
4030
  isPartsel() {
3904
4031
  return !this.selected && !!this._partsel;
3905
4032
  }
3906
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4033
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3907
4034
  isRadio() {
3908
4035
  return !!this.parent.radiogroup || this.getOption("checkbox") === "radio";
3909
4036
  }
3910
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
4037
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
3911
4038
  isRendered() {
3912
4039
  return !!this._rowElem;
3913
4040
  }
@@ -4353,10 +4480,11 @@
4353
4480
  * @param options
4354
4481
  */
4355
4482
  async navigate(where, options) {
4483
+ var _a;
4356
4484
  // Allow to pass 'ArrowLeft' instead of 'left'
4357
- where = KEY_TO_ACTION_DICT[where] || where;
4485
+ const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where);
4358
4486
  // Otherwise activate or focus the related node
4359
- const node = this.findRelatedNode(where);
4487
+ const node = this.findRelatedNode(navType);
4360
4488
  if (!node) {
4361
4489
  this.logWarn(`Could not find related node '${where}'.`);
4362
4490
  return Promise.resolve(this);
@@ -4453,86 +4581,17 @@
4453
4581
  renderColInfosById: renderColInfosById,
4454
4582
  };
4455
4583
  }
4456
- _createIcon(iconMap, parentElem, replaceChild, showLoading) {
4457
- let iconSpan;
4458
- let icon = this.getOption("icon");
4459
- if (this._errorInfo) {
4460
- icon = iconMap.error;
4461
- }
4462
- else if (this._isLoading && showLoading) {
4463
- // Status nodes, or nodes without expander (< minExpandLevel) should
4464
- // display the 'loading' status with the i.wb-icon span
4465
- icon = iconMap.loading;
4466
- }
4467
- if (icon === false) {
4468
- return null; // explicitly disabled: don't try default icons
4469
- }
4470
- if (typeof icon === "string") ;
4471
- else if (this.statusNodeType) {
4472
- icon = iconMap[this.statusNodeType];
4473
- }
4474
- else if (this.expanded) {
4475
- icon = iconMap.folderOpen;
4476
- }
4477
- else if (this.children) {
4478
- icon = iconMap.folder;
4479
- }
4480
- else if (this.lazy) {
4481
- icon = iconMap.folderLazy;
4482
- }
4483
- else {
4484
- icon = iconMap.doc;
4485
- }
4486
- // this.log("_createIcon: " + icon);
4487
- if (!icon) {
4488
- iconSpan = document.createElement("i");
4489
- iconSpan.className = "wb-icon";
4490
- }
4491
- else if (icon.indexOf("<") >= 0) {
4492
- // HTML
4493
- iconSpan = elemFromHtml(icon);
4494
- }
4495
- else if (TEST_IMG.test(icon)) {
4496
- // Image URL
4497
- iconSpan = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
4498
- }
4499
- else {
4500
- // Class name
4501
- iconSpan = document.createElement("i");
4502
- iconSpan.className = "wb-icon " + icon;
4503
- }
4504
- if (replaceChild) {
4505
- parentElem.replaceChild(iconSpan, replaceChild);
4506
- }
4507
- else {
4508
- parentElem.appendChild(iconSpan);
4509
- }
4510
- // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
4511
- const cbRes = this._callEvent("iconBadge", { iconSpan: iconSpan });
4512
- let badge = null;
4513
- if (cbRes != null && cbRes !== false) {
4514
- let classes = "";
4515
- let tooltip = "";
4516
- if (isPlainObject(cbRes)) {
4517
- badge = "" + cbRes.badge;
4518
- classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
4519
- tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
4520
- }
4521
- else if (typeof cbRes === "number") {
4522
- badge = "" + cbRes;
4584
+ _createIcon(parentElem, replaceChild, showLoading) {
4585
+ const iconElem = this.tree._createNodeIcon(this, showLoading, true);
4586
+ if (iconElem) {
4587
+ if (replaceChild) {
4588
+ parentElem.replaceChild(iconElem, replaceChild);
4523
4589
  }
4524
4590
  else {
4525
- badge = cbRes; // string or HTMLSpanElement
4526
- }
4527
- if (typeof badge === "string") {
4528
- badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
4529
- }
4530
- if (badge) {
4531
- iconSpan.append(badge);
4591
+ parentElem.appendChild(iconElem);
4532
4592
  }
4533
4593
  }
4534
- // this.log("_createIcon: ", iconSpan);
4535
- return iconSpan;
4594
+ return iconElem;
4536
4595
  }
4537
4596
  /**
4538
4597
  * Create a whole new `<div class="wb-row">` element.
@@ -4587,7 +4646,7 @@
4587
4646
  }
4588
4647
  // Render the icon (show a 'loading' icon if we do not have an expander that
4589
4648
  // we would prefer).
4590
- const iconSpan = this._createIcon(tree.iconMap, nodeElem, null, !expanderSpan);
4649
+ const iconSpan = this._createIcon(nodeElem, null, !expanderSpan);
4591
4650
  if (iconSpan) {
4592
4651
  ofsTitlePx += ICON_WIDTH;
4593
4652
  }
@@ -4730,9 +4789,9 @@
4730
4789
  const typeInfo = this.type ? tree.types[this.type] : null;
4731
4790
  const rowDiv = this._rowElem;
4732
4791
  // Row markup already exists
4733
- const nodeElem = rowDiv.querySelector("span.wb-node");
4734
- const expanderSpan = nodeElem.querySelector("i.wb-expander");
4735
- const checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
4792
+ const nodeSpan = rowDiv.querySelector("span.wb-node");
4793
+ const expanderElem = nodeSpan.querySelector("i.wb-expander");
4794
+ const checkboxElem = nodeSpan.querySelector("i.wb-checkbox");
4736
4795
  const rowClasses = ["wb-row"];
4737
4796
  this.expanded ? rowClasses.push("wb-expanded") : 0;
4738
4797
  this.lazy ? rowClasses.push("wb-lazy") : 0;
@@ -4757,7 +4816,7 @@
4757
4816
  if (typeInfo && typeInfo.classes) {
4758
4817
  rowDiv.classList.add(...typeInfo.classes);
4759
4818
  }
4760
- if (expanderSpan) {
4819
+ if (expanderElem) {
4761
4820
  let image = null;
4762
4821
  if (this._isLoading) {
4763
4822
  image = iconMap.loading;
@@ -4774,16 +4833,20 @@
4774
4833
  image = iconMap.expanderLazy;
4775
4834
  }
4776
4835
  if (image == null) {
4777
- expanderSpan.classList.add("wb-indent");
4836
+ expanderElem.className = "wb-expander";
4837
+ expanderElem.classList.add("wb-indent");
4838
+ }
4839
+ else if (TEST_HTML.test(image)) {
4840
+ expanderElem.replaceWith(elemFromHtml(image));
4778
4841
  }
4779
- else if (TEST_IMG.test(image)) {
4780
- expanderSpan.style.backgroundImage = `url('${image}')`;
4842
+ else if (TEST_FILE_PATH.test(image)) {
4843
+ expanderElem.style.backgroundImage = `url('${image}')`;
4781
4844
  }
4782
4845
  else {
4783
- expanderSpan.className = "wb-expander " + image;
4846
+ expanderElem.className = "wb-expander " + image;
4784
4847
  }
4785
4848
  }
4786
- if (checkboxSpan) {
4849
+ if (checkboxElem) {
4787
4850
  let cbclass = "wb-checkbox ";
4788
4851
  if (this.isRadio()) {
4789
4852
  cbclass += "wb-radio ";
@@ -4807,7 +4870,7 @@
4807
4870
  cbclass += iconMap.checkUnchecked;
4808
4871
  }
4809
4872
  }
4810
- checkboxSpan.className = cbclass;
4873
+ checkboxElem.className = cbclass;
4811
4874
  }
4812
4875
  // Fix active cell in cell-nav mode
4813
4876
  if (!opts.isNew) {
@@ -4817,9 +4880,9 @@
4817
4880
  colSpan.classList.remove("wb-error", "wb-invalid");
4818
4881
  }
4819
4882
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4820
- const iconSpan = nodeElem.querySelector("i.wb-icon");
4883
+ const iconSpan = nodeSpan.querySelector("i.wb-icon");
4821
4884
  if (iconSpan) {
4822
- this._createIcon(tree.iconMap, nodeElem, iconSpan, !expanderSpan);
4885
+ this._createIcon(nodeSpan, iconSpan, !expanderElem);
4823
4886
  }
4824
4887
  }
4825
4888
  // Adjust column width
@@ -5124,6 +5187,32 @@
5124
5187
  setKey(key, refKey) {
5125
5188
  throw new Error("Not yet implemented");
5126
5189
  }
5190
+ // /**
5191
+ // * Calculate a *stable*, unique key for this node from its refKey (or title).
5192
+ // * We also add information from the parent, because a refKey may occur multiple
5193
+ // * times in a tree.
5194
+ // */
5195
+ // calcUniqueKey() {
5196
+ // // Assuming that the parent's key was calculated the same way, we implicitly
5197
+ // // involve the whole refKey-path:
5198
+ // const s = this.key + (this.refKey || this.title);
5199
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5200
+ // // https://security.stackexchange.com/q/209882/207588
5201
+ // const h1 = util.murmurHash3(s, true);
5202
+ // return "id_" + h1 + util.murmurHash3(h1 + s, true);
5203
+ // // const l = [];
5204
+ // // // eslint-disable-next-line @typescript-eslint/no-this-alias
5205
+ // // let node: WunderbaumNode = this;
5206
+ // // while (node.parent) {
5207
+ // // l.unshift(node.refKey || node.key);
5208
+ // // node = node.parent;
5209
+ // // }
5210
+ // // const path = l.join("/");
5211
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
5212
+ // // https://security.stackexchange.com/q/209882/207588
5213
+ // // const h1 = util.murmurHash3(path, true);
5214
+ // // return "id_" + h1 + util.murmurHash3(h1 + path, true);
5215
+ // }
5127
5216
  /**
5128
5217
  * Trigger a repaint, typically after a status or data change.
5129
5218
  *
@@ -5155,6 +5244,23 @@
5155
5244
  });
5156
5245
  return nodeList;
5157
5246
  }
5247
+ /**
5248
+ * Return an array of refKey values.
5249
+ *
5250
+ * RefKeys are unique identifiers for a node data, and are used to identify
5251
+ * clones.
5252
+ * If more than one node has the same refKey, it is only returned once.
5253
+ * @param selected if true, only return refKeys of selected nodes.
5254
+ */
5255
+ getRefKeys(selected = false) {
5256
+ const refKeys = new Set();
5257
+ this.visit((node) => {
5258
+ if (node.refKey != null && (!selected || node.selected)) {
5259
+ refKeys.add(node.refKey);
5260
+ }
5261
+ });
5262
+ return Array.from(refKeys);
5263
+ }
5158
5264
  /** Toggle the check/uncheck state. */
5159
5265
  toggleSelected(options) {
5160
5266
  let flag = this.isSelected();
@@ -5334,9 +5440,11 @@
5334
5440
  if (selectMode === "hier") {
5335
5441
  this.fixSelection3AfterClick();
5336
5442
  }
5337
- else if (selectMode === "single") {
5443
+ else if (selectMode === "single" && flag) {
5338
5444
  tree.visit((n) => {
5339
- n.selected = false;
5445
+ if (n !== this) {
5446
+ n.selected = false;
5447
+ }
5340
5448
  });
5341
5449
  }
5342
5450
  }
@@ -5368,7 +5476,7 @@
5368
5476
  assert(data.statusNodeType, "Not a status node");
5369
5477
  assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node");
5370
5478
  statusNode = this.addNode(data, "prependChild");
5371
- statusNode.match = true;
5479
+ statusNode.match = -1; // Mark as 'match' to avoid hiding
5372
5480
  tree.update(ChangeType.structure);
5373
5481
  return statusNode;
5374
5482
  };
@@ -5438,30 +5546,16 @@
5438
5546
  this.tooltip = tooltip;
5439
5547
  this.update();
5440
5548
  }
5441
- _sortChildren(cmp, deep) {
5442
- const cl = this.children;
5443
- if (!cl) {
5444
- return;
5445
- }
5446
- cl.sort(cmp);
5447
- if (deep) {
5448
- for (let i = 0, l = cl.length; i < l; i++) {
5449
- if (cl[i].children) {
5450
- cl[i]._sortChildren(cmp, deep);
5451
- }
5452
- }
5453
- }
5454
- }
5455
5549
  /**
5456
5550
  * Sort child list by title or custom criteria.
5457
5551
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
5458
5552
  * (defaults to sorting by title).
5459
5553
  * @param {boolean} deep pass true to sort all descendant nodes recursively
5554
+ * @deprecated use {@link sort}
5460
5555
  */
5461
5556
  sortChildren(cmp = nodeTitleSorter, deep = false) {
5462
- this._sortChildren(cmp || nodeTitleSorter, deep);
5463
- this.tree.update(ChangeType.structure);
5464
- // this.triggerModify("sort"); // TODO
5557
+ this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" });
5558
+ return this.sort({ cmp: cmp ? cmp : undefined, deep: deep });
5465
5559
  }
5466
5560
  /**
5467
5561
  * Renumber nodes `_nativeIndex`. This is useful to allow to restore the
@@ -5483,74 +5577,142 @@
5483
5577
  /**
5484
5578
  * Convenience method to implement column sorting.
5485
5579
  * @since 0.11.0
5580
+ * @deprecated use {@link sort}
5486
5581
  */
5487
5582
  sortByProperty(options) {
5488
- var _a, _b, _c;
5489
- const { caseInsensitive = true, deep = true, nativeOrderPropName = "_nativeIndex", updateColInfo = false, } = options;
5490
- let order;
5491
- let colDef;
5583
+ this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" });
5584
+ return this.sort(options);
5585
+ }
5586
+ /**
5587
+ * Implement column sorting.
5588
+ * @since 0.14.0
5589
+ */
5590
+ sort(options) {
5591
+ const tree = this.tree;
5592
+ let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined,
5593
+ // Support click on column sort header:
5594
+ updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options;
5595
+ propName !== null && propName !== void 0 ? propName : (propName = colId);
5596
+ if (propName === "*") {
5597
+ propName = "title";
5598
+ }
5599
+ const isFolder = tree.options.sortFoldersFirst === true
5600
+ ? (node) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER
5601
+ : tree.options.sortFoldersFirst;
5492
5602
  if (updateColInfo) {
5493
- colDef = this.tree["_columnsById"][options.colId];
5603
+ const colDef = this.tree["_columnsById"][options.colId];
5494
5604
  assert(colDef, `Invalid colId specified: ${options.colId}`);
5495
- order =
5496
- (_a = options.order) !== null && _a !== void 0 ? _a : rotate(colDef.sortOrder, ["asc", "desc", undefined]);
5605
+ order !== null && order !== void 0 ? order : (order = rotate(colDef.sortOrder, ["asc", "desc", undefined]));
5497
5606
  for (const col of this.tree.columns) {
5498
5607
  col.sortOrder = col === colDef ? order : undefined;
5499
5608
  }
5609
+ if (order === undefined) {
5610
+ propName = nativeOrderPropName;
5611
+ order = "asc";
5612
+ }
5500
5613
  this.tree.update(ChangeType.colStructure);
5501
5614
  }
5502
5615
  else {
5503
- order = (_b = options.order) !== null && _b !== void 0 ? _b : "asc";
5616
+ propName !== null && propName !== void 0 ? propName : (propName = "title");
5617
+ order !== null && order !== void 0 ? order : (order = "asc");
5618
+ }
5619
+ this.logDebug(`sort(), propName=${propName}, ${order}`, options);
5620
+ assert(propName || cmp || key, "No `propName` or `key` specified");
5621
+ // Define a key callback from the parameters we have
5622
+ if (key == null && cmp == null) {
5623
+ key = (node) => {
5624
+ let val;
5625
+ if (NODE_DICT_PROPS.has(propName)) {
5626
+ val = node[propName];
5627
+ }
5628
+ else {
5629
+ val = node.data[propName];
5630
+ }
5631
+ if (caseInsensitive && typeof val === "string") {
5632
+ val = val.toLowerCase();
5633
+ }
5634
+ return val;
5635
+ };
5504
5636
  }
5505
- let propName = (_c = options.propName) !== null && _c !== void 0 ? _c : (options.colId || "");
5506
- if (propName === "*") {
5507
- propName = "title";
5637
+ // Define a compare callback that uses the key callback
5638
+ if (cmp) {
5639
+ assert(!key, "`key` and `cmp` are mutually exclusive");
5640
+ tree.logDeprecate("SortOptions.cmp", {
5641
+ since: "0.14.0",
5642
+ hint: "use the `key` callback instead",
5643
+ });
5508
5644
  }
5509
- if (order == null) {
5510
- propName = nativeOrderPropName;
5511
- order = "asc";
5645
+ else {
5646
+ if (options.propName || options.caseInsensitive) {
5647
+ tree.logWarn("sort(): ignoring propName, caseInsensitive");
5648
+ }
5649
+ cmp = (a, b) => {
5650
+ if (isFolder) {
5651
+ const isFolderA = isFolder(a);
5652
+ if (isFolderA !== isFolder(b)) {
5653
+ return isFolderA ? -1 : 1;
5654
+ }
5655
+ }
5656
+ let x = key(a);
5657
+ let y = key(b);
5658
+ // Assure we have reasonable comparisons with null values:
5659
+ if (x == null) {
5660
+ x = typeof y === "string" ? "" : 0;
5661
+ }
5662
+ else if (typeof x === "boolean") {
5663
+ x = x ? 1 : 0;
5664
+ }
5665
+ if (y == null) {
5666
+ y = typeof x === "string" ? "" : 0;
5667
+ }
5668
+ else if (typeof y === "boolean") {
5669
+ y = y ? 1 : 0;
5670
+ }
5671
+ if (order === "desc") {
5672
+ return x === y ? 0 : x > y ? -1 : 1;
5673
+ }
5674
+ return x === y ? 0 : x > y ? 1 : -1;
5675
+ };
5512
5676
  }
5513
- this.logDebug(`sortByProperty(), propName=${propName}, ${order}`, options);
5514
- assert(propName, "No property name specified");
5515
- const cmp = (a, b) => {
5516
- let av, bv;
5517
- if (NODE_DICT_PROPS.has(propName)) {
5518
- av = a[propName];
5519
- bv = b[propName];
5520
- }
5521
- else {
5522
- av = a.data[propName];
5523
- bv = b.data[propName];
5524
- }
5525
- if (av == null && bv == null) {
5526
- return 0;
5527
- }
5528
- if (av == null) {
5529
- av = typeof bv === "string" ? "" : 0;
5530
- }
5531
- else if (typeof av === "boolean") {
5532
- av = av ? 1 : 0;
5533
- }
5534
- if (bv == null) {
5535
- bv = typeof av === "string" ? "" : 0;
5536
- }
5537
- else if (typeof bv === "boolean") {
5538
- bv = bv ? 1 : 0;
5677
+ function _sortChildren(cl) {
5678
+ if (!cl) {
5679
+ return;
5539
5680
  }
5540
- if (caseInsensitive) {
5541
- if (typeof av === "string") {
5542
- av = av.toLowerCase();
5543
- }
5544
- if (typeof bv === "string") {
5545
- bv = bv.toLowerCase();
5681
+ cl.sort(cmp);
5682
+ if (deep) {
5683
+ for (let i = 0, l = cl.length; i < l; i++) {
5684
+ if (cl[i].children) {
5685
+ _sortChildren(cl[i].children);
5686
+ }
5546
5687
  }
5547
5688
  }
5548
- if (order === "desc") {
5549
- return av === bv ? 0 : av > bv ? -1 : 1;
5689
+ }
5690
+ if (this.children) {
5691
+ _sortChildren(this.children);
5692
+ }
5693
+ this.tree.update(ChangeType.structure);
5694
+ // this.triggerModify("sort"); // TODO
5695
+ }
5696
+ /**
5697
+ * Re-apply current sorting if any (use after lazy load).
5698
+ * Example:
5699
+ * ```js
5700
+ * load: function (e) {
5701
+ * // Whe loading a lazy branch, apply current sort order if any
5702
+ * e.node.resort();
5703
+ * },
5704
+ * ```
5705
+ * @since 0.14.0
5706
+ */
5707
+ resort(options = {}) {
5708
+ for (const colDef of this.tree.columns) {
5709
+ if (colDef.sortOrder) {
5710
+ options.colId = colDef.id;
5711
+ options.order = colDef.sortOrder;
5712
+ this.sort(options);
5713
+ break;
5550
5714
  }
5551
- return av === bv ? 0 : av > bv ? 1 : -1;
5552
- };
5553
- return this.sortChildren(cmp, deep);
5715
+ }
5554
5716
  }
5555
5717
  /**
5556
5718
  * Trigger `modifyChild` event on a parent to signal that a child was modified.
@@ -5587,7 +5749,8 @@
5587
5749
  * @param {function} callback the callback function.
5588
5750
  * Return false to stop iteration, return "skip" to skip this node and
5589
5751
  * its children only.
5590
- * @see {@link IterableIterator<WunderbaumNode>}, {@link Wunderbaum.visit}.
5752
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
5753
+ * @see {@link Wunderbaum.visit}.
5591
5754
  */
5592
5755
  visit(callback, includeSelf = false) {
5593
5756
  let res = true;
@@ -5660,7 +5823,7 @@
5660
5823
  /*!
5661
5824
  * Wunderbaum - ext-edit
5662
5825
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
5663
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
5826
+ * v0.14.0, Fri, 20 Mar 2026 16:58:31 GMT (https://github.com/mar10/wunderbaum)
5664
5827
  */
5665
5828
  // const START_MARKER = "\uFFF7";
5666
5829
  class EditExtension extends WunderbaumExtension {
@@ -5895,7 +6058,7 @@
5895
6058
  newValue = newValue.trim();
5896
6059
  }
5897
6060
  if (!node) {
5898
- this.tree.logDebug("stopEditTitle: not in edit mode.");
6061
+ // this.tree.logDebug("stopEditTitle: not in edit mode.");
5899
6062
  return;
5900
6063
  }
5901
6064
  node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue);
@@ -5979,7 +6142,7 @@
5979
6142
  newNode.setClass("wb-edit-new");
5980
6143
  this.relatedNode = node;
5981
6144
  // Don't filter new nodes:
5982
- newNode.match = true;
6145
+ newNode.match = -1;
5983
6146
  newNode.makeVisible({ noAnimation: true }).then(() => {
5984
6147
  this.startEditTitle(newNode);
5985
6148
  });
@@ -5995,8 +6158,8 @@
5995
6158
  * https://github.com/mar10/wunderbaum
5996
6159
  *
5997
6160
  * Released under the MIT license.
5998
- * @version v0.12.1
5999
- * @date Sat, 22 Feb 2025 22:59:20 GMT
6161
+ * @version v0.14.0
6162
+ * @date Fri, 20 Mar 2026 16:58:31 GMT
6000
6163
  */
6001
6164
  // import "./wunderbaum.scss";
6002
6165
  class WbSystemRoot extends WunderbaumNode {
@@ -6045,18 +6208,21 @@
6045
6208
  this._disableUpdateIgnoreCount = 0;
6046
6209
  this._activeNode = null;
6047
6210
  this._focusNode = null;
6211
+ this._initialSource = null;
6048
6212
  /** Shared properties, referenced by `node.type`. */
6049
6213
  this.types = {};
6050
6214
  /** List of column definitions. */
6051
- this.columns = []; // any[] = [];
6215
+ this.columns = [];
6052
6216
  this._columnsById = {};
6053
6217
  // Modification Status
6054
6218
  this.pendingChangeTypes = new Set();
6055
6219
  /** Expose some useful methods of the util.ts module as `tree._util`. */
6056
6220
  this._util = util;
6057
6221
  // --- SELECT ---
6058
- // /** @internal */
6059
6222
  // public selectRangeAnchor: WunderbaumNode | null = null;
6223
+ // --- BREADCRUMB ---
6224
+ /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6225
+ this.breadcrumb = null;
6060
6226
  // --- FILTER ---
6061
6227
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6062
6228
  this.filterMode = null;
@@ -6071,45 +6237,57 @@
6071
6237
  this.lastQuicksearchTerm = "";
6072
6238
  // --- EDIT ---
6073
6239
  this.lastClickTime = 0;
6074
- const opts = (this.options = extend({
6075
- id: null,
6076
- source: null, // URL for GET/PUT, Ajax options, or callback
6077
- element: null, // <div class="wunderbaum">
6240
+ // Set default options and merge with user options
6241
+ const initOptions = Object.assign({
6242
+ id: undefined,
6243
+ source: [], // URL for GET/PUT, Ajax options, or callback
6244
+ element: unsafeCast(null),
6078
6245
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
6079
6246
  header: null, // Show/hide header (pass bool or string)
6080
- // headerHeightPx: ROW_HEIGHT,
6081
6247
  rowHeightPx: DEFAULT_ROW_HEIGHT,
6082
6248
  iconMap: "bootstrap",
6083
- columns: null,
6084
- types: null,
6085
- // escapeTitles: true,
6249
+ columns: [], //util.unsafeCast<ColumnDefinitionList>(null),
6250
+ types: {},
6086
6251
  enabled: true,
6087
6252
  fixedCol: false,
6088
6253
  showSpinner: false,
6089
6254
  checkbox: false,
6090
6255
  minExpandLevel: 0,
6091
6256
  emptyChildListExpandable: false,
6092
- // updateThrottleWait: 200,
6093
6257
  skeleton: false,
6094
- connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
6258
+ autoCollapse: false,
6259
+ adjustHeight: true,
6260
+ connectTopBreadcrumb: null,
6261
+ columnsFilterable: false,
6262
+ columnsMenu: false,
6263
+ columnsResizable: false,
6264
+ columnsSortable: false,
6095
6265
  selectMode: "multi", // SelectModeType
6266
+ scrollIntoViewOnExpandClick: true,
6267
+ // --- Extensions (actually set by exensions on init)
6268
+ dnd: unsafeCast(null),
6269
+ edit: unsafeCast(null),
6270
+ filter: unsafeCast(null),
6096
6271
  // --- KeyNav ---
6097
- navigationModeOption: null, // NavModeEnum.startRow,
6272
+ navigationModeOption: unsafeCast(null),
6098
6273
  quicksearch: true,
6099
6274
  // --- Events ---
6100
- iconBadge: null,
6101
- change: null,
6102
- // enhanceTitle: null,
6103
- error: null,
6104
- receive: null,
6275
+ // iconBadge: null,
6276
+ // change: null,
6277
+ // ...
6105
6278
  // --- Strings ---
6106
6279
  strings: {
6107
6280
  loadError: "Error",
6108
6281
  loading: "Loading...",
6109
- // loading: "Loading&hellip;",
6110
6282
  noData: "No data",
6283
+ breadcrumbDelimiter: " » ",
6284
+ queryResult: "Found ${matches} of ${count}",
6285
+ noMatch: "No results",
6286
+ matchIndex: "${match} of ${matches}",
6111
6287
  },
6112
- }, options));
6288
+ }, options);
6289
+ const opts = initOptions;
6290
+ this.options = opts;
6113
6291
  const readyDeferred = new Deferred();
6114
6292
  this.ready = readyDeferred.promise();
6115
6293
  let readyOk = false;
@@ -6136,7 +6314,8 @@
6136
6314
  this._callEvent("init", { error: err });
6137
6315
  }
6138
6316
  });
6139
- this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
6317
+ this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence;
6318
+ delete initOptions.id;
6140
6319
  this.root = new WbSystemRoot(this);
6141
6320
  this._registerExtension(new KeynavExtension(this));
6142
6321
  this._registerExtension(new EditExtension(this));
@@ -6146,19 +6325,20 @@
6146
6325
  this._registerExtension(new LoggerExtension(this));
6147
6326
  this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {});
6148
6327
  // --- Evaluate options
6149
- this.columns = opts.columns;
6150
- delete opts.columns;
6328
+ this.columns = initOptions.columns || [];
6329
+ delete initOptions.columns;
6151
6330
  if (!this.columns || !this.columns.length) {
6152
6331
  const title = typeof opts.header === "string" ? opts.header : this.id;
6153
6332
  this.columns = [{ id: "*", title: title, width: "*" }];
6154
6333
  }
6155
- if (opts.types) {
6156
- this.setTypes(opts.types, true);
6334
+ if (initOptions.types) {
6335
+ this.setTypes(initOptions.types, true);
6157
6336
  }
6158
- delete opts.types;
6337
+ delete initOptions.types;
6159
6338
  // --- Create Markup
6160
- this.element = elemFromSelector(opts.element);
6161
- assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
6339
+ this.element = elemFromSelector(initOptions.element);
6340
+ assert(!!this.element, `Invalid 'element' option: ${initOptions.element}`);
6341
+ delete initOptions.element;
6162
6342
  this.element.classList.add("wunderbaum");
6163
6343
  if (!this.element.getAttribute("tabindex")) {
6164
6344
  this.element.tabIndex = 0;
@@ -6213,6 +6393,19 @@
6213
6393
  this.headerElement =
6214
6394
  this.element.querySelector("div.wb-header");
6215
6395
  this.element.classList.toggle("wb-grid", this.columns.length > 1);
6396
+ if (this.options.connectTopBreadcrumb) {
6397
+ this.breadcrumb = elemFromSelector(this.options.connectTopBreadcrumb);
6398
+ assert(!this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`);
6399
+ this.breadcrumb.addEventListener("click", (e) => {
6400
+ // const node = Wunderbaum.getNode(e)!;
6401
+ const elem = e.target;
6402
+ if (elem && elem.matches("a.wb-breadcrumb")) {
6403
+ const node = this.keyMap.get(elem.dataset.key);
6404
+ node === null || node === void 0 ? void 0 : node.setActive();
6405
+ e.preventDefault();
6406
+ }
6407
+ });
6408
+ }
6216
6409
  this._initExtensions();
6217
6410
  // --- apply initial options
6218
6411
  ["enabled", "fixedCol"].forEach((optName) => {
@@ -6221,11 +6414,11 @@
6221
6414
  }
6222
6415
  });
6223
6416
  // --- Load initial data
6224
- if (opts.source) {
6417
+ if (initOptions.source) {
6225
6418
  if (opts.showSpinner) {
6226
6419
  this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
6227
6420
  }
6228
- this.load(opts.source)
6421
+ this.load(initOptions.source)
6229
6422
  .then(() => {
6230
6423
  // The source may have defined columns, so we may adjust the nav mode
6231
6424
  if (opts.navigationModeOption == null) {
@@ -6258,15 +6451,18 @@
6258
6451
  // has a wrong value at start???
6259
6452
  this.update(ChangeType.any);
6260
6453
  // --- Bind listeners
6261
- this.element.addEventListener("scroll", (e) => {
6262
- // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6263
- this.update(ChangeType.scroll);
6264
- });
6454
+ this._registerEventHandlers();
6265
6455
  this.resizeObserver = new ResizeObserver((entries) => {
6266
6456
  // this.log("ResizeObserver: Size changed", entries);
6267
6457
  this.update(ChangeType.resize);
6268
6458
  });
6269
6459
  this.resizeObserver.observe(this.element);
6460
+ }
6461
+ _registerEventHandlers() {
6462
+ this.element.addEventListener("scroll", (e) => {
6463
+ // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
6464
+ this.update(ChangeType.scroll);
6465
+ });
6270
6466
  onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
6271
6467
  var _a, _b;
6272
6468
  const info = Wunderbaum.getEventInfo(e);
@@ -6282,9 +6478,6 @@
6282
6478
  const node = info.node;
6283
6479
  const mouseEvent = e;
6284
6480
  // this.log("click", info);
6285
- // if (this._selectRange(info) === false) {
6286
- // return;
6287
- // }
6288
6481
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
6289
6482
  this.lastClickTime = Date.now();
6290
6483
  return false;
@@ -6303,20 +6496,22 @@
6303
6496
  (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) {
6304
6497
  node.startEditTitle();
6305
6498
  }
6306
- if (info.colIdx >= 0) {
6307
- node.setActive(true, { colIdx: info.colIdx, event: e });
6308
- }
6309
- else {
6310
- node.setActive(true, { event: e });
6311
- }
6312
6499
  if (info.region === NodeRegion.expander) {
6313
6500
  node.setExpanded(!node.isExpanded(), {
6314
- scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
6501
+ scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
6315
6502
  });
6316
6503
  }
6317
6504
  else if (info.region === NodeRegion.checkbox) {
6318
6505
  node.toggleSelected();
6319
6506
  }
6507
+ else {
6508
+ if (info.colIdx >= 0) {
6509
+ node.setActive(true, { colIdx: info.colIdx, event: e });
6510
+ }
6511
+ else {
6512
+ node.setActive(true, { event: e });
6513
+ }
6514
+ }
6320
6515
  }
6321
6516
  this.lastClickTime = Date.now();
6322
6517
  });
@@ -6352,7 +6547,7 @@
6352
6547
  const targetNode = Wunderbaum.getNode(e);
6353
6548
  this._callEvent("focus", { flag: flag, event: e });
6354
6549
  if (flag && this.isRowNav() && !this.isEditingTitle()) {
6355
- if (opts.navigationModeOption === NavModeEnum.row) {
6550
+ if (this.options.navigationModeOption === NavModeEnum.row) {
6356
6551
  targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive();
6357
6552
  }
6358
6553
  else {
@@ -6419,11 +6614,12 @@
6419
6614
  }
6420
6615
  /**
6421
6616
  * Return the icon-function -> icon-definition mapping.
6617
+ * @deprecated Use {@link Wunderbaum.iconMaps}
6422
6618
  */
6423
6619
  get iconMap() {
6424
6620
  const map = this.options.iconMap;
6425
6621
  if (typeof map === "string") {
6426
- return iconMaps[map];
6622
+ return defaultIconMaps[map];
6427
6623
  }
6428
6624
  return map;
6429
6625
  }
@@ -6476,7 +6672,38 @@
6476
6672
  ext.init();
6477
6673
  }
6478
6674
  }
6479
- /** Add node to tree's bookkeeping data structures. */
6675
+ /**
6676
+ * Calculate a *stable*, unique key for a node from its refKey (or title).
6677
+ * We also add information from the parent, because a refKey may occur multiple
6678
+ * times in a tree (but not as child of the same parent).
6679
+ * @internal
6680
+ */
6681
+ _calculateKey(data, parent) {
6682
+ if (data.key) {
6683
+ // Always use an explicitly passed key
6684
+ return data.key;
6685
+ }
6686
+ // Auto-keys are optional, use a monotonic counter by default:
6687
+ if (!this.options.autoKeys) {
6688
+ return "" + ++WunderbaumNode.sequence;
6689
+ }
6690
+ // Add the parent's key to the hash. Assuming this was generated by the
6691
+ // same algorithm, this should incorporate the whole path:
6692
+ const s = (parent ? parent.key : "") + (data.refKey || data.title);
6693
+ // 32-bit has a high probability of collisions, so we pump up to 64-bit
6694
+ // https://security.stackexchange.com/q/209882/207588
6695
+ const h1 = murmurHash3(s, true);
6696
+ let key = "id_" + h1 + murmurHash3(h1 + s, true);
6697
+ // Check for collisions
6698
+ // (Most likely if the same title occurs multiple in the same parent).
6699
+ const existingNode = this.keyMap.get(key);
6700
+ if (existingNode) {
6701
+ key += "." + ++Wunderbaum.sequence;
6702
+ this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data);
6703
+ }
6704
+ return key;
6705
+ }
6706
+ /** Add node to tree's bookkeeping data structures. @internal */
6480
6707
  _registerNode(node) {
6481
6708
  const key = node.key;
6482
6709
  assert(key != null, `Missing key: '${node}'.`);
@@ -6493,7 +6720,7 @@
6493
6720
  }
6494
6721
  }
6495
6722
  }
6496
- /** Remove node from tree's bookkeeping data structures. */
6723
+ /** Remove node from tree's bookkeeping data structures. @internal */
6497
6724
  _unregisterNode(node) {
6498
6725
  // Remove refKey reference from map (if any)
6499
6726
  const rk = node.refKey;
@@ -6576,7 +6803,10 @@
6576
6803
  });
6577
6804
  return node;
6578
6805
  }
6579
- /** Return the topmost visible node in the viewport. */
6806
+ /** Return the topmost visible node in the viewport.
6807
+ * @param complete If `false`, the node is considered visible if at least one
6808
+ * pixel is visible.
6809
+ */
6580
6810
  getTopmostVpNode(complete = true) {
6581
6811
  const rowHeight = this.options.rowHeightPx;
6582
6812
  const gracePx = 1; // ignore subpixel scrolling
@@ -6609,7 +6839,7 @@
6609
6839
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6610
6840
  return this._getNodeByRowIdx(bottomIdx);
6611
6841
  }
6612
- /** Return preceeding visible node in the viewport. */
6842
+ /** Return preceding visible node in the viewport. */
6613
6843
  _getPrevNodeInView(node, ofs = 1) {
6614
6844
  this.visitRows((n) => {
6615
6845
  node = n;
@@ -6620,13 +6850,18 @@
6620
6850
  return node;
6621
6851
  }
6622
6852
  /** Return following visible node in the viewport. */
6623
- _getNextNodeInView(node, ofs = 1) {
6853
+ _getNextNodeInView(node, options) {
6854
+ let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
6855
+ const reverse = !!(options === null || options === void 0 ? void 0 : options.reverse);
6624
6856
  this.visitRows((n) => {
6625
6857
  node = n;
6858
+ if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) {
6859
+ return false;
6860
+ }
6626
6861
  if (ofs-- <= 0) {
6627
6862
  return false;
6628
6863
  }
6629
- }, { reverse: false, start: node || this.getActiveNode() });
6864
+ }, { reverse: reverse, start: node || this.getActiveNode() });
6630
6865
  return node;
6631
6866
  }
6632
6867
  /**
@@ -6746,9 +6981,11 @@
6746
6981
  case "first":
6747
6982
  case "last":
6748
6983
  case "left":
6984
+ case "nextMatch":
6749
6985
  case "pageDown":
6750
6986
  case "pageUp":
6751
6987
  case "parent":
6988
+ case "prevMatch":
6752
6989
  case "right":
6753
6990
  case "up":
6754
6991
  return node.navigate(cmd);
@@ -6863,22 +7100,39 @@
6863
7100
  /** Run code, but defer rendering of viewport until done.
6864
7101
  *
6865
7102
  * ```js
6866
- * tree.runWithDeferredUpdate(() => {
6867
- * return someFuncThatWouldUpdateManyNodes();
7103
+ * const res = tree.runWithDeferredUpdate(() => {
7104
+ * return someFunctionThatWouldUpdateManyNodes();
6868
7105
  * });
6869
7106
  * ```
6870
7107
  */
6871
- runWithDeferredUpdate(func, hint = null) {
7108
+ runWithDeferredUpdate(func) {
6872
7109
  try {
6873
7110
  this.enableUpdate(false);
6874
7111
  const res = func();
6875
- assert(!(res instanceof Promise), `Promise return not allowed: ${res}`);
7112
+ assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`);
6876
7113
  return res;
6877
7114
  }
6878
7115
  finally {
6879
7116
  this.enableUpdate(true);
6880
7117
  }
6881
7118
  }
7119
+ /** Run code, but defer rendering of viewport until done.
7120
+ *
7121
+ * ```js
7122
+ * const res = await tree.runWithDeferredUpdate(async () => {
7123
+ * return someAsyncFunctionThatWouldUpdateManyNodes();
7124
+ * });
7125
+ * ```
7126
+ */
7127
+ async runWithDeferredUpdateAsync(func) {
7128
+ try {
7129
+ this.enableUpdate(false);
7130
+ return await func();
7131
+ }
7132
+ finally {
7133
+ this.enableUpdate(true);
7134
+ }
7135
+ }
6882
7136
  /** Recursively expand all expandable nodes (triggers lazy load if needed). */
6883
7137
  async expandAll(flag = true, options) {
6884
7138
  await this.root.expandAll(flag, options);
@@ -6898,6 +7152,17 @@
6898
7152
  getSelectedNodes(stopOnParents = false) {
6899
7153
  return this.root.getSelectedNodes(stopOnParents);
6900
7154
  }
7155
+ /**
7156
+ * Return an array of refKey values.
7157
+ *
7158
+ * RefKeys are unique identifiers for a node data, and are used to identify
7159
+ * clones.
7160
+ * If more than one node has the same refKey, it is only returned once.
7161
+ * @param selected if true, only return refKeys of selected nodes.
7162
+ */
7163
+ getRefKeys(selected = false) {
7164
+ return this.root.getRefKeys(selected);
7165
+ }
6901
7166
  /*
6902
7167
  * Return an array of selected nodes.
6903
7168
  */
@@ -6940,6 +7205,11 @@
6940
7205
  count(visible = false) {
6941
7206
  return visible ? this.treeRowCount : this.keyMap.size;
6942
7207
  }
7208
+ /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`.
7209
+ */
7210
+ countUnique() {
7211
+ return this.refKeyMap.size;
7212
+ }
6943
7213
  /** @internal sanity check. */
6944
7214
  _check() {
6945
7215
  let i = 0;
@@ -6998,12 +7268,14 @@
6998
7268
  * and wrap-around at the end.
6999
7269
  * Used by quicksearch and keyboard navigation.
7000
7270
  */
7001
- findNextNode(match, startNode) {
7271
+ findNextNode(match, startNode, reverse = false) {
7002
7272
  //, visibleOnly) {
7003
7273
  let res = null;
7004
7274
  const firstNode = this.getFirstChild();
7275
+ // Last visible node (calculation is expensive, so do only if we need it):
7276
+ const lastNode = reverse ? this.findRelatedNode(firstNode, "last") : null;
7005
7277
  const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
7006
- startNode = startNode || firstNode;
7278
+ startNode = startNode || (reverse ? lastNode : firstNode);
7007
7279
  function _checkNode(n) {
7008
7280
  // console.log("_check " + n)
7009
7281
  if (matcher(n)) {
@@ -7016,12 +7288,14 @@
7016
7288
  this.visitRows(_checkNode, {
7017
7289
  start: startNode,
7018
7290
  includeSelf: false,
7291
+ reverse: reverse,
7019
7292
  });
7020
7293
  // Wrap around search
7021
7294
  if (!res && startNode !== firstNode) {
7022
7295
  this.visitRows(_checkNode, {
7023
- start: firstNode,
7296
+ start: reverse ? lastNode : firstNode,
7024
7297
  includeSelf: true,
7298
+ reverse: reverse,
7025
7299
  });
7026
7300
  }
7027
7301
  return res;
@@ -7088,7 +7362,7 @@
7088
7362
  // }
7089
7363
  break;
7090
7364
  case "up":
7091
- res = this._getPrevNodeInView(node);
7365
+ res = this._getNextNodeInView(node, { reverse: true });
7092
7366
  break;
7093
7367
  case "down":
7094
7368
  res = this._getNextNodeInView(node);
@@ -7101,7 +7375,10 @@
7101
7375
  res = bottomNode;
7102
7376
  }
7103
7377
  else {
7104
- res = this._getNextNodeInView(node, pageSize);
7378
+ res = this._getNextNodeInView(node, {
7379
+ reverse: false,
7380
+ ofs: pageSize,
7381
+ });
7105
7382
  }
7106
7383
  }
7107
7384
  break;
@@ -7116,10 +7393,23 @@
7116
7393
  res = topNode;
7117
7394
  }
7118
7395
  else {
7119
- res = this._getPrevNodeInView(node, pageSize);
7396
+ res = this._getNextNodeInView(node, {
7397
+ reverse: true,
7398
+ ofs: pageSize,
7399
+ });
7120
7400
  }
7121
7401
  }
7122
7402
  break;
7403
+ case "prevMatch":
7404
+ // fallthrough
7405
+ case "nextMatch":
7406
+ if (!this.isFilterActive) {
7407
+ this.logWarn(`${where}: Filter is not active.`);
7408
+ break;
7409
+ }
7410
+ res = this.findNextNode((n) => n.isMatched(), node, where === "prevMatch");
7411
+ res === null || res === void 0 ? void 0 : res.setActive();
7412
+ break;
7123
7413
  default:
7124
7414
  this.logWarn("Unknown relation '" + where + "'.");
7125
7415
  }
@@ -7154,6 +7444,18 @@
7154
7444
  format(name_cb, connectors) {
7155
7445
  return this.root.format(name_cb, connectors);
7156
7446
  }
7447
+ /**
7448
+ * Always returns null (so a tree instance behaves as `tree.root`).
7449
+ */
7450
+ get parent() {
7451
+ return null;
7452
+ }
7453
+ /**
7454
+ * Return a list of top-level nodes.
7455
+ */
7456
+ get children() {
7457
+ return this.root.children || [];
7458
+ }
7157
7459
  /**
7158
7460
  * Return the active cell (`span.wb-col`) of the currently active node or null.
7159
7461
  */
@@ -7181,6 +7483,12 @@
7181
7483
  getFirstChild() {
7182
7484
  return this.root.getFirstChild();
7183
7485
  }
7486
+ /**
7487
+ * Return the last top level node if any (not the invisible root node).
7488
+ */
7489
+ getLastChild() {
7490
+ return this.root.getLastChild();
7491
+ }
7184
7492
  /**
7185
7493
  * Return the node that currently has keyboard focus or null.
7186
7494
  * Alias for {@link Wunderbaum.focusNode}.
@@ -7266,7 +7574,7 @@
7266
7574
  }
7267
7575
  /** Return true if any node title or grid cell is currently beeing edited.
7268
7576
  *
7269
- * See also {@link Wunderbaum.isEditingTitle}.
7577
+ * See also {@link isEditingTitle}.
7270
7578
  */
7271
7579
  isEditing() {
7272
7580
  const focusElem = this.nodeListElement.querySelector("input:focus,select:focus");
@@ -7274,7 +7582,7 @@
7274
7582
  }
7275
7583
  /** Return true if any node is currently in edit-title mode.
7276
7584
  *
7277
- * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
7585
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
7278
7586
  */
7279
7587
  isEditingTitle() {
7280
7588
  return this._callMethod("edit.isEditingTitle");
@@ -7294,7 +7602,7 @@
7294
7602
  return res;
7295
7603
  }
7296
7604
  /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
7297
- * @see {@link Wunderbaum.logDebug}
7605
+ * @see {@link logDebug}
7298
7606
  */
7299
7607
  log(...args) {
7300
7608
  if (this.options.debugLevel >= 4) {
@@ -7303,7 +7611,7 @@
7303
7611
  }
7304
7612
  /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
7305
7613
  * and browser console level includes debug/verbose messages.
7306
- * @see {@link Wunderbaum.log}
7614
+ * @see {@link log}
7307
7615
  */
7308
7616
  logDebug(...args) {
7309
7617
  if (this.options.debugLevel >= 4) {
@@ -7341,6 +7649,19 @@
7341
7649
  console.warn(this.toString(), ...args); // eslint-disable-line no-console
7342
7650
  }
7343
7651
  }
7652
+ /** Emit a warning for deprecated methods. @internal */
7653
+ logDeprecate(method, options) {
7654
+ if (this.options.debugLevel >= 2) {
7655
+ let msg = `${this}: ${method} is deprecated`;
7656
+ if (options === null || options === void 0 ? void 0 : options.since) {
7657
+ msg += ` since ${options.since}`;
7658
+ }
7659
+ if (options === null || options === void 0 ? void 0 : options.hint) {
7660
+ msg += ` (${options.since})`;
7661
+ }
7662
+ console.warn(msg + "."); // eslint-disable-line no-console
7663
+ }
7664
+ }
7344
7665
  /** Reset column widths to default. @since 0.10.0 */
7345
7666
  resetColumns() {
7346
7667
  this.columns.forEach((col) => {
@@ -7508,6 +7829,69 @@
7508
7829
  _setFocusNode(node) {
7509
7830
  this._focusNode = node;
7510
7831
  }
7832
+ /** Return the current selection/expansion/activation status. @experimental */
7833
+ getState(options = {}) {
7834
+ var _a, _b;
7835
+ const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options;
7836
+ const expandSet = new Set();
7837
+ if (expandedKeys) {
7838
+ for (const node of this) {
7839
+ if (node.isExpanded() && node.hasChildren()) {
7840
+ expandSet.add(node.key);
7841
+ }
7842
+ }
7843
+ }
7844
+ // Parents of active node are always expanded
7845
+ if (activeKey && this.activeNode) {
7846
+ this.activeNode.visitParents((n) => {
7847
+ if (n.parent) {
7848
+ expandSet.add(n.key);
7849
+ }
7850
+ }, false);
7851
+ }
7852
+ const state = {
7853
+ expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
7854
+ activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
7855
+ activeColIdx: this.activeColIdx,
7856
+ selectedKeys: selectedKeys
7857
+ ? this.getSelectedNodes().flatMap((n) => n.key)
7858
+ : undefined,
7859
+ };
7860
+ return state;
7861
+ }
7862
+ /** Apply selection/expansion/activation status. @experimental */
7863
+ async setState(state, options = {}) {
7864
+ const { expandLazy = true } = options;
7865
+ return this.runWithDeferredUpdateAsync(async () => {
7866
+ var _a, _b;
7867
+ if (state.expandedKeys && state.expandedKeys.length) {
7868
+ if (expandLazy) {
7869
+ // Expand all keys recursively, even if they are not in the tree yet
7870
+ await this._loadLazyNodes(state.expandedKeys, {
7871
+ expand: true,
7872
+ noEvents: true,
7873
+ });
7874
+ }
7875
+ else {
7876
+ for (const key of state.expandedKeys) {
7877
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true);
7878
+ }
7879
+ }
7880
+ }
7881
+ if (state.activeKey) {
7882
+ this.setActiveNode(state.activeKey);
7883
+ }
7884
+ if (state.selectedKeys) {
7885
+ this.selectAll(false);
7886
+ for (const key of state.selectedKeys) {
7887
+ (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true);
7888
+ }
7889
+ }
7890
+ if (this.isCellNav() && state.activeColIdx != null) {
7891
+ this.setColumn(state.activeColIdx);
7892
+ }
7893
+ });
7894
+ }
7511
7895
  update(change, node, options) {
7512
7896
  // this.log(`update(${change}) node=${node}`);
7513
7897
  if (!(node instanceof WunderbaumNode)) {
@@ -7664,18 +8048,33 @@
7664
8048
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
7665
8049
  * (defaults to sorting by title).
7666
8050
  * @param {boolean} deep pass true to sort all descendant nodes recursively
8051
+ * @deprecated use {@link sort}
7667
8052
  */
7668
8053
  sortChildren(cmp = nodeTitleSorter, deep = false) {
7669
- this.root.sortChildren(cmp, deep);
8054
+ this.logDeprecate("sortChildren()", { since: "0.14.0" });
8055
+ return this.sort({
8056
+ cmp: cmp ? cmp : undefined,
8057
+ deep: deep,
8058
+ propName: "title",
8059
+ });
7670
8060
  }
7671
8061
  /**
7672
8062
  * Convenience method to implement column sorting.
7673
8063
  * @see {@link WunderbaumNode.sortByProperty}.
7674
8064
  * @since 0.11.0
8065
+ * @deprecated use {@link sort}
7675
8066
  */
7676
8067
  sortByProperty(options) {
8068
+ this.logDeprecate("sortByProperty()", { since: "0.14.0" });
7677
8069
  this.root.sortByProperty(options);
7678
8070
  }
8071
+ /**
8072
+ * Sort nodes list by title or custom criteria.
8073
+ * @since 0.14.0
8074
+ */
8075
+ sort(options) {
8076
+ this.root.sort(options);
8077
+ }
7679
8078
  /** Convert tree to an array of plain objects.
7680
8079
  *
7681
8080
  * @param callback is called for every node, in order to allow
@@ -7789,11 +8188,11 @@
7789
8188
  // }
7790
8189
  return modified;
7791
8190
  }
7792
- _insertIcon(icon, elem) {
7793
- const iconElem = document.createElement("i");
7794
- iconElem.className = icon;
7795
- elem.appendChild(iconElem);
7796
- }
8191
+ // protected _insertIcon(icon: string, elem: HTMLElement) {
8192
+ // const iconElem = document.createElement("i");
8193
+ // iconElem.className = icon;
8194
+ // elem.appendChild(iconElem);
8195
+ // }
7797
8196
  /** Create/update header markup from `this.columns` definition.
7798
8197
  * @internal
7799
8198
  */
@@ -7891,6 +8290,102 @@
7891
8290
  this._updateViewportImmediately();
7892
8291
  }
7893
8292
  }
8293
+ /** @internal */
8294
+ _createNodeIcon(node, showLoading, showBadge) {
8295
+ const iconMap = this.iconMap;
8296
+ let iconElem;
8297
+ let icon = node.getOption("icon");
8298
+ if (node._errorInfo) {
8299
+ icon = iconMap.error;
8300
+ }
8301
+ else if (node._isLoading && showLoading) {
8302
+ // Status nodes, or nodes without expander (< minExpandLevel) should
8303
+ // display the 'loading' status with the i.wb-icon span
8304
+ icon = iconMap.loading;
8305
+ }
8306
+ if (icon === false) {
8307
+ return null; // explicitly disabled: don't try default icons
8308
+ }
8309
+ if (typeof icon === "string") ;
8310
+ else if (node.statusNodeType) {
8311
+ icon = iconMap[node.statusNodeType];
8312
+ }
8313
+ else if (node.expanded) {
8314
+ icon = iconMap.folderOpen;
8315
+ }
8316
+ else if (node.children) {
8317
+ icon = iconMap.folder;
8318
+ }
8319
+ else if (node.lazy) {
8320
+ icon = iconMap.folderLazy;
8321
+ }
8322
+ else {
8323
+ icon = iconMap.doc;
8324
+ }
8325
+ if (!icon) {
8326
+ iconElem = document.createElement("i");
8327
+ iconElem.className = "wb-icon";
8328
+ }
8329
+ else if (TEST_HTML.test(icon)) {
8330
+ iconElem = elemFromHtml(icon);
8331
+ }
8332
+ else if (TEST_FILE_PATH.test(icon)) {
8333
+ iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
8334
+ }
8335
+ else {
8336
+ // Class name
8337
+ iconElem = document.createElement("i");
8338
+ iconElem.className = "wb-icon " + icon;
8339
+ }
8340
+ // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
8341
+ const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem });
8342
+ let badge = null;
8343
+ if (cbRes != null && cbRes !== false) {
8344
+ let classes = "";
8345
+ let tooltip = "";
8346
+ if (isPlainObject(cbRes)) {
8347
+ badge = "" + cbRes.badge;
8348
+ classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
8349
+ tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
8350
+ }
8351
+ else if (typeof cbRes === "number") {
8352
+ badge = "" + cbRes;
8353
+ }
8354
+ else {
8355
+ badge = cbRes; // string or HTMLSpanElement
8356
+ }
8357
+ if (typeof badge === "string") {
8358
+ badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
8359
+ }
8360
+ if (badge) {
8361
+ iconElem.append(badge);
8362
+ }
8363
+ }
8364
+ return iconElem;
8365
+ }
8366
+ _updateTopBreadcrumb() {
8367
+ const breadcrumb = this.breadcrumb;
8368
+ const topmost = this.getTopmostVpNode(true);
8369
+ const parentList = topmost === null || topmost === void 0 ? void 0 : topmost.getParentList(false, false);
8370
+ if (parentList === null || parentList === void 0 ? void 0 : parentList.length) {
8371
+ breadcrumb.innerHTML = "";
8372
+ for (const n of topmost.getParentList(false, false)) {
8373
+ const icon = this._createNodeIcon(n, false, false);
8374
+ if (icon) {
8375
+ breadcrumb.append(icon, " ");
8376
+ }
8377
+ const part = document.createElement("a");
8378
+ part.textContent = n.title;
8379
+ part.href = "#";
8380
+ part.classList.add("wb-breadcrumb");
8381
+ part.dataset.key = n.key;
8382
+ breadcrumb.append(part, this.options.strings.breadcrumbDelimiter);
8383
+ }
8384
+ }
8385
+ else {
8386
+ breadcrumb.innerHTML = "&nbsp;";
8387
+ }
8388
+ }
7894
8389
  /**
7895
8390
  * This is the actual update method, which is wrapped inside a throttle method.
7896
8391
  * It calls `updateColumns()` and `_updateRows()`.
@@ -7901,7 +8396,6 @@
7901
8396
  * @internal
7902
8397
  */
7903
8398
  _updateViewportImmediately() {
7904
- var _a;
7905
8399
  if (this._disableUpdateCount) {
7906
8400
  this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`);
7907
8401
  this._disableUpdateIgnoreCount++;
@@ -7948,11 +8442,8 @@
7948
8442
  this._updateRows();
7949
8443
  // console.profileEnd(`_updateViewportImmediately()`)
7950
8444
  }
7951
- if (this.options.connectTopBreadcrumb) {
7952
- assert(this.options.connectTopBreadcrumb.textContent != null, `Invalid 'connectTopBreadcrumb' option (input element expected).`);
7953
- let path = (_a = this.getTopmostVpNode(true)) === null || _a === void 0 ? void 0 : _a.getPath(false, "title", " > ");
7954
- path = path ? path + " >" : "";
7955
- this.options.connectTopBreadcrumb.textContent = path;
8445
+ if (this.breadcrumb) {
8446
+ this._updateTopBreadcrumb();
7956
8447
  }
7957
8448
  this._callEvent("update");
7958
8449
  }
@@ -8076,7 +8567,8 @@
8076
8567
  }
8077
8568
  /**
8078
8569
  * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
8079
- * @see {@link IterableIterator<WunderbaumNode>}, {@link WunderbaumNode.visit}.
8570
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
8571
+ * @see {@link WunderbaumNode.visit}.
8080
8572
  *
8081
8573
  * @param {function} callback the callback function.
8082
8574
  * Return false to stop iteration, return "skip" to skip this node and
@@ -8219,11 +8711,71 @@
8219
8711
  *
8220
8712
  * Previous data is cleared. Note that also column- and type defintions may
8221
8713
  * be passed with the `source` object.
8714
+ * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request
8715
+ * and restore the previous state.
8222
8716
  */
8223
- load(source) {
8717
+ async load(source) {
8224
8718
  this.clear();
8719
+ this._initialSource = source;
8225
8720
  return this.root.load(source);
8226
8721
  }
8722
+ /** Reload the tree and optionally restore state.
8723
+ * Source defaults to last ajax url if any.
8724
+ * Restoring the active node requires stable keys
8725
+ * @see {@link WunderbaumOptions.autoKeys}
8726
+ * @see {@link Wunderbaum.load}
8727
+ * @experimental
8728
+ */
8729
+ async reload(options = {}) {
8730
+ const { source = this._initialSource, reactivate = true } = options;
8731
+ if (!source) {
8732
+ this.logWarn("No previous ajax source to reload.");
8733
+ return;
8734
+ }
8735
+ if (!reactivate) {
8736
+ return this.load(source);
8737
+ }
8738
+ const state = this.getState();
8739
+ await this.load(source);
8740
+ return this.setState(state);
8741
+ }
8742
+ /**
8743
+ * Make sure that all nodes in the given keyList are accessible.
8744
+ * This may include loading lazy parent nodes.
8745
+ * Recursively load (and optionally expand) all requested node paths.
8746
+ */
8747
+ async _loadLazyNodes(keyList, options = {}) {
8748
+ const { expand = true } = options;
8749
+ const keySet = new Set(keyList);
8750
+ // Make sure that all parent nodes are loaded (and expand if requested)
8751
+ while (keySet.size > 0) {
8752
+ const pendingNodes = [];
8753
+ const curSet = new Set(keySet);
8754
+ for (const key of curSet) {
8755
+ const node = this.findKey(key);
8756
+ if (!node) {
8757
+ continue; // key not yet found (need to load lazy parent?)
8758
+ }
8759
+ keySet.delete(key);
8760
+ if (expand) {
8761
+ pendingNodes.push(node.setExpanded(true));
8762
+ }
8763
+ else if (node.isUnloaded()) {
8764
+ pendingNodes.push(node.loadLazy());
8765
+ }
8766
+ if (node._rowElem) {
8767
+ node._render(); // show spinner even is update is suppressed
8768
+ }
8769
+ }
8770
+ if (pendingNodes.length === 0) {
8771
+ // will not load any more nodes, so if if there are still keys
8772
+ // left in the set, we will never find them
8773
+ this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet);
8774
+ break;
8775
+ }
8776
+ await Promise.allSettled(pendingNodes);
8777
+ }
8778
+ }
8227
8779
  /**
8228
8780
  * Disable render requests during operations that would trigger many updates.
8229
8781
  *
@@ -8321,9 +8873,21 @@
8321
8873
  }
8322
8874
  Wunderbaum.sequence = 0;
8323
8875
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8324
- Wunderbaum.version = "v0.12.1"; // Set to semver by 'grunt release'
8876
+ Wunderbaum.version = "v0.14.0"; // Set to semver by 'grunt release'
8325
8877
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8326
8878
  Wunderbaum.util = util;
8879
+ /** A map of default iconMaps.
8880
+ * May be used as default, when passing partial icon definition maps:
8881
+ * ```js
8882
+ * const tree = new mar10.Wunderbaum({
8883
+ * ...
8884
+ * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
8885
+ * folder: "bi bi-archive",
8886
+ * }),
8887
+ * });
8888
+ * ```
8889
+ */
8890
+ Wunderbaum.iconMaps = defaultIconMaps;
8327
8891
 
8328
8892
  exports.Wunderbaum = Wunderbaum;
8329
8893