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.
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Wunderbaum - debounce.ts
3
3
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
4
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
4
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
  /*
7
7
  * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21
@@ -293,7 +293,7 @@ function throttle(func, wait = 0, options = {}) {
293
293
  /*!
294
294
  * Wunderbaum - util
295
295
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
296
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
296
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
297
297
  */
298
298
  /** @module util */
299
299
  /** Readable names for `MouseEvent.button` */
@@ -676,18 +676,6 @@ function elemFromSelector(obj) {
676
676
  }
677
677
  return obj;
678
678
  }
679
- // /** Return a EventTarget from selector or cast an existing element. */
680
- // export function eventTargetFromSelector(
681
- // obj: string | EventTarget
682
- // ): EventTarget | null {
683
- // if (!obj) {
684
- // return null;
685
- // }
686
- // if (typeof obj === "string") {
687
- // return document.querySelector(obj) as EventTarget;
688
- // }
689
- // return obj as EventTarget;
690
- // }
691
679
  /**
692
680
  * Return a canonical descriptive string for a keyboard or mouse event.
693
681
  *
@@ -1147,7 +1135,7 @@ var util = /*#__PURE__*/Object.freeze({
1147
1135
  /*!
1148
1136
  * Wunderbaum - types
1149
1137
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1150
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1138
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1151
1139
  */
1152
1140
  /**
1153
1141
  * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
@@ -1215,7 +1203,7 @@ var NavModeEnum;
1215
1203
  /*!
1216
1204
  * Wunderbaum - wb_extension_base
1217
1205
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1218
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1206
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1219
1207
  */
1220
1208
  class WunderbaumExtension {
1221
1209
  constructor(tree, id, defaults) {
@@ -1274,7 +1262,7 @@ class WunderbaumExtension {
1274
1262
  /*!
1275
1263
  * Wunderbaum - ext-filter
1276
1264
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1277
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1265
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1278
1266
  */
1279
1267
  const START_MARKER = "\uFFF7";
1280
1268
  const END_MARKER = "\uFFF8";
@@ -1286,7 +1274,7 @@ class FilterExtension extends WunderbaumExtension {
1286
1274
  autoApply: true, // Re-apply last filter if lazy data is loaded
1287
1275
  autoExpand: false, // Expand all branches that contain matches while filtered
1288
1276
  matchBranch: false, // Whether to implicitly match all children of matched nodes
1289
- connectInput: null, // Element or selector of an input control for filter query strings
1277
+ connect: null, // Element or selector of an input control for filter query strings
1290
1278
  fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
1291
1279
  hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
1292
1280
  highlight: true, // Highlight matches by wrapping inside <mark> tags
@@ -1294,36 +1282,117 @@ class FilterExtension extends WunderbaumExtension {
1294
1282
  mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
1295
1283
  noData: true, // Display a 'no data' status node if result is empty
1296
1284
  });
1285
+ this.queryInput = null;
1286
+ this.prevButton = null;
1287
+ this.nextButton = null;
1288
+ this.modeButton = null;
1289
+ this.matchInfoElem = null;
1297
1290
  this.lastFilterArgs = null;
1298
1291
  }
1299
1292
  init() {
1300
1293
  super.init();
1301
- const connectInput = this.getPluginOption("connectInput");
1302
- if (connectInput) {
1303
- this.queryInput = elemFromSelector(connectInput);
1304
- assert(this.queryInput, `Invalid 'filter.connectInput' option: ${connectInput}.`);
1305
- onEvent(this.queryInput, "input", debounce((e) => {
1306
- // this.tree.log("query", e);
1307
- this.filterNodes(this.queryInput.value.trim(), {});
1308
- }, 700));
1294
+ const connect = this.getPluginOption("connect");
1295
+ if (connect) {
1296
+ this._connectControls();
1309
1297
  }
1310
1298
  }
1311
1299
  setPluginOption(name, value) {
1312
- // alert("filter opt=" + name + ", " + value)
1313
1300
  super.setPluginOption(name, value);
1314
1301
  switch (name) {
1315
1302
  case "mode":
1316
- this.tree.filterMode = value === "hide" ? "hide" : "dim";
1303
+ this.tree.filterMode =
1304
+ value === "hide" ? "hide" : value === "mark" ? "mark" : "dim";
1317
1305
  this.tree.updateFilter();
1318
1306
  break;
1319
1307
  }
1320
1308
  }
1309
+ _updatedConnectedControls() {
1310
+ var _a;
1311
+ const filterActive = this.tree.filterMode !== null;
1312
+ const activeNode = this.tree.getActiveNode();
1313
+ const matchCount = filterActive ? this.countMatches() : 0;
1314
+ const strings = this.treeOpts.strings;
1315
+ let matchIdx = "?";
1316
+ if (this.matchInfoElem) {
1317
+ if (filterActive) {
1318
+ let info;
1319
+ if (matchCount === 0) {
1320
+ info = strings.noMatch;
1321
+ }
1322
+ else if (activeNode && activeNode.match >= 1) {
1323
+ matchIdx = (_a = activeNode.match) !== null && _a !== void 0 ? _a : "?";
1324
+ info = strings.matchIndex;
1325
+ }
1326
+ else {
1327
+ info = strings.queryResult;
1328
+ }
1329
+ info = info
1330
+ .replace("${count}", this.tree.count().toLocaleString())
1331
+ .replace("${match}", "" + matchIdx)
1332
+ .replace("${matches}", matchCount.toLocaleString());
1333
+ this.matchInfoElem.textContent = info;
1334
+ }
1335
+ else {
1336
+ this.matchInfoElem.textContent = "";
1337
+ }
1338
+ }
1339
+ if (this.nextButton instanceof HTMLButtonElement) {
1340
+ this.nextButton.disabled = !matchCount;
1341
+ }
1342
+ if (this.prevButton instanceof HTMLButtonElement) {
1343
+ this.prevButton.disabled = !matchCount;
1344
+ }
1345
+ if (this.modeButton) {
1346
+ this.modeButton.disabled = !filterActive;
1347
+ this.modeButton.classList.toggle("wb-filter-hide", this.tree.filterMode === "hide");
1348
+ }
1349
+ }
1350
+ _connectControls() {
1351
+ const tree = this.tree;
1352
+ const connect = this.getPluginOption("connect");
1353
+ if (!connect) {
1354
+ return;
1355
+ }
1356
+ this.queryInput = elemFromSelector(connect.inputElem);
1357
+ if (!this.queryInput) {
1358
+ throw new Error(`Invalid 'filter.connect' option: ${connect.inputElem}.`);
1359
+ }
1360
+ this.prevButton = elemFromSelector(connect.prevButton);
1361
+ this.nextButton = elemFromSelector(connect.nextButton);
1362
+ this.modeButton = elemFromSelector(connect.modeButton);
1363
+ this.matchInfoElem = elemFromSelector(connect.matchInfoElem);
1364
+ if (this.prevButton) {
1365
+ onEvent(this.prevButton, "click", () => {
1366
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "prevMatch");
1367
+ this._updatedConnectedControls();
1368
+ });
1369
+ }
1370
+ if (this.nextButton) {
1371
+ onEvent(this.nextButton, "click", () => {
1372
+ tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "nextMatch");
1373
+ this._updatedConnectedControls();
1374
+ });
1375
+ }
1376
+ if (this.modeButton) {
1377
+ onEvent(this.modeButton, "click", (e) => {
1378
+ if (!this.tree.filterMode) {
1379
+ return;
1380
+ }
1381
+ this.setPluginOption("mode", tree.filterMode === "dim" ? "hide" : "dim");
1382
+ });
1383
+ }
1384
+ onEvent(this.queryInput, "input", debounce((e) => {
1385
+ this.filterNodes(this.queryInput.value.trim(), {});
1386
+ }, 700));
1387
+ this._updatedConnectedControls();
1388
+ }
1321
1389
  _applyFilterNoUpdate(filter, _opts) {
1322
1390
  return this.tree.runWithDeferredUpdate(() => {
1323
1391
  return this._applyFilterImpl(filter, _opts);
1324
1392
  });
1325
1393
  }
1326
1394
  _applyFilterImpl(filter, _opts) {
1395
+ var _a;
1327
1396
  let //temp,
1328
1397
  count = 0;
1329
1398
  const start = Date.now();
@@ -1405,11 +1474,11 @@ class FilterExtension extends WunderbaumExtension {
1405
1474
  return !!res;
1406
1475
  };
1407
1476
  }
1408
- tree.filterMode = opts.mode;
1477
+ tree.filterMode = (_a = opts.mode) !== null && _a !== void 0 ? _a : "dim";
1409
1478
  // eslint-disable-next-line prefer-rest-params
1410
1479
  this.lastFilterArgs = arguments;
1411
1480
  tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
1412
- tree.element.classList.toggle("wb-ext-filter-dim", !hideMode);
1481
+ tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim");
1413
1482
  tree.element.classList.toggle("wb-ext-filter-hide-expanders", !!opts.hideExpanders);
1414
1483
  // Reset current filter
1415
1484
  tree.root.subMatchCount = 0;
@@ -1418,10 +1487,6 @@ class FilterExtension extends WunderbaumExtension {
1418
1487
  delete node.titleWithHighlight;
1419
1488
  node.subMatchCount = 0;
1420
1489
  });
1421
- // statusNode = tree.root.findDirectChild(KEY_NODATA);
1422
- // if (statusNode) {
1423
- // statusNode.remove();
1424
- // }
1425
1490
  tree.setStatus(NodeStatusType.ok);
1426
1491
  // Adjust node.hide, .match, and .subMatchCount properties
1427
1492
  treeOpts.autoCollapse = false; // #528
@@ -1432,7 +1497,7 @@ class FilterExtension extends WunderbaumExtension {
1432
1497
  let res = filter(node);
1433
1498
  if (res === "skip") {
1434
1499
  node.visit(function (c) {
1435
- c.match = false;
1500
+ c.match = undefined;
1436
1501
  }, true);
1437
1502
  return "skip";
1438
1503
  }
@@ -1443,7 +1508,7 @@ class FilterExtension extends WunderbaumExtension {
1443
1508
  }
1444
1509
  if (res) {
1445
1510
  count++;
1446
- node.match = true;
1511
+ node.match = count;
1447
1512
  node.visitParents((p) => {
1448
1513
  if (p !== node) {
1449
1514
  p.subMatchCount += 1;
@@ -1470,6 +1535,7 @@ class FilterExtension extends WunderbaumExtension {
1470
1535
  }
1471
1536
  // Redraw whole tree
1472
1537
  tree.logDebug(`Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.`);
1538
+ this._updatedConnectedControls();
1473
1539
  return count;
1474
1540
  }
1475
1541
  /**
@@ -1514,34 +1580,22 @@ class FilterExtension extends WunderbaumExtension {
1514
1580
  else {
1515
1581
  tree.logWarn("updateFilter(): no filter active.");
1516
1582
  }
1583
+ this._updatedConnectedControls();
1517
1584
  }
1518
1585
  /**
1519
1586
  * [ext-filter] Reset the filter.
1520
1587
  */
1521
1588
  clearFilter() {
1522
1589
  const tree = this.tree;
1523
- // statusNode = tree.root.findDirectChild(KEY_NODATA),
1524
- // escapeTitles = tree.options.escapeTitles;
1525
1590
  tree.enableUpdate(false);
1526
- // if (statusNode) {
1527
- // statusNode.remove();
1528
- // }
1529
1591
  tree.setStatus(NodeStatusType.ok);
1530
1592
  // we also counted root node's subMatchCount
1531
1593
  delete tree.root.match;
1532
1594
  delete tree.root.subMatchCount;
1533
1595
  tree.visit((node) => {
1534
- // if (node.match && node._rowElem) {
1535
- // let titleElem = node._rowElem.querySelector("span.wb-title")!;
1536
- // node._callEvent("enhanceTitle", { titleElem: titleElem });
1537
- // }
1538
1596
  delete node.match;
1539
1597
  delete node.subMatchCount;
1540
1598
  delete node.titleWithHighlight;
1541
- // if (node.subMatchBadge) {
1542
- // node.subMatchBadge.remove();
1543
- // delete node.subMatchBadge;
1544
- // }
1545
1599
  if (node._filterAutoExpanded && node.expanded) {
1546
1600
  node.setExpanded(false, {
1547
1601
  noAnimation: true,
@@ -1555,7 +1609,7 @@ class FilterExtension extends WunderbaumExtension {
1555
1609
  tree.element.classList.remove(
1556
1610
  // "wb-ext-filter",
1557
1611
  "wb-ext-filter-dim", "wb-ext-filter-hide");
1558
- // tree._callHook("treeStructureChanged", this, "clearFilter");
1612
+ this._updatedConnectedControls();
1559
1613
  tree.enableUpdate(true);
1560
1614
  }
1561
1615
  }
@@ -1599,7 +1653,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1599
1653
  /*!
1600
1654
  * Wunderbaum - ext-keynav
1601
1655
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1602
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
1656
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1603
1657
  */
1604
1658
  const QUICKSEARCH_DELAY = 500;
1605
1659
  class KeynavExtension extends WunderbaumExtension {
@@ -1963,7 +2017,7 @@ class KeynavExtension extends WunderbaumExtension {
1963
2017
  /*!
1964
2018
  * Wunderbaum - ext-logger
1965
2019
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1966
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2020
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1967
2021
  */
1968
2022
  class LoggerExtension extends WunderbaumExtension {
1969
2023
  constructor(tree) {
@@ -2005,7 +2059,7 @@ class LoggerExtension extends WunderbaumExtension {
2005
2059
  /*!
2006
2060
  * Wunderbaum - ext-dnd
2007
2061
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2008
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2062
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2009
2063
  */
2010
2064
  const nodeMimeType = "application/x-wunderbaum-node";
2011
2065
  class DndExtension extends WunderbaumExtension {
@@ -2455,7 +2509,7 @@ class DndExtension extends WunderbaumExtension {
2455
2509
  /*!
2456
2510
  * Wunderbaum - drag_observer
2457
2511
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2458
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2512
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2459
2513
  */
2460
2514
  /**
2461
2515
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2604,7 +2658,7 @@ class DragObserver {
2604
2658
  /*!
2605
2659
  * Wunderbaum - common
2606
2660
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2607
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
2661
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2608
2662
  */
2609
2663
  const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
2610
2664
  /**
@@ -2717,29 +2771,20 @@ const RESERVED_TREE_SOURCE_KEYS = new Set([
2717
2771
  // "Escape",
2718
2772
  // ]);
2719
2773
  /** Map `KeyEvent.key` to navigation action. */
2720
- const KEY_TO_ACTION_DICT = {
2721
- " ": "toggleSelect",
2722
- "+": "expand",
2723
- Add: "expand",
2774
+ const KEY_TO_NAVIGATION_MAP = {
2724
2775
  ArrowDown: "down",
2725
2776
  ArrowLeft: "left",
2726
2777
  ArrowRight: "right",
2727
2778
  ArrowUp: "up",
2728
2779
  Backspace: "parent",
2729
- "/": "collapseAll",
2730
- Divide: "collapseAll",
2731
2780
  End: "lastCol",
2732
2781
  Home: "firstCol",
2733
2782
  "Control+End": "last",
2734
2783
  "Control+Home": "first",
2735
2784
  "Meta+ArrowDown": "last", // macOs
2736
2785
  "Meta+ArrowUp": "first", // macOs
2737
- "*": "expandAll",
2738
- Multiply: "expandAll",
2739
2786
  PageDown: "pageDown",
2740
2787
  PageUp: "pageUp",
2741
- "-": "collapse",
2742
- Subtract: "collapse",
2743
2788
  };
2744
2789
  /** Return a callback that returns true if the node title matches the string
2745
2790
  * or regular expression.
@@ -2963,7 +3008,7 @@ function decompressSourceData(source) {
2963
3008
  /*!
2964
3009
  * Wunderbaum - ext-grid
2965
3010
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2966
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3011
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2967
3012
  */
2968
3013
  class GridExtension extends WunderbaumExtension {
2969
3014
  constructor(tree) {
@@ -3054,7 +3099,7 @@ class GridExtension extends WunderbaumExtension {
3054
3099
  /*!
3055
3100
  * Wunderbaum - deferred
3056
3101
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3057
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3102
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3058
3103
  */
3059
3104
  /**
3060
3105
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -3107,7 +3152,7 @@ class Deferred {
3107
3152
  /*!
3108
3153
  * Wunderbaum - wunderbaum_node
3109
3154
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3110
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
3155
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3111
3156
  */
3112
3157
  /** WunderbaumNode properties that can be passed with source data.
3113
3158
  * (Any other source properties will be stored as `node.data.PROP`.)
@@ -3889,7 +3934,7 @@ class WunderbaumNode {
3889
3934
  isParentOf(other) {
3890
3935
  return other && other.parent === this;
3891
3936
  }
3892
- /** (experimental) Return true if this node is partially loaded. */
3937
+ /** Return true if this node is partially loaded. @experimental */
3893
3938
  isPartload() {
3894
3939
  return !!this._partload;
3895
3940
  }
@@ -4347,10 +4392,11 @@ class WunderbaumNode {
4347
4392
  * @param options
4348
4393
  */
4349
4394
  async navigate(where, options) {
4395
+ var _a;
4350
4396
  // Allow to pass 'ArrowLeft' instead of 'left'
4351
- where = KEY_TO_ACTION_DICT[where] || where;
4397
+ const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where);
4352
4398
  // Otherwise activate or focus the related node
4353
- const node = this.findRelatedNode(where);
4399
+ const node = this.findRelatedNode(navType);
4354
4400
  if (!node) {
4355
4401
  this.logWarn(`Could not find related node '${where}'.`);
4356
4402
  return Promise.resolve(this);
@@ -4447,86 +4493,17 @@ class WunderbaumNode {
4447
4493
  renderColInfosById: renderColInfosById,
4448
4494
  };
4449
4495
  }
4450
- _createIcon(iconMap, parentElem, replaceChild, showLoading) {
4451
- let iconSpan;
4452
- let icon = this.getOption("icon");
4453
- if (this._errorInfo) {
4454
- icon = iconMap.error;
4455
- }
4456
- else if (this._isLoading && showLoading) {
4457
- // Status nodes, or nodes without expander (< minExpandLevel) should
4458
- // display the 'loading' status with the i.wb-icon span
4459
- icon = iconMap.loading;
4460
- }
4461
- if (icon === false) {
4462
- return null; // explicitly disabled: don't try default icons
4463
- }
4464
- if (typeof icon === "string") ;
4465
- else if (this.statusNodeType) {
4466
- icon = iconMap[this.statusNodeType];
4467
- }
4468
- else if (this.expanded) {
4469
- icon = iconMap.folderOpen;
4470
- }
4471
- else if (this.children) {
4472
- icon = iconMap.folder;
4473
- }
4474
- else if (this.lazy) {
4475
- icon = iconMap.folderLazy;
4476
- }
4477
- else {
4478
- icon = iconMap.doc;
4479
- }
4480
- // this.log("_createIcon: " + icon);
4481
- if (!icon) {
4482
- iconSpan = document.createElement("i");
4483
- iconSpan.className = "wb-icon";
4484
- }
4485
- else if (icon.indexOf("<") >= 0) {
4486
- // HTML
4487
- iconSpan = elemFromHtml(icon);
4488
- }
4489
- else if (TEST_IMG.test(icon)) {
4490
- // Image URL
4491
- iconSpan = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
4492
- }
4493
- else {
4494
- // Class name
4495
- iconSpan = document.createElement("i");
4496
- iconSpan.className = "wb-icon " + icon;
4497
- }
4498
- if (replaceChild) {
4499
- parentElem.replaceChild(iconSpan, replaceChild);
4500
- }
4501
- else {
4502
- parentElem.appendChild(iconSpan);
4503
- }
4504
- // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
4505
- const cbRes = this._callEvent("iconBadge", { iconSpan: iconSpan });
4506
- let badge = null;
4507
- if (cbRes != null && cbRes !== false) {
4508
- let classes = "";
4509
- let tooltip = "";
4510
- if (isPlainObject(cbRes)) {
4511
- badge = "" + cbRes.badge;
4512
- classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
4513
- tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
4514
- }
4515
- else if (typeof cbRes === "number") {
4516
- badge = "" + cbRes;
4496
+ _createIcon(parentElem, replaceChild, showLoading) {
4497
+ const iconElem = this.tree._createNodeIcon(this, showLoading, true);
4498
+ if (iconElem) {
4499
+ if (replaceChild) {
4500
+ parentElem.replaceChild(iconElem, replaceChild);
4517
4501
  }
4518
4502
  else {
4519
- badge = cbRes; // string or HTMLSpanElement
4520
- }
4521
- if (typeof badge === "string") {
4522
- badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
4523
- }
4524
- if (badge) {
4525
- iconSpan.append(badge);
4503
+ parentElem.appendChild(iconElem);
4526
4504
  }
4527
4505
  }
4528
- // this.log("_createIcon: ", iconSpan);
4529
- return iconSpan;
4506
+ return iconElem;
4530
4507
  }
4531
4508
  /**
4532
4509
  * Create a whole new `<div class="wb-row">` element.
@@ -4581,7 +4558,7 @@ class WunderbaumNode {
4581
4558
  }
4582
4559
  // Render the icon (show a 'loading' icon if we do not have an expander that
4583
4560
  // we would prefer).
4584
- const iconSpan = this._createIcon(tree.iconMap, nodeElem, null, !expanderSpan);
4561
+ const iconSpan = this._createIcon(nodeElem, null, !expanderSpan);
4585
4562
  if (iconSpan) {
4586
4563
  ofsTitlePx += ICON_WIDTH;
4587
4564
  }
@@ -4813,7 +4790,7 @@ class WunderbaumNode {
4813
4790
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4814
4791
  const iconSpan = nodeElem.querySelector("i.wb-icon");
4815
4792
  if (iconSpan) {
4816
- this._createIcon(tree.iconMap, nodeElem, iconSpan, !expanderSpan);
4793
+ this._createIcon(nodeElem, iconSpan, !expanderSpan);
4817
4794
  }
4818
4795
  }
4819
4796
  // Adjust column width
@@ -5362,7 +5339,7 @@ class WunderbaumNode {
5362
5339
  assert(data.statusNodeType, "Not a status node");
5363
5340
  assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node");
5364
5341
  statusNode = this.addNode(data, "prependChild");
5365
- statusNode.match = true;
5342
+ statusNode.match = -1; // Mark as 'match' to avoid hiding
5366
5343
  tree.update(ChangeType.structure);
5367
5344
  return statusNode;
5368
5345
  };
@@ -5654,7 +5631,7 @@ WunderbaumNode.sequence = 0;
5654
5631
  /*!
5655
5632
  * Wunderbaum - ext-edit
5656
5633
  * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
5657
- * v0.12.1, Sat, 22 Feb 2025 22:59:20 GMT (https://github.com/mar10/wunderbaum)
5634
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
5658
5635
  */
5659
5636
  // const START_MARKER = "\uFFF7";
5660
5637
  class EditExtension extends WunderbaumExtension {
@@ -5973,7 +5950,7 @@ class EditExtension extends WunderbaumExtension {
5973
5950
  newNode.setClass("wb-edit-new");
5974
5951
  this.relatedNode = node;
5975
5952
  // Don't filter new nodes:
5976
- newNode.match = true;
5953
+ newNode.match = -1;
5977
5954
  newNode.makeVisible({ noAnimation: true }).then(() => {
5978
5955
  this.startEditTitle(newNode);
5979
5956
  });
@@ -5989,8 +5966,8 @@ class EditExtension extends WunderbaumExtension {
5989
5966
  * https://github.com/mar10/wunderbaum
5990
5967
  *
5991
5968
  * Released under the MIT license.
5992
- * @version v0.12.1
5993
- * @date Sat, 22 Feb 2025 22:59:20 GMT
5969
+ * @version v0.13.0
5970
+ * @date Sat, 08 Mar 2025 14:16:31 GMT
5994
5971
  */
5995
5972
  // import "./wunderbaum.scss";
5996
5973
  class WbSystemRoot extends WunderbaumNode {
@@ -6051,6 +6028,9 @@ class Wunderbaum {
6051
6028
  // --- SELECT ---
6052
6029
  // /** @internal */
6053
6030
  // public selectRangeAnchor: WunderbaumNode | null = null;
6031
+ // --- BREADCRUMB ---
6032
+ /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6033
+ this.breadcrumb = null;
6054
6034
  // --- FILTER ---
6055
6035
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6056
6036
  this.filterMode = null;
@@ -6085,10 +6065,10 @@ class Wunderbaum {
6085
6065
  emptyChildListExpandable: false,
6086
6066
  // updateThrottleWait: 200,
6087
6067
  skeleton: false,
6088
- connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
6068
+ connectTopBreadcrumb: null,
6089
6069
  selectMode: "multi", // SelectModeType
6090
6070
  // --- KeyNav ---
6091
- navigationModeOption: null, // NavModeEnum.startRow,
6071
+ navigationModeOption: null, // NavModeEnum,
6092
6072
  quicksearch: true,
6093
6073
  // --- Events ---
6094
6074
  iconBadge: null,
@@ -6100,8 +6080,11 @@ class Wunderbaum {
6100
6080
  strings: {
6101
6081
  loadError: "Error",
6102
6082
  loading: "Loading...",
6103
- // loading: "Loading&hellip;",
6104
6083
  noData: "No data",
6084
+ breadcrumbDelimiter: " » ",
6085
+ queryResult: "Found ${matches} of ${count}",
6086
+ noMatch: "No results",
6087
+ matchIndex: "${match} of ${matches}",
6105
6088
  },
6106
6089
  }, options));
6107
6090
  const readyDeferred = new Deferred();
@@ -6207,6 +6190,19 @@ class Wunderbaum {
6207
6190
  this.headerElement =
6208
6191
  this.element.querySelector("div.wb-header");
6209
6192
  this.element.classList.toggle("wb-grid", this.columns.length > 1);
6193
+ if (this.options.connectTopBreadcrumb) {
6194
+ this.breadcrumb = elemFromSelector(this.options.connectTopBreadcrumb);
6195
+ assert(!this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`);
6196
+ this.breadcrumb.addEventListener("click", (e) => {
6197
+ // const node = Wunderbaum.getNode(e)!;
6198
+ const elem = e.target;
6199
+ if (elem && elem.matches("a.wb-breadcrumb")) {
6200
+ const node = this.keyMap.get(elem.dataset.key);
6201
+ node === null || node === void 0 ? void 0 : node.setActive();
6202
+ e.preventDefault();
6203
+ }
6204
+ });
6205
+ }
6210
6206
  this._initExtensions();
6211
6207
  // --- apply initial options
6212
6208
  ["enabled", "fixedCol"].forEach((optName) => {
@@ -6570,7 +6566,10 @@ class Wunderbaum {
6570
6566
  });
6571
6567
  return node;
6572
6568
  }
6573
- /** Return the topmost visible node in the viewport. */
6569
+ /** Return the topmost visible node in the viewport.
6570
+ * @param complete If `false`, the node is considered visible if at least one
6571
+ * pixel is visible.
6572
+ */
6574
6573
  getTopmostVpNode(complete = true) {
6575
6574
  const rowHeight = this.options.rowHeightPx;
6576
6575
  const gracePx = 1; // ignore subpixel scrolling
@@ -6603,24 +6602,19 @@ class Wunderbaum {
6603
6602
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6604
6603
  return this._getNodeByRowIdx(bottomIdx);
6605
6604
  }
6606
- /** Return preceeding visible node in the viewport. */
6607
- _getPrevNodeInView(node, ofs = 1) {
6605
+ /** Return following visible node in the viewport. */
6606
+ _getNextNodeInView(node, options) {
6607
+ let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
6608
+ const reverse = !!(options === null || options === void 0 ? void 0 : options.reverse);
6608
6609
  this.visitRows((n) => {
6609
6610
  node = n;
6610
- if (ofs-- <= 0) {
6611
+ if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) {
6611
6612
  return false;
6612
6613
  }
6613
- }, { reverse: true, start: node || this.getActiveNode() });
6614
- return node;
6615
- }
6616
- /** Return following visible node in the viewport. */
6617
- _getNextNodeInView(node, ofs = 1) {
6618
- this.visitRows((n) => {
6619
- node = n;
6620
6614
  if (ofs-- <= 0) {
6621
6615
  return false;
6622
6616
  }
6623
- }, { reverse: false, start: node || this.getActiveNode() });
6617
+ }, { reverse: reverse, start: node || this.getActiveNode() });
6624
6618
  return node;
6625
6619
  }
6626
6620
  /**
@@ -6740,9 +6734,11 @@ class Wunderbaum {
6740
6734
  case "first":
6741
6735
  case "last":
6742
6736
  case "left":
6737
+ case "nextMatch":
6743
6738
  case "pageDown":
6744
6739
  case "pageUp":
6745
6740
  case "parent":
6741
+ case "prevMatch":
6746
6742
  case "right":
6747
6743
  case "up":
6748
6744
  return node.navigate(cmd);
@@ -6934,6 +6930,11 @@ class Wunderbaum {
6934
6930
  count(visible = false) {
6935
6931
  return visible ? this.treeRowCount : this.keyMap.size;
6936
6932
  }
6933
+ /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`.
6934
+ */
6935
+ countUnique() {
6936
+ return this.refKeyMap.size;
6937
+ }
6937
6938
  /** @internal sanity check. */
6938
6939
  _check() {
6939
6940
  let i = 0;
@@ -6992,12 +6993,14 @@ class Wunderbaum {
6992
6993
  * and wrap-around at the end.
6993
6994
  * Used by quicksearch and keyboard navigation.
6994
6995
  */
6995
- findNextNode(match, startNode) {
6996
+ findNextNode(match, startNode, reverse = false) {
6996
6997
  //, visibleOnly) {
6997
6998
  let res = null;
6998
6999
  const firstNode = this.getFirstChild();
7000
+ // Last visible node (calculation is expensive, so do only if we need it):
7001
+ const lastNode = reverse ? this.findRelatedNode(firstNode, "last") : null;
6999
7002
  const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
7000
- startNode = startNode || firstNode;
7003
+ startNode = startNode || (reverse ? lastNode : firstNode);
7001
7004
  function _checkNode(n) {
7002
7005
  // console.log("_check " + n)
7003
7006
  if (matcher(n)) {
@@ -7010,12 +7013,14 @@ class Wunderbaum {
7010
7013
  this.visitRows(_checkNode, {
7011
7014
  start: startNode,
7012
7015
  includeSelf: false,
7016
+ reverse: reverse,
7013
7017
  });
7014
7018
  // Wrap around search
7015
7019
  if (!res && startNode !== firstNode) {
7016
7020
  this.visitRows(_checkNode, {
7017
- start: firstNode,
7021
+ start: reverse ? lastNode : firstNode,
7018
7022
  includeSelf: true,
7023
+ reverse: reverse,
7019
7024
  });
7020
7025
  }
7021
7026
  return res;
@@ -7082,7 +7087,7 @@ class Wunderbaum {
7082
7087
  // }
7083
7088
  break;
7084
7089
  case "up":
7085
- res = this._getPrevNodeInView(node);
7090
+ res = this._getNextNodeInView(node, { reverse: true });
7086
7091
  break;
7087
7092
  case "down":
7088
7093
  res = this._getNextNodeInView(node);
@@ -7095,7 +7100,10 @@ class Wunderbaum {
7095
7100
  res = bottomNode;
7096
7101
  }
7097
7102
  else {
7098
- res = this._getNextNodeInView(node, pageSize);
7103
+ res = this._getNextNodeInView(node, {
7104
+ reverse: false,
7105
+ ofs: pageSize,
7106
+ });
7099
7107
  }
7100
7108
  }
7101
7109
  break;
@@ -7110,10 +7118,23 @@ class Wunderbaum {
7110
7118
  res = topNode;
7111
7119
  }
7112
7120
  else {
7113
- res = this._getPrevNodeInView(node, pageSize);
7121
+ res = this._getNextNodeInView(node, {
7122
+ reverse: true,
7123
+ ofs: pageSize,
7124
+ });
7114
7125
  }
7115
7126
  }
7116
7127
  break;
7128
+ case "prevMatch":
7129
+ // fallthrough
7130
+ case "nextMatch":
7131
+ if (!this.isFilterActive) {
7132
+ this.logWarn(`${where}: Filter is not active.`);
7133
+ break;
7134
+ }
7135
+ res = this.findNextNode((n) => n.isMatched(), node, where === "prevMatch");
7136
+ res === null || res === void 0 ? void 0 : res.setActive();
7137
+ break;
7117
7138
  default:
7118
7139
  this.logWarn("Unknown relation '" + where + "'.");
7119
7140
  }
@@ -7175,6 +7196,12 @@ class Wunderbaum {
7175
7196
  getFirstChild() {
7176
7197
  return this.root.getFirstChild();
7177
7198
  }
7199
+ /**
7200
+ * Return the last top level node if any (not the invisible root node).
7201
+ */
7202
+ getLastChild() {
7203
+ return this.root.getLastChild();
7204
+ }
7178
7205
  /**
7179
7206
  * Return the node that currently has keyboard focus or null.
7180
7207
  * Alias for {@link Wunderbaum.focusNode}.
@@ -7502,6 +7529,51 @@ class Wunderbaum {
7502
7529
  _setFocusNode(node) {
7503
7530
  this._focusNode = node;
7504
7531
  }
7532
+ /** Return the current selection/expansion/activation status. @experimental */
7533
+ getState(options) {
7534
+ var _a, _b;
7535
+ let expandedKeys = undefined;
7536
+ if (options.expandedKeys !== false) {
7537
+ expandedKeys = [];
7538
+ for (const node of this) {
7539
+ if (node.expanded) {
7540
+ expandedKeys.push(node.key);
7541
+ }
7542
+ }
7543
+ }
7544
+ const state = {
7545
+ activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
7546
+ activeColIdx: this.activeColIdx,
7547
+ selectedKeys: options.selectedKeys === false
7548
+ ? undefined
7549
+ : this.getSelectedNodes().flatMap((n) => n.key),
7550
+ expandedKeys: expandedKeys,
7551
+ };
7552
+ return state;
7553
+ }
7554
+ /** Apply selection/expansion/activation status. @experimental */
7555
+ setState(state, options) {
7556
+ this.runWithDeferredUpdate(() => {
7557
+ var _a, _b;
7558
+ if (state.selectedKeys) {
7559
+ this.selectAll(false);
7560
+ for (const key of state.selectedKeys) {
7561
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setSelected(true);
7562
+ }
7563
+ }
7564
+ if (state.expandedKeys) {
7565
+ for (const key of state.expandedKeys) {
7566
+ (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setExpanded(true);
7567
+ }
7568
+ }
7569
+ if (state.activeKey) {
7570
+ this.setActiveNode(state.activeKey);
7571
+ }
7572
+ if (state.activeColIdx != null) {
7573
+ this.setColumn(state.activeColIdx);
7574
+ }
7575
+ });
7576
+ }
7505
7577
  update(change, node, options) {
7506
7578
  // this.log(`update(${change}) node=${node}`);
7507
7579
  if (!(node instanceof WunderbaumNode)) {
@@ -7783,11 +7855,11 @@ class Wunderbaum {
7783
7855
  // }
7784
7856
  return modified;
7785
7857
  }
7786
- _insertIcon(icon, elem) {
7787
- const iconElem = document.createElement("i");
7788
- iconElem.className = icon;
7789
- elem.appendChild(iconElem);
7790
- }
7858
+ // protected _insertIcon(icon: string, elem: HTMLElement) {
7859
+ // const iconElem = document.createElement("i");
7860
+ // iconElem.className = icon;
7861
+ // elem.appendChild(iconElem);
7862
+ // }
7791
7863
  /** Create/update header markup from `this.columns` definition.
7792
7864
  * @internal
7793
7865
  */
@@ -7885,6 +7957,104 @@ class Wunderbaum {
7885
7957
  this._updateViewportImmediately();
7886
7958
  }
7887
7959
  }
7960
+ /** @internal */
7961
+ _createNodeIcon(node, showLoading, showBadge) {
7962
+ const iconMap = this.iconMap;
7963
+ let iconElem;
7964
+ let icon = node.getOption("icon");
7965
+ if (node._errorInfo) {
7966
+ icon = iconMap.error;
7967
+ }
7968
+ else if (node._isLoading && showLoading) {
7969
+ // Status nodes, or nodes without expander (< minExpandLevel) should
7970
+ // display the 'loading' status with the i.wb-icon span
7971
+ icon = iconMap.loading;
7972
+ }
7973
+ if (icon === false) {
7974
+ return null; // explicitly disabled: don't try default icons
7975
+ }
7976
+ if (typeof icon === "string") ;
7977
+ else if (node.statusNodeType) {
7978
+ icon = iconMap[node.statusNodeType];
7979
+ }
7980
+ else if (node.expanded) {
7981
+ icon = iconMap.folderOpen;
7982
+ }
7983
+ else if (node.children) {
7984
+ icon = iconMap.folder;
7985
+ }
7986
+ else if (node.lazy) {
7987
+ icon = iconMap.folderLazy;
7988
+ }
7989
+ else {
7990
+ icon = iconMap.doc;
7991
+ }
7992
+ if (!icon) {
7993
+ iconElem = document.createElement("i");
7994
+ iconElem.className = "wb-icon";
7995
+ }
7996
+ else if (icon.indexOf("<") >= 0) {
7997
+ // HTML
7998
+ iconElem = elemFromHtml(icon);
7999
+ }
8000
+ else if (TEST_IMG.test(icon)) {
8001
+ // Image URL
8002
+ iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
8003
+ }
8004
+ else {
8005
+ // Class name
8006
+ iconElem = document.createElement("i");
8007
+ iconElem.className = "wb-icon " + icon;
8008
+ }
8009
+ // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
8010
+ const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem });
8011
+ let badge = null;
8012
+ if (cbRes != null && cbRes !== false) {
8013
+ let classes = "";
8014
+ let tooltip = "";
8015
+ if (isPlainObject(cbRes)) {
8016
+ badge = "" + cbRes.badge;
8017
+ classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
8018
+ tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
8019
+ }
8020
+ else if (typeof cbRes === "number") {
8021
+ badge = "" + cbRes;
8022
+ }
8023
+ else {
8024
+ badge = cbRes; // string or HTMLSpanElement
8025
+ }
8026
+ if (typeof badge === "string") {
8027
+ badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
8028
+ }
8029
+ if (badge) {
8030
+ iconElem.append(badge);
8031
+ }
8032
+ }
8033
+ return iconElem;
8034
+ }
8035
+ _updateTopBreadcrumb() {
8036
+ const breadcrumb = this.breadcrumb;
8037
+ const topmost = this.getTopmostVpNode(true);
8038
+ const parentList = topmost === null || topmost === void 0 ? void 0 : topmost.getParentList(false, false);
8039
+ if (parentList === null || parentList === void 0 ? void 0 : parentList.length) {
8040
+ breadcrumb.innerHTML = "";
8041
+ for (const n of topmost.getParentList(false, false)) {
8042
+ const icon = this._createNodeIcon(n, false, false);
8043
+ if (icon) {
8044
+ breadcrumb.append(icon, " ");
8045
+ }
8046
+ const part = document.createElement("a");
8047
+ part.textContent = n.title;
8048
+ part.href = "#";
8049
+ part.classList.add("wb-breadcrumb");
8050
+ part.dataset.key = n.key;
8051
+ breadcrumb.append(part, this.options.strings.breadcrumbDelimiter);
8052
+ }
8053
+ }
8054
+ else {
8055
+ breadcrumb.innerHTML = "&nbsp;";
8056
+ }
8057
+ }
7888
8058
  /**
7889
8059
  * This is the actual update method, which is wrapped inside a throttle method.
7890
8060
  * It calls `updateColumns()` and `_updateRows()`.
@@ -7895,7 +8065,6 @@ class Wunderbaum {
7895
8065
  * @internal
7896
8066
  */
7897
8067
  _updateViewportImmediately() {
7898
- var _a;
7899
8068
  if (this._disableUpdateCount) {
7900
8069
  this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`);
7901
8070
  this._disableUpdateIgnoreCount++;
@@ -7942,11 +8111,8 @@ class Wunderbaum {
7942
8111
  this._updateRows();
7943
8112
  // console.profileEnd(`_updateViewportImmediately()`)
7944
8113
  }
7945
- if (this.options.connectTopBreadcrumb) {
7946
- assert(this.options.connectTopBreadcrumb.textContent != null, `Invalid 'connectTopBreadcrumb' option (input element expected).`);
7947
- let path = (_a = this.getTopmostVpNode(true)) === null || _a === void 0 ? void 0 : _a.getPath(false, "title", " > ");
7948
- path = path ? path + " >" : "";
7949
- this.options.connectTopBreadcrumb.textContent = path;
8114
+ if (this.breadcrumb) {
8115
+ this._updateTopBreadcrumb();
7950
8116
  }
7951
8117
  this._callEvent("update");
7952
8118
  }
@@ -8315,7 +8481,7 @@ class Wunderbaum {
8315
8481
  }
8316
8482
  Wunderbaum.sequence = 0;
8317
8483
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8318
- Wunderbaum.version = "v0.12.1"; // Set to semver by 'grunt release'
8484
+ Wunderbaum.version = "v0.13.0"; // Set to semver by 'grunt release'
8319
8485
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8320
8486
  Wunderbaum.util = util;
8321
8487