wunderbaum 0.12.1 → 0.13.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.13.0, Sat, 08 Mar 2025 14:16: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.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
303
303
  */
304
304
  /** @module util */
305
305
  /** Readable names for `MouseEvent.button` */
@@ -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
  *
@@ -1153,7 +1141,7 @@
1153
1141
  /*!
1154
1142
  * Wunderbaum - types
1155
1143
  * 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)
1144
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1157
1145
  */
1158
1146
  /**
1159
1147
  * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
@@ -1221,7 +1209,7 @@
1221
1209
  /*!
1222
1210
  * Wunderbaum - wb_extension_base
1223
1211
  * 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)
1212
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1225
1213
  */
1226
1214
  class WunderbaumExtension {
1227
1215
  constructor(tree, id, defaults) {
@@ -1280,7 +1268,7 @@
1280
1268
  /*!
1281
1269
  * Wunderbaum - ext-filter
1282
1270
  * 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)
1271
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1284
1272
  */
1285
1273
  const START_MARKER = "\uFFF7";
1286
1274
  const END_MARKER = "\uFFF8";
@@ -1292,7 +1280,7 @@
1292
1280
  autoApply: true, // Re-apply last filter if lazy data is loaded
1293
1281
  autoExpand: false, // Expand all branches that contain matches while filtered
1294
1282
  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
1283
+ connect: null, // Element or selector of an input control for filter query strings
1296
1284
  fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
1297
1285
  hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
1298
1286
  highlight: true, // Highlight matches by wrapping inside <mark> tags
@@ -1300,36 +1288,117 @@
1300
1288
  mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
1301
1289
  noData: true, // Display a 'no data' status node if result is empty
1302
1290
  });
1291
+ this.queryInput = null;
1292
+ this.prevButton = null;
1293
+ this.nextButton = null;
1294
+ this.modeButton = null;
1295
+ this.matchInfoElem = null;
1303
1296
  this.lastFilterArgs = null;
1304
1297
  }
1305
1298
  init() {
1306
1299
  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));
1300
+ const connect = this.getPluginOption("connect");
1301
+ if (connect) {
1302
+ this._connectControls();
1315
1303
  }
1316
1304
  }
1317
1305
  setPluginOption(name, value) {
1318
- // alert("filter opt=" + name + ", " + value)
1319
1306
  super.setPluginOption(name, value);
1320
1307
  switch (name) {
1321
1308
  case "mode":
1322
- this.tree.filterMode = value === "hide" ? "hide" : "dim";
1309
+ this.tree.filterMode =
1310
+ value === "hide" ? "hide" : value === "mark" ? "mark" : "dim";
1323
1311
  this.tree.updateFilter();
1324
1312
  break;
1325
1313
  }
1326
1314
  }
1315
+ _updatedConnectedControls() {
1316
+ var _a;
1317
+ const filterActive = this.tree.filterMode !== null;
1318
+ const activeNode = this.tree.getActiveNode();
1319
+ const matchCount = filterActive ? this.countMatches() : 0;
1320
+ const strings = this.treeOpts.strings;
1321
+ let matchIdx = "?";
1322
+ if (this.matchInfoElem) {
1323
+ if (filterActive) {
1324
+ let info;
1325
+ if (matchCount === 0) {
1326
+ info = strings.noMatch;
1327
+ }
1328
+ else if (activeNode && activeNode.match >= 1) {
1329
+ matchIdx = (_a = activeNode.match) !== null && _a !== void 0 ? _a : "?";
1330
+ info = strings.matchIndex;
1331
+ }
1332
+ else {
1333
+ info = strings.queryResult;
1334
+ }
1335
+ info = info
1336
+ .replace("${count}", this.tree.count().toLocaleString())
1337
+ .replace("${match}", "" + matchIdx)
1338
+ .replace("${matches}", matchCount.toLocaleString());
1339
+ this.matchInfoElem.textContent = info;
1340
+ }
1341
+ else {
1342
+ this.matchInfoElem.textContent = "";
1343
+ }
1344
+ }
1345
+ if (this.nextButton instanceof HTMLButtonElement) {
1346
+ this.nextButton.disabled = !matchCount;
1347
+ }
1348
+ if (this.prevButton instanceof HTMLButtonElement) {
1349
+ this.prevButton.disabled = !matchCount;
1350
+ }
1351
+ if (this.modeButton) {
1352
+ this.modeButton.disabled = !filterActive;
1353
+ this.modeButton.classList.toggle("wb-filter-hide", this.tree.filterMode === "hide");
1354
+ }
1355
+ }
1356
+ _connectControls() {
1357
+ const tree = this.tree;
1358
+ const connect = this.getPluginOption("connect");
1359
+ if (!connect) {
1360
+ return;
1361
+ }
1362
+ this.queryInput = elemFromSelector(connect.inputElem);
1363
+ if (!this.queryInput) {
1364
+ throw new Error(`Invalid 'filter.connect' option: ${connect.inputElem}.`);
1365
+ }
1366
+ this.prevButton = elemFromSelector(connect.prevButton);
1367
+ this.nextButton = elemFromSelector(connect.nextButton);
1368
+ this.modeButton = elemFromSelector(connect.modeButton);
1369
+ this.matchInfoElem = elemFromSelector(connect.matchInfoElem);
1370
+ if (this.prevButton) {
1371
+ onEvent(this.prevButton, "click", () => {
1372
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "prevMatch");
1373
+ this._updatedConnectedControls();
1374
+ });
1375
+ }
1376
+ if (this.nextButton) {
1377
+ onEvent(this.nextButton, "click", () => {
1378
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "nextMatch");
1379
+ this._updatedConnectedControls();
1380
+ });
1381
+ }
1382
+ if (this.modeButton) {
1383
+ onEvent(this.modeButton, "click", (e) => {
1384
+ if (!this.tree.filterMode) {
1385
+ return;
1386
+ }
1387
+ this.setPluginOption("mode", tree.filterMode === "dim" ? "hide" : "dim");
1388
+ });
1389
+ }
1390
+ onEvent(this.queryInput, "input", debounce((e) => {
1391
+ this.filterNodes(this.queryInput.value.trim(), {});
1392
+ }, 700));
1393
+ this._updatedConnectedControls();
1394
+ }
1327
1395
  _applyFilterNoUpdate(filter, _opts) {
1328
1396
  return this.tree.runWithDeferredUpdate(() => {
1329
1397
  return this._applyFilterImpl(filter, _opts);
1330
1398
  });
1331
1399
  }
1332
1400
  _applyFilterImpl(filter, _opts) {
1401
+ var _a;
1333
1402
  let //temp,
1334
1403
  count = 0;
1335
1404
  const start = Date.now();
@@ -1411,11 +1480,11 @@
1411
1480
  return !!res;
1412
1481
  };
1413
1482
  }
1414
- tree.filterMode = opts.mode;
1483
+ tree.filterMode = (_a = opts.mode) !== null && _a !== void 0 ? _a : "dim";
1415
1484
  // eslint-disable-next-line prefer-rest-params
1416
1485
  this.lastFilterArgs = arguments;
1417
1486
  tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
1418
- tree.element.classList.toggle("wb-ext-filter-dim", !hideMode);
1487
+ tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim");
1419
1488
  tree.element.classList.toggle("wb-ext-filter-hide-expanders", !!opts.hideExpanders);
1420
1489
  // Reset current filter
1421
1490
  tree.root.subMatchCount = 0;
@@ -1424,10 +1493,6 @@
1424
1493
  delete node.titleWithHighlight;
1425
1494
  node.subMatchCount = 0;
1426
1495
  });
1427
- // statusNode = tree.root.findDirectChild(KEY_NODATA);
1428
- // if (statusNode) {
1429
- // statusNode.remove();
1430
- // }
1431
1496
  tree.setStatus(NodeStatusType.ok);
1432
1497
  // Adjust node.hide, .match, and .subMatchCount properties
1433
1498
  treeOpts.autoCollapse = false; // #528
@@ -1438,7 +1503,7 @@
1438
1503
  let res = filter(node);
1439
1504
  if (res === "skip") {
1440
1505
  node.visit(function (c) {
1441
- c.match = false;
1506
+ c.match = undefined;
1442
1507
  }, true);
1443
1508
  return "skip";
1444
1509
  }
@@ -1449,7 +1514,7 @@
1449
1514
  }
1450
1515
  if (res) {
1451
1516
  count++;
1452
- node.match = true;
1517
+ node.match = count;
1453
1518
  node.visitParents((p) => {
1454
1519
  if (p !== node) {
1455
1520
  p.subMatchCount += 1;
@@ -1476,6 +1541,7 @@
1476
1541
  }
1477
1542
  // Redraw whole tree
1478
1543
  tree.logDebug(`Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.`);
1544
+ this._updatedConnectedControls();
1479
1545
  return count;
1480
1546
  }
1481
1547
  /**
@@ -1520,34 +1586,22 @@
1520
1586
  else {
1521
1587
  tree.logWarn("updateFilter(): no filter active.");
1522
1588
  }
1589
+ this._updatedConnectedControls();
1523
1590
  }
1524
1591
  /**
1525
1592
  * [ext-filter] Reset the filter.
1526
1593
  */
1527
1594
  clearFilter() {
1528
1595
  const tree = this.tree;
1529
- // statusNode = tree.root.findDirectChild(KEY_NODATA),
1530
- // escapeTitles = tree.options.escapeTitles;
1531
1596
  tree.enableUpdate(false);
1532
- // if (statusNode) {
1533
- // statusNode.remove();
1534
- // }
1535
1597
  tree.setStatus(NodeStatusType.ok);
1536
1598
  // we also counted root node's subMatchCount
1537
1599
  delete tree.root.match;
1538
1600
  delete tree.root.subMatchCount;
1539
1601
  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
1602
  delete node.match;
1545
1603
  delete node.subMatchCount;
1546
1604
  delete node.titleWithHighlight;
1547
- // if (node.subMatchBadge) {
1548
- // node.subMatchBadge.remove();
1549
- // delete node.subMatchBadge;
1550
- // }
1551
1605
  if (node._filterAutoExpanded && node.expanded) {
1552
1606
  node.setExpanded(false, {
1553
1607
  noAnimation: true,
@@ -1561,7 +1615,7 @@
1561
1615
  tree.element.classList.remove(
1562
1616
  // "wb-ext-filter",
1563
1617
  "wb-ext-filter-dim", "wb-ext-filter-hide");
1564
- // tree._callHook("treeStructureChanged", this, "clearFilter");
1618
+ this._updatedConnectedControls();
1565
1619
  tree.enableUpdate(true);
1566
1620
  }
1567
1621
  }
@@ -1605,7 +1659,7 @@
1605
1659
  /*!
1606
1660
  * Wunderbaum - ext-keynav
1607
1661
  * 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)
1662
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1609
1663
  */
1610
1664
  const QUICKSEARCH_DELAY = 500;
1611
1665
  class KeynavExtension extends WunderbaumExtension {
@@ -1969,7 +2023,7 @@
1969
2023
  /*!
1970
2024
  * Wunderbaum - ext-logger
1971
2025
  * 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)
2026
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1973
2027
  */
1974
2028
  class LoggerExtension extends WunderbaumExtension {
1975
2029
  constructor(tree) {
@@ -2011,7 +2065,7 @@
2011
2065
  /*!
2012
2066
  * Wunderbaum - ext-dnd
2013
2067
  * 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)
2068
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2015
2069
  */
2016
2070
  const nodeMimeType = "application/x-wunderbaum-node";
2017
2071
  class DndExtension extends WunderbaumExtension {
@@ -2461,7 +2515,7 @@
2461
2515
  /*!
2462
2516
  * Wunderbaum - drag_observer
2463
2517
  * 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)
2518
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2465
2519
  */
2466
2520
  /**
2467
2521
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2610,7 +2664,7 @@
2610
2664
  /*!
2611
2665
  * Wunderbaum - common
2612
2666
  * 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)
2667
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2614
2668
  */
2615
2669
  const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
2616
2670
  /**
@@ -2723,29 +2777,20 @@
2723
2777
  // "Escape",
2724
2778
  // ]);
2725
2779
  /** Map `KeyEvent.key` to navigation action. */
2726
- const KEY_TO_ACTION_DICT = {
2727
- " ": "toggleSelect",
2728
- "+": "expand",
2729
- Add: "expand",
2780
+ const KEY_TO_NAVIGATION_MAP = {
2730
2781
  ArrowDown: "down",
2731
2782
  ArrowLeft: "left",
2732
2783
  ArrowRight: "right",
2733
2784
  ArrowUp: "up",
2734
2785
  Backspace: "parent",
2735
- "/": "collapseAll",
2736
- Divide: "collapseAll",
2737
2786
  End: "lastCol",
2738
2787
  Home: "firstCol",
2739
2788
  "Control+End": "last",
2740
2789
  "Control+Home": "first",
2741
2790
  "Meta+ArrowDown": "last", // macOs
2742
2791
  "Meta+ArrowUp": "first", // macOs
2743
- "*": "expandAll",
2744
- Multiply: "expandAll",
2745
2792
  PageDown: "pageDown",
2746
2793
  PageUp: "pageUp",
2747
- "-": "collapse",
2748
- Subtract: "collapse",
2749
2794
  };
2750
2795
  /** Return a callback that returns true if the node title matches the string
2751
2796
  * or regular expression.
@@ -2969,7 +3014,7 @@
2969
3014
  /*!
2970
3015
  * Wunderbaum - ext-grid
2971
3016
  * 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)
3017
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2973
3018
  */
2974
3019
  class GridExtension extends WunderbaumExtension {
2975
3020
  constructor(tree) {
@@ -3060,7 +3105,7 @@
3060
3105
  /*!
3061
3106
  * Wunderbaum - deferred
3062
3107
  * 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)
3108
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3064
3109
  */
3065
3110
  /**
3066
3111
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -3113,7 +3158,7 @@
3113
3158
  /*!
3114
3159
  * Wunderbaum - wunderbaum_node
3115
3160
  * 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)
3161
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3117
3162
  */
3118
3163
  /** WunderbaumNode properties that can be passed with source data.
3119
3164
  * (Any other source properties will be stored as `node.data.PROP`.)
@@ -3895,7 +3940,7 @@
3895
3940
  isParentOf(other) {
3896
3941
  return other && other.parent === this;
3897
3942
  }
3898
- /** (experimental) Return true if this node is partially loaded. */
3943
+ /** Return true if this node is partially loaded. @experimental */
3899
3944
  isPartload() {
3900
3945
  return !!this._partload;
3901
3946
  }
@@ -4353,10 +4398,11 @@
4353
4398
  * @param options
4354
4399
  */
4355
4400
  async navigate(where, options) {
4401
+ var _a;
4356
4402
  // Allow to pass 'ArrowLeft' instead of 'left'
4357
- where = KEY_TO_ACTION_DICT[where] || where;
4403
+ const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where);
4358
4404
  // Otherwise activate or focus the related node
4359
- const node = this.findRelatedNode(where);
4405
+ const node = this.findRelatedNode(navType);
4360
4406
  if (!node) {
4361
4407
  this.logWarn(`Could not find related node '${where}'.`);
4362
4408
  return Promise.resolve(this);
@@ -4453,86 +4499,17 @@
4453
4499
  renderColInfosById: renderColInfosById,
4454
4500
  };
4455
4501
  }
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;
4502
+ _createIcon(parentElem, replaceChild, showLoading) {
4503
+ const iconElem = this.tree._createNodeIcon(this, showLoading, true);
4504
+ if (iconElem) {
4505
+ if (replaceChild) {
4506
+ parentElem.replaceChild(iconElem, replaceChild);
4523
4507
  }
4524
4508
  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);
4509
+ parentElem.appendChild(iconElem);
4532
4510
  }
4533
4511
  }
4534
- // this.log("_createIcon: ", iconSpan);
4535
- return iconSpan;
4512
+ return iconElem;
4536
4513
  }
4537
4514
  /**
4538
4515
  * Create a whole new `<div class="wb-row">` element.
@@ -4587,7 +4564,7 @@
4587
4564
  }
4588
4565
  // Render the icon (show a 'loading' icon if we do not have an expander that
4589
4566
  // we would prefer).
4590
- const iconSpan = this._createIcon(tree.iconMap, nodeElem, null, !expanderSpan);
4567
+ const iconSpan = this._createIcon(nodeElem, null, !expanderSpan);
4591
4568
  if (iconSpan) {
4592
4569
  ofsTitlePx += ICON_WIDTH;
4593
4570
  }
@@ -4819,7 +4796,7 @@
4819
4796
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4820
4797
  const iconSpan = nodeElem.querySelector("i.wb-icon");
4821
4798
  if (iconSpan) {
4822
- this._createIcon(tree.iconMap, nodeElem, iconSpan, !expanderSpan);
4799
+ this._createIcon(nodeElem, iconSpan, !expanderSpan);
4823
4800
  }
4824
4801
  }
4825
4802
  // Adjust column width
@@ -5368,7 +5345,7 @@
5368
5345
  assert(data.statusNodeType, "Not a status node");
5369
5346
  assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node");
5370
5347
  statusNode = this.addNode(data, "prependChild");
5371
- statusNode.match = true;
5348
+ statusNode.match = -1; // Mark as 'match' to avoid hiding
5372
5349
  tree.update(ChangeType.structure);
5373
5350
  return statusNode;
5374
5351
  };
@@ -5660,7 +5637,7 @@
5660
5637
  /*!
5661
5638
  * Wunderbaum - ext-edit
5662
5639
  * 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)
5640
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
5664
5641
  */
5665
5642
  // const START_MARKER = "\uFFF7";
5666
5643
  class EditExtension extends WunderbaumExtension {
@@ -5979,7 +5956,7 @@
5979
5956
  newNode.setClass("wb-edit-new");
5980
5957
  this.relatedNode = node;
5981
5958
  // Don't filter new nodes:
5982
- newNode.match = true;
5959
+ newNode.match = -1;
5983
5960
  newNode.makeVisible({ noAnimation: true }).then(() => {
5984
5961
  this.startEditTitle(newNode);
5985
5962
  });
@@ -5995,8 +5972,8 @@
5995
5972
  * https://github.com/mar10/wunderbaum
5996
5973
  *
5997
5974
  * Released under the MIT license.
5998
- * @version v0.12.1
5999
- * @date Sat, 22 Feb 2025 22:59:20 GMT
5975
+ * @version v0.13.0
5976
+ * @date Sat, 08 Mar 2025 14:16:31 GMT
6000
5977
  */
6001
5978
  // import "./wunderbaum.scss";
6002
5979
  class WbSystemRoot extends WunderbaumNode {
@@ -6057,6 +6034,9 @@
6057
6034
  // --- SELECT ---
6058
6035
  // /** @internal */
6059
6036
  // public selectRangeAnchor: WunderbaumNode | null = null;
6037
+ // --- BREADCRUMB ---
6038
+ /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6039
+ this.breadcrumb = null;
6060
6040
  // --- FILTER ---
6061
6041
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6062
6042
  this.filterMode = null;
@@ -6091,10 +6071,10 @@
6091
6071
  emptyChildListExpandable: false,
6092
6072
  // updateThrottleWait: 200,
6093
6073
  skeleton: false,
6094
- connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
6074
+ connectTopBreadcrumb: null,
6095
6075
  selectMode: "multi", // SelectModeType
6096
6076
  // --- KeyNav ---
6097
- navigationModeOption: null, // NavModeEnum.startRow,
6077
+ navigationModeOption: null, // NavModeEnum,
6098
6078
  quicksearch: true,
6099
6079
  // --- Events ---
6100
6080
  iconBadge: null,
@@ -6106,8 +6086,11 @@
6106
6086
  strings: {
6107
6087
  loadError: "Error",
6108
6088
  loading: "Loading...",
6109
- // loading: "Loading&hellip;",
6110
6089
  noData: "No data",
6090
+ breadcrumbDelimiter: " » ",
6091
+ queryResult: "Found ${matches} of ${count}",
6092
+ noMatch: "No results",
6093
+ matchIndex: "${match} of ${matches}",
6111
6094
  },
6112
6095
  }, options));
6113
6096
  const readyDeferred = new Deferred();
@@ -6213,6 +6196,19 @@
6213
6196
  this.headerElement =
6214
6197
  this.element.querySelector("div.wb-header");
6215
6198
  this.element.classList.toggle("wb-grid", this.columns.length > 1);
6199
+ if (this.options.connectTopBreadcrumb) {
6200
+ this.breadcrumb = elemFromSelector(this.options.connectTopBreadcrumb);
6201
+ assert(!this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`);
6202
+ this.breadcrumb.addEventListener("click", (e) => {
6203
+ // const node = Wunderbaum.getNode(e)!;
6204
+ const elem = e.target;
6205
+ if (elem && elem.matches("a.wb-breadcrumb")) {
6206
+ const node = this.keyMap.get(elem.dataset.key);
6207
+ node === null || node === void 0 ? void 0 : node.setActive();
6208
+ e.preventDefault();
6209
+ }
6210
+ });
6211
+ }
6216
6212
  this._initExtensions();
6217
6213
  // --- apply initial options
6218
6214
  ["enabled", "fixedCol"].forEach((optName) => {
@@ -6576,7 +6572,10 @@
6576
6572
  });
6577
6573
  return node;
6578
6574
  }
6579
- /** Return the topmost visible node in the viewport. */
6575
+ /** Return the topmost visible node in the viewport.
6576
+ * @param complete If `false`, the node is considered visible if at least one
6577
+ * pixel is visible.
6578
+ */
6580
6579
  getTopmostVpNode(complete = true) {
6581
6580
  const rowHeight = this.options.rowHeightPx;
6582
6581
  const gracePx = 1; // ignore subpixel scrolling
@@ -6609,24 +6608,19 @@
6609
6608
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6610
6609
  return this._getNodeByRowIdx(bottomIdx);
6611
6610
  }
6612
- /** Return preceeding visible node in the viewport. */
6613
- _getPrevNodeInView(node, ofs = 1) {
6611
+ /** Return following visible node in the viewport. */
6612
+ _getNextNodeInView(node, options) {
6613
+ let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
6614
+ const reverse = !!(options === null || options === void 0 ? void 0 : options.reverse);
6614
6615
  this.visitRows((n) => {
6615
6616
  node = n;
6616
- if (ofs-- <= 0) {
6617
+ if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) {
6617
6618
  return false;
6618
6619
  }
6619
- }, { reverse: true, start: node || this.getActiveNode() });
6620
- return node;
6621
- }
6622
- /** Return following visible node in the viewport. */
6623
- _getNextNodeInView(node, ofs = 1) {
6624
- this.visitRows((n) => {
6625
- node = n;
6626
6620
  if (ofs-- <= 0) {
6627
6621
  return false;
6628
6622
  }
6629
- }, { reverse: false, start: node || this.getActiveNode() });
6623
+ }, { reverse: reverse, start: node || this.getActiveNode() });
6630
6624
  return node;
6631
6625
  }
6632
6626
  /**
@@ -6746,9 +6740,11 @@
6746
6740
  case "first":
6747
6741
  case "last":
6748
6742
  case "left":
6743
+ case "nextMatch":
6749
6744
  case "pageDown":
6750
6745
  case "pageUp":
6751
6746
  case "parent":
6747
+ case "prevMatch":
6752
6748
  case "right":
6753
6749
  case "up":
6754
6750
  return node.navigate(cmd);
@@ -6940,6 +6936,11 @@
6940
6936
  count(visible = false) {
6941
6937
  return visible ? this.treeRowCount : this.keyMap.size;
6942
6938
  }
6939
+ /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`.
6940
+ */
6941
+ countUnique() {
6942
+ return this.refKeyMap.size;
6943
+ }
6943
6944
  /** @internal sanity check. */
6944
6945
  _check() {
6945
6946
  let i = 0;
@@ -6998,12 +6999,14 @@
6998
6999
  * and wrap-around at the end.
6999
7000
  * Used by quicksearch and keyboard navigation.
7000
7001
  */
7001
- findNextNode(match, startNode) {
7002
+ findNextNode(match, startNode, reverse = false) {
7002
7003
  //, visibleOnly) {
7003
7004
  let res = null;
7004
7005
  const firstNode = this.getFirstChild();
7006
+ // Last visible node (calculation is expensive, so do only if we need it):
7007
+ const lastNode = reverse ? this.findRelatedNode(firstNode, "last") : null;
7005
7008
  const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
7006
- startNode = startNode || firstNode;
7009
+ startNode = startNode || (reverse ? lastNode : firstNode);
7007
7010
  function _checkNode(n) {
7008
7011
  // console.log("_check " + n)
7009
7012
  if (matcher(n)) {
@@ -7016,12 +7019,14 @@
7016
7019
  this.visitRows(_checkNode, {
7017
7020
  start: startNode,
7018
7021
  includeSelf: false,
7022
+ reverse: reverse,
7019
7023
  });
7020
7024
  // Wrap around search
7021
7025
  if (!res && startNode !== firstNode) {
7022
7026
  this.visitRows(_checkNode, {
7023
- start: firstNode,
7027
+ start: reverse ? lastNode : firstNode,
7024
7028
  includeSelf: true,
7029
+ reverse: reverse,
7025
7030
  });
7026
7031
  }
7027
7032
  return res;
@@ -7088,7 +7093,7 @@
7088
7093
  // }
7089
7094
  break;
7090
7095
  case "up":
7091
- res = this._getPrevNodeInView(node);
7096
+ res = this._getNextNodeInView(node, { reverse: true });
7092
7097
  break;
7093
7098
  case "down":
7094
7099
  res = this._getNextNodeInView(node);
@@ -7101,7 +7106,10 @@
7101
7106
  res = bottomNode;
7102
7107
  }
7103
7108
  else {
7104
- res = this._getNextNodeInView(node, pageSize);
7109
+ res = this._getNextNodeInView(node, {
7110
+ reverse: false,
7111
+ ofs: pageSize,
7112
+ });
7105
7113
  }
7106
7114
  }
7107
7115
  break;
@@ -7116,10 +7124,23 @@
7116
7124
  res = topNode;
7117
7125
  }
7118
7126
  else {
7119
- res = this._getPrevNodeInView(node, pageSize);
7127
+ res = this._getNextNodeInView(node, {
7128
+ reverse: true,
7129
+ ofs: pageSize,
7130
+ });
7120
7131
  }
7121
7132
  }
7122
7133
  break;
7134
+ case "prevMatch":
7135
+ // fallthrough
7136
+ case "nextMatch":
7137
+ if (!this.isFilterActive) {
7138
+ this.logWarn(`${where}: Filter is not active.`);
7139
+ break;
7140
+ }
7141
+ res = this.findNextNode((n) => n.isMatched(), node, where === "prevMatch");
7142
+ res === null || res === void 0 ? void 0 : res.setActive();
7143
+ break;
7123
7144
  default:
7124
7145
  this.logWarn("Unknown relation '" + where + "'.");
7125
7146
  }
@@ -7181,6 +7202,12 @@
7181
7202
  getFirstChild() {
7182
7203
  return this.root.getFirstChild();
7183
7204
  }
7205
+ /**
7206
+ * Return the last top level node if any (not the invisible root node).
7207
+ */
7208
+ getLastChild() {
7209
+ return this.root.getLastChild();
7210
+ }
7184
7211
  /**
7185
7212
  * Return the node that currently has keyboard focus or null.
7186
7213
  * Alias for {@link Wunderbaum.focusNode}.
@@ -7508,6 +7535,51 @@
7508
7535
  _setFocusNode(node) {
7509
7536
  this._focusNode = node;
7510
7537
  }
7538
+ /** Return the current selection/expansion/activation status. @experimental */
7539
+ getState(options) {
7540
+ var _a, _b;
7541
+ let expandedKeys = undefined;
7542
+ if (options.expandedKeys !== false) {
7543
+ expandedKeys = [];
7544
+ for (const node of this) {
7545
+ if (node.expanded) {
7546
+ expandedKeys.push(node.key);
7547
+ }
7548
+ }
7549
+ }
7550
+ const state = {
7551
+ activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
7552
+ activeColIdx: this.activeColIdx,
7553
+ selectedKeys: options.selectedKeys === false
7554
+ ? undefined
7555
+ : this.getSelectedNodes().flatMap((n) => n.key),
7556
+ expandedKeys: expandedKeys,
7557
+ };
7558
+ return state;
7559
+ }
7560
+ /** Apply selection/expansion/activation status. @experimental */
7561
+ setState(state, options) {
7562
+ this.runWithDeferredUpdate(() => {
7563
+ var _a, _b;
7564
+ if (state.selectedKeys) {
7565
+ this.selectAll(false);
7566
+ for (const key of state.selectedKeys) {
7567
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setSelected(true);
7568
+ }
7569
+ }
7570
+ if (state.expandedKeys) {
7571
+ for (const key of state.expandedKeys) {
7572
+ (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setExpanded(true);
7573
+ }
7574
+ }
7575
+ if (state.activeKey) {
7576
+ this.setActiveNode(state.activeKey);
7577
+ }
7578
+ if (state.activeColIdx != null) {
7579
+ this.setColumn(state.activeColIdx);
7580
+ }
7581
+ });
7582
+ }
7511
7583
  update(change, node, options) {
7512
7584
  // this.log(`update(${change}) node=${node}`);
7513
7585
  if (!(node instanceof WunderbaumNode)) {
@@ -7789,11 +7861,11 @@
7789
7861
  // }
7790
7862
  return modified;
7791
7863
  }
7792
- _insertIcon(icon, elem) {
7793
- const iconElem = document.createElement("i");
7794
- iconElem.className = icon;
7795
- elem.appendChild(iconElem);
7796
- }
7864
+ // protected _insertIcon(icon: string, elem: HTMLElement) {
7865
+ // const iconElem = document.createElement("i");
7866
+ // iconElem.className = icon;
7867
+ // elem.appendChild(iconElem);
7868
+ // }
7797
7869
  /** Create/update header markup from `this.columns` definition.
7798
7870
  * @internal
7799
7871
  */
@@ -7891,6 +7963,104 @@
7891
7963
  this._updateViewportImmediately();
7892
7964
  }
7893
7965
  }
7966
+ /** @internal */
7967
+ _createNodeIcon(node, showLoading, showBadge) {
7968
+ const iconMap = this.iconMap;
7969
+ let iconElem;
7970
+ let icon = node.getOption("icon");
7971
+ if (node._errorInfo) {
7972
+ icon = iconMap.error;
7973
+ }
7974
+ else if (node._isLoading && showLoading) {
7975
+ // Status nodes, or nodes without expander (< minExpandLevel) should
7976
+ // display the 'loading' status with the i.wb-icon span
7977
+ icon = iconMap.loading;
7978
+ }
7979
+ if (icon === false) {
7980
+ return null; // explicitly disabled: don't try default icons
7981
+ }
7982
+ if (typeof icon === "string") ;
7983
+ else if (node.statusNodeType) {
7984
+ icon = iconMap[node.statusNodeType];
7985
+ }
7986
+ else if (node.expanded) {
7987
+ icon = iconMap.folderOpen;
7988
+ }
7989
+ else if (node.children) {
7990
+ icon = iconMap.folder;
7991
+ }
7992
+ else if (node.lazy) {
7993
+ icon = iconMap.folderLazy;
7994
+ }
7995
+ else {
7996
+ icon = iconMap.doc;
7997
+ }
7998
+ if (!icon) {
7999
+ iconElem = document.createElement("i");
8000
+ iconElem.className = "wb-icon";
8001
+ }
8002
+ else if (icon.indexOf("<") >= 0) {
8003
+ // HTML
8004
+ iconElem = elemFromHtml(icon);
8005
+ }
8006
+ else if (TEST_IMG.test(icon)) {
8007
+ // Image URL
8008
+ iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
8009
+ }
8010
+ else {
8011
+ // Class name
8012
+ iconElem = document.createElement("i");
8013
+ iconElem.className = "wb-icon " + icon;
8014
+ }
8015
+ // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
8016
+ const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem });
8017
+ let badge = null;
8018
+ if (cbRes != null && cbRes !== false) {
8019
+ let classes = "";
8020
+ let tooltip = "";
8021
+ if (isPlainObject(cbRes)) {
8022
+ badge = "" + cbRes.badge;
8023
+ classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
8024
+ tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
8025
+ }
8026
+ else if (typeof cbRes === "number") {
8027
+ badge = "" + cbRes;
8028
+ }
8029
+ else {
8030
+ badge = cbRes; // string or HTMLSpanElement
8031
+ }
8032
+ if (typeof badge === "string") {
8033
+ badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
8034
+ }
8035
+ if (badge) {
8036
+ iconElem.append(badge);
8037
+ }
8038
+ }
8039
+ return iconElem;
8040
+ }
8041
+ _updateTopBreadcrumb() {
8042
+ const breadcrumb = this.breadcrumb;
8043
+ const topmost = this.getTopmostVpNode(true);
8044
+ const parentList = topmost === null || topmost === void 0 ? void 0 : topmost.getParentList(false, false);
8045
+ if (parentList === null || parentList === void 0 ? void 0 : parentList.length) {
8046
+ breadcrumb.innerHTML = "";
8047
+ for (const n of topmost.getParentList(false, false)) {
8048
+ const icon = this._createNodeIcon(n, false, false);
8049
+ if (icon) {
8050
+ breadcrumb.append(icon, " ");
8051
+ }
8052
+ const part = document.createElement("a");
8053
+ part.textContent = n.title;
8054
+ part.href = "#";
8055
+ part.classList.add("wb-breadcrumb");
8056
+ part.dataset.key = n.key;
8057
+ breadcrumb.append(part, this.options.strings.breadcrumbDelimiter);
8058
+ }
8059
+ }
8060
+ else {
8061
+ breadcrumb.innerHTML = "&nbsp;";
8062
+ }
8063
+ }
7894
8064
  /**
7895
8065
  * This is the actual update method, which is wrapped inside a throttle method.
7896
8066
  * It calls `updateColumns()` and `_updateRows()`.
@@ -7901,7 +8071,6 @@
7901
8071
  * @internal
7902
8072
  */
7903
8073
  _updateViewportImmediately() {
7904
- var _a;
7905
8074
  if (this._disableUpdateCount) {
7906
8075
  this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`);
7907
8076
  this._disableUpdateIgnoreCount++;
@@ -7948,11 +8117,8 @@
7948
8117
  this._updateRows();
7949
8118
  // console.profileEnd(`_updateViewportImmediately()`)
7950
8119
  }
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;
8120
+ if (this.breadcrumb) {
8121
+ this._updateTopBreadcrumb();
7956
8122
  }
7957
8123
  this._callEvent("update");
7958
8124
  }
@@ -8321,7 +8487,7 @@
8321
8487
  }
8322
8488
  Wunderbaum.sequence = 0;
8323
8489
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8324
- Wunderbaum.version = "v0.12.1"; // Set to semver by 'grunt release'
8490
+ Wunderbaum.version = "v0.13.0"; // Set to semver by 'grunt release'
8325
8491
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8326
8492
  Wunderbaum.util = util;
8327
8493