wunderbaum 0.12.0 → 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,4 +1,9 @@
1
1
  /*!
2
+ * Wunderbaum - debounce.ts
3
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
4
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
5
+ */
6
+ /*
2
7
  * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21
3
8
  * MIT License: https://raw.githubusercontent.com/lodash/lodash/4.17.21-npm/LICENSE
4
9
  * Modified for TypeScript type annotations.
@@ -287,8 +292,8 @@ function throttle(func, wait = 0, options = {}) {
287
292
 
288
293
  /*!
289
294
  * Wunderbaum - util
290
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
291
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
295
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
296
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
292
297
  */
293
298
  /** @module util */
294
299
  /** Readable names for `MouseEvent.button` */
@@ -671,18 +676,6 @@ function elemFromSelector(obj) {
671
676
  }
672
677
  return obj;
673
678
  }
674
- // /** Return a EventTarget from selector or cast an existing element. */
675
- // export function eventTargetFromSelector(
676
- // obj: string | EventTarget
677
- // ): EventTarget | null {
678
- // if (!obj) {
679
- // return null;
680
- // }
681
- // if (typeof obj === "string") {
682
- // return document.querySelector(obj) as EventTarget;
683
- // }
684
- // return obj as EventTarget;
685
- // }
686
679
  /**
687
680
  * Return a canonical descriptive string for a keyboard or mouse event.
688
681
  *
@@ -1141,8 +1134,8 @@ var util = /*#__PURE__*/Object.freeze({
1141
1134
 
1142
1135
  /*!
1143
1136
  * Wunderbaum - types
1144
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
1145
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
1137
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1138
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1146
1139
  */
1147
1140
  /**
1148
1141
  * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
@@ -1166,7 +1159,7 @@ var ChangeType;
1166
1159
  /** Vertical scroll event. Update the 'top' property of all rows. */
1167
1160
  ChangeType["scroll"] = "scroll";
1168
1161
  })(ChangeType || (ChangeType = {}));
1169
- /* Internal use. */
1162
+ /** @internal */
1170
1163
  var RenderFlag;
1171
1164
  (function (RenderFlag) {
1172
1165
  RenderFlag["clearMarkup"] = "clearMarkup";
@@ -1197,16 +1190,20 @@ var NodeRegion;
1197
1190
  /** Initial navigation mode and possible transition. */
1198
1191
  var NavModeEnum;
1199
1192
  (function (NavModeEnum) {
1193
+ /** Start with row mode, but allow cell-nav mode */
1200
1194
  NavModeEnum["startRow"] = "startRow";
1195
+ /** Cell-nav mode only */
1201
1196
  NavModeEnum["cell"] = "cell";
1197
+ /** Start in cell-nav mode, but allow row mode */
1202
1198
  NavModeEnum["startCell"] = "startCell";
1199
+ /** Row mode only */
1203
1200
  NavModeEnum["row"] = "row";
1204
1201
  })(NavModeEnum || (NavModeEnum = {}));
1205
1202
 
1206
1203
  /*!
1207
1204
  * Wunderbaum - wb_extension_base
1208
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
1209
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
1205
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1206
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1210
1207
  */
1211
1208
  class WunderbaumExtension {
1212
1209
  constructor(tree, id, defaults) {
@@ -1264,8 +1261,8 @@ class WunderbaumExtension {
1264
1261
 
1265
1262
  /*!
1266
1263
  * Wunderbaum - ext-filter
1267
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
1268
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
1264
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1265
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1269
1266
  */
1270
1267
  const START_MARKER = "\uFFF7";
1271
1268
  const END_MARKER = "\uFFF8";
@@ -1277,7 +1274,7 @@ class FilterExtension extends WunderbaumExtension {
1277
1274
  autoApply: true, // Re-apply last filter if lazy data is loaded
1278
1275
  autoExpand: false, // Expand all branches that contain matches while filtered
1279
1276
  matchBranch: false, // Whether to implicitly match all children of matched nodes
1280
- 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
1281
1278
  fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
1282
1279
  hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
1283
1280
  highlight: true, // Highlight matches by wrapping inside <mark> tags
@@ -1285,36 +1282,117 @@ class FilterExtension extends WunderbaumExtension {
1285
1282
  mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
1286
1283
  noData: true, // Display a 'no data' status node if result is empty
1287
1284
  });
1285
+ this.queryInput = null;
1286
+ this.prevButton = null;
1287
+ this.nextButton = null;
1288
+ this.modeButton = null;
1289
+ this.matchInfoElem = null;
1288
1290
  this.lastFilterArgs = null;
1289
1291
  }
1290
1292
  init() {
1291
1293
  super.init();
1292
- const connectInput = this.getPluginOption("connectInput");
1293
- if (connectInput) {
1294
- this.queryInput = elemFromSelector(connectInput);
1295
- assert(this.queryInput, `Invalid 'filter.connectInput' option: ${connectInput}.`);
1296
- onEvent(this.queryInput, "input", debounce((e) => {
1297
- // this.tree.log("query", e);
1298
- this.filterNodes(this.queryInput.value.trim(), {});
1299
- }, 700));
1294
+ const connect = this.getPluginOption("connect");
1295
+ if (connect) {
1296
+ this._connectControls();
1300
1297
  }
1301
1298
  }
1302
1299
  setPluginOption(name, value) {
1303
- // alert("filter opt=" + name + ", " + value)
1304
1300
  super.setPluginOption(name, value);
1305
1301
  switch (name) {
1306
1302
  case "mode":
1307
- this.tree.filterMode = value === "hide" ? "hide" : "dim";
1303
+ this.tree.filterMode =
1304
+ value === "hide" ? "hide" : value === "mark" ? "mark" : "dim";
1308
1305
  this.tree.updateFilter();
1309
1306
  break;
1310
1307
  }
1311
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
+ }
1312
1389
  _applyFilterNoUpdate(filter, _opts) {
1313
1390
  return this.tree.runWithDeferredUpdate(() => {
1314
1391
  return this._applyFilterImpl(filter, _opts);
1315
1392
  });
1316
1393
  }
1317
1394
  _applyFilterImpl(filter, _opts) {
1395
+ var _a;
1318
1396
  let //temp,
1319
1397
  count = 0;
1320
1398
  const start = Date.now();
@@ -1396,11 +1474,11 @@ class FilterExtension extends WunderbaumExtension {
1396
1474
  return !!res;
1397
1475
  };
1398
1476
  }
1399
- tree.filterMode = opts.mode;
1400
- // eslint-disable-next-line prefer-rest-params, prefer-spread
1477
+ tree.filterMode = (_a = opts.mode) !== null && _a !== void 0 ? _a : "dim";
1478
+ // eslint-disable-next-line prefer-rest-params
1401
1479
  this.lastFilterArgs = arguments;
1402
1480
  tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
1403
- tree.element.classList.toggle("wb-ext-filter-dim", !hideMode);
1481
+ tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim");
1404
1482
  tree.element.classList.toggle("wb-ext-filter-hide-expanders", !!opts.hideExpanders);
1405
1483
  // Reset current filter
1406
1484
  tree.root.subMatchCount = 0;
@@ -1409,10 +1487,6 @@ class FilterExtension extends WunderbaumExtension {
1409
1487
  delete node.titleWithHighlight;
1410
1488
  node.subMatchCount = 0;
1411
1489
  });
1412
- // statusNode = tree.root.findDirectChild(KEY_NODATA);
1413
- // if (statusNode) {
1414
- // statusNode.remove();
1415
- // }
1416
1490
  tree.setStatus(NodeStatusType.ok);
1417
1491
  // Adjust node.hide, .match, and .subMatchCount properties
1418
1492
  treeOpts.autoCollapse = false; // #528
@@ -1423,7 +1497,7 @@ class FilterExtension extends WunderbaumExtension {
1423
1497
  let res = filter(node);
1424
1498
  if (res === "skip") {
1425
1499
  node.visit(function (c) {
1426
- c.match = false;
1500
+ c.match = undefined;
1427
1501
  }, true);
1428
1502
  return "skip";
1429
1503
  }
@@ -1434,7 +1508,7 @@ class FilterExtension extends WunderbaumExtension {
1434
1508
  }
1435
1509
  if (res) {
1436
1510
  count++;
1437
- node.match = true;
1511
+ node.match = count;
1438
1512
  node.visitParents((p) => {
1439
1513
  if (p !== node) {
1440
1514
  p.subMatchCount += 1;
@@ -1461,6 +1535,7 @@ class FilterExtension extends WunderbaumExtension {
1461
1535
  }
1462
1536
  // Redraw whole tree
1463
1537
  tree.logDebug(`Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.`);
1538
+ this._updatedConnectedControls();
1464
1539
  return count;
1465
1540
  }
1466
1541
  /**
@@ -1505,34 +1580,22 @@ class FilterExtension extends WunderbaumExtension {
1505
1580
  else {
1506
1581
  tree.logWarn("updateFilter(): no filter active.");
1507
1582
  }
1583
+ this._updatedConnectedControls();
1508
1584
  }
1509
1585
  /**
1510
1586
  * [ext-filter] Reset the filter.
1511
1587
  */
1512
1588
  clearFilter() {
1513
1589
  const tree = this.tree;
1514
- // statusNode = tree.root.findDirectChild(KEY_NODATA),
1515
- // escapeTitles = tree.options.escapeTitles;
1516
1590
  tree.enableUpdate(false);
1517
- // if (statusNode) {
1518
- // statusNode.remove();
1519
- // }
1520
1591
  tree.setStatus(NodeStatusType.ok);
1521
1592
  // we also counted root node's subMatchCount
1522
1593
  delete tree.root.match;
1523
1594
  delete tree.root.subMatchCount;
1524
1595
  tree.visit((node) => {
1525
- // if (node.match && node._rowElem) {
1526
- // let titleElem = node._rowElem.querySelector("span.wb-title")!;
1527
- // node._callEvent("enhanceTitle", { titleElem: titleElem });
1528
- // }
1529
1596
  delete node.match;
1530
1597
  delete node.subMatchCount;
1531
1598
  delete node.titleWithHighlight;
1532
- // if (node.subMatchBadge) {
1533
- // node.subMatchBadge.remove();
1534
- // delete node.subMatchBadge;
1535
- // }
1536
1599
  if (node._filterAutoExpanded && node.expanded) {
1537
1600
  node.setExpanded(false, {
1538
1601
  noAnimation: true,
@@ -1546,7 +1609,7 @@ class FilterExtension extends WunderbaumExtension {
1546
1609
  tree.element.classList.remove(
1547
1610
  // "wb-ext-filter",
1548
1611
  "wb-ext-filter-dim", "wb-ext-filter-hide");
1549
- // tree._callHook("treeStructureChanged", this, "clearFilter");
1612
+ this._updatedConnectedControls();
1550
1613
  tree.enableUpdate(true);
1551
1614
  }
1552
1615
  }
@@ -1589,8 +1652,8 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1589
1652
 
1590
1653
  /*!
1591
1654
  * Wunderbaum - ext-keynav
1592
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
1593
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
1655
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
1656
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1594
1657
  */
1595
1658
  const QUICKSEARCH_DELAY = 500;
1596
1659
  class KeynavExtension extends WunderbaumExtension {
@@ -1953,8 +2016,8 @@ class KeynavExtension extends WunderbaumExtension {
1953
2016
 
1954
2017
  /*!
1955
2018
  * Wunderbaum - ext-logger
1956
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
1957
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
2019
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2020
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
1958
2021
  */
1959
2022
  class LoggerExtension extends WunderbaumExtension {
1960
2023
  constructor(tree) {
@@ -1995,8 +2058,8 @@ class LoggerExtension extends WunderbaumExtension {
1995
2058
 
1996
2059
  /*!
1997
2060
  * Wunderbaum - ext-dnd
1998
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
1999
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
2061
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2062
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2000
2063
  */
2001
2064
  const nodeMimeType = "application/x-wunderbaum-node";
2002
2065
  class DndExtension extends WunderbaumExtension {
@@ -2216,7 +2279,7 @@ class DndExtension extends WunderbaumExtension {
2216
2279
  viewportY >= height - sensitivity) {
2217
2280
  // Mouse in bottom 20px area: scroll down
2218
2281
  // sp.scrollTop = scrollTop + dndOpts.scrollSpeed;
2219
- this.currentScrollDir = +1;
2282
+ this.currentScrollDir = 1;
2220
2283
  }
2221
2284
  if (this.currentScrollDir) {
2222
2285
  this.applyScrollDirThrottled();
@@ -2445,8 +2508,8 @@ class DndExtension extends WunderbaumExtension {
2445
2508
 
2446
2509
  /*!
2447
2510
  * Wunderbaum - drag_observer
2448
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
2449
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
2511
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2512
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2450
2513
  */
2451
2514
  /**
2452
2515
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2594,8 +2657,8 @@ class DragObserver {
2594
2657
 
2595
2658
  /*!
2596
2659
  * Wunderbaum - common
2597
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
2598
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
2660
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
2661
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2599
2662
  */
2600
2663
  const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
2601
2664
  /**
@@ -2622,8 +2685,11 @@ const TEST_IMG = new RegExp(/\.|\//);
2622
2685
  // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
2623
2686
  // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
2624
2687
  /**
2625
- * Default node icons.
2626
- * Requires bootstrap icons https://icons.getbootstrap.com
2688
+ * Default node icons for icon libraries
2689
+ *
2690
+ * - 'bootstrap': {@link https://icons.getbootstrap.com}
2691
+ * - 'fontawesome6' {@link https://fontawesome.com/icons}
2692
+ *
2627
2693
  */
2628
2694
  const iconMaps = {
2629
2695
  bootstrap: {
@@ -2705,29 +2771,20 @@ const RESERVED_TREE_SOURCE_KEYS = new Set([
2705
2771
  // "Escape",
2706
2772
  // ]);
2707
2773
  /** Map `KeyEvent.key` to navigation action. */
2708
- const KEY_TO_ACTION_DICT = {
2709
- " ": "toggleSelect",
2710
- "+": "expand",
2711
- Add: "expand",
2774
+ const KEY_TO_NAVIGATION_MAP = {
2712
2775
  ArrowDown: "down",
2713
2776
  ArrowLeft: "left",
2714
2777
  ArrowRight: "right",
2715
2778
  ArrowUp: "up",
2716
2779
  Backspace: "parent",
2717
- "/": "collapseAll",
2718
- Divide: "collapseAll",
2719
2780
  End: "lastCol",
2720
2781
  Home: "firstCol",
2721
2782
  "Control+End": "last",
2722
2783
  "Control+Home": "first",
2723
2784
  "Meta+ArrowDown": "last", // macOs
2724
2785
  "Meta+ArrowUp": "first", // macOs
2725
- "*": "expandAll",
2726
- Multiply: "expandAll",
2727
2786
  PageDown: "pageDown",
2728
2787
  PageUp: "pageUp",
2729
- "-": "collapse",
2730
- Subtract: "collapse",
2731
2788
  };
2732
2789
  /** Return a callback that returns true if the node title matches the string
2733
2790
  * or regular expression.
@@ -2764,10 +2821,12 @@ function nodeTitleSorter(a, b) {
2764
2821
  /**
2765
2822
  * Convert 'flat' to 'nested' format.
2766
2823
  *
2767
- * Flat node entry format:
2768
- * [PARENT_ID, [POSITIONAL_ARGS]]
2769
- * or
2770
- * [PARENT_ID, [POSITIONAL_ARGS], {KEY_VALUE_ARGS}]
2824
+ * Flat node entry format:
2825
+ * [PARENT_IDX, {KEY_VALUE_ARGS}]
2826
+ * or, if N _positional re defined:
2827
+ * [PARENT_IDX, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., POSITIONAL_ARG_N]
2828
+ * Even if _positional additional are defined, KEY_VALUE_ARGS can be appended:
2829
+ * [PARENT_IDX, POSITIONAL_ARG_1, ..., {KEY_VALUE_ARGS}]
2771
2830
  *
2772
2831
  * 1. Parent-referencing list is converted to a list of nested dicts with
2773
2832
  * optional `children` properties.
@@ -2776,10 +2835,11 @@ function nodeTitleSorter(a, b) {
2776
2835
  function unflattenSource(source) {
2777
2836
  var _a, _b, _c;
2778
2837
  const { _format, _keyMap = {}, _positional = [], children } = source;
2838
+ const _positionalCount = _positional.length;
2779
2839
  if (_format !== "flat") {
2780
2840
  throw new Error(`Expected source._format: "flat", but got ${_format}`);
2781
2841
  }
2782
- if (_positional && _positional.includes("children")) {
2842
+ if (_positionalCount && _positional.includes("children")) {
2783
2843
  throw new Error(`source._positional must not include "children": ${_positional}`);
2784
2844
  }
2785
2845
  let longToShort = _keyMap;
@@ -2793,7 +2853,7 @@ function unflattenSource(source) {
2793
2853
  longToShort[value] = key;
2794
2854
  }
2795
2855
  }
2796
- const positionalShort = _positional.map((e) => longToShort[e]);
2856
+ const positionalShort = _positional.map((e) => { var _a; return (_a = longToShort[e]) !== null && _a !== void 0 ? _a : e; });
2797
2857
  const newChildren = [];
2798
2858
  const keyToNodeMap = {};
2799
2859
  const indexToNodeMap = {};
@@ -2803,19 +2863,32 @@ function unflattenSource(source) {
2803
2863
  // Node entry format:
2804
2864
  // [PARENT_ID, [POSITIONAL_ARGS]]
2805
2865
  // or
2806
- // [PARENT_ID, [POSITIONAL_ARGS], {KEY_VALUE_ARGS}]
2807
- const [parentId, args, kwargs = {}] = nodeTuple;
2866
+ // [PARENT_ID, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., {KEY_VALUE_ARGS}]
2867
+ let kwargs;
2868
+ const [parentId, ...args] = nodeTuple;
2869
+ if (args.length === _positionalCount) {
2870
+ kwargs = {};
2871
+ }
2872
+ else if (args.length === _positionalCount + 1) {
2873
+ kwargs = args.pop();
2874
+ if (typeof kwargs !== "object") {
2875
+ throw new Error(`unflattenSource: Expected dict as last tuple element: ${nodeTuple}`);
2876
+ }
2877
+ }
2878
+ else {
2879
+ throw new Error(`unflattenSource: unexpected tuple length: ${nodeTuple}`);
2880
+ }
2808
2881
  // Free up some memory as we go
2809
2882
  nodeTuple[1] = null;
2810
2883
  if (nodeTuple[2] != null) {
2811
2884
  nodeTuple[2] = null;
2812
2885
  }
2813
- // console.log("flatten", parentId, args, kwargs)
2814
2886
  // We keep `kwargs` as our new node definition. Then we add all positional
2815
2887
  // values to this object:
2816
2888
  args.forEach((val, positionalIdx) => {
2817
2889
  kwargs[positionalShort[positionalIdx]] = val;
2818
2890
  });
2891
+ args.length = 0;
2819
2892
  // Find the parent node. `null` means 'toplevel'. PARENT_ID may be the numeric
2820
2893
  // index of the source.children list. If PARENT_ID is a string, we search
2821
2894
  // a parent with node.key of this value.
@@ -2934,8 +3007,8 @@ function decompressSourceData(source) {
2934
3007
 
2935
3008
  /*!
2936
3009
  * Wunderbaum - ext-grid
2937
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
2938
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
3010
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3011
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
2939
3012
  */
2940
3013
  class GridExtension extends WunderbaumExtension {
2941
3014
  constructor(tree) {
@@ -3025,8 +3098,8 @@ class GridExtension extends WunderbaumExtension {
3025
3098
 
3026
3099
  /*!
3027
3100
  * Wunderbaum - deferred
3028
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
3029
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
3101
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3102
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3030
3103
  */
3031
3104
  /**
3032
3105
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -3078,8 +3151,8 @@ class Deferred {
3078
3151
 
3079
3152
  /*!
3080
3153
  * Wunderbaum - wunderbaum_node
3081
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
3082
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
3154
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
3155
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
3083
3156
  */
3084
3157
  /** WunderbaumNode properties that can be passed with source data.
3085
3158
  * (Any other source properties will be stored as `node.data.PROP`.)
@@ -3861,7 +3934,7 @@ class WunderbaumNode {
3861
3934
  isParentOf(other) {
3862
3935
  return other && other.parent === this;
3863
3936
  }
3864
- /** (experimental) Return true if this node is partially loaded. */
3937
+ /** Return true if this node is partially loaded. @experimental */
3865
3938
  isPartload() {
3866
3939
  return !!this._partload;
3867
3940
  }
@@ -4319,10 +4392,11 @@ class WunderbaumNode {
4319
4392
  * @param options
4320
4393
  */
4321
4394
  async navigate(where, options) {
4395
+ var _a;
4322
4396
  // Allow to pass 'ArrowLeft' instead of 'left'
4323
- where = KEY_TO_ACTION_DICT[where] || where;
4397
+ const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where);
4324
4398
  // Otherwise activate or focus the related node
4325
- const node = this.findRelatedNode(where);
4399
+ const node = this.findRelatedNode(navType);
4326
4400
  if (!node) {
4327
4401
  this.logWarn(`Could not find related node '${where}'.`);
4328
4402
  return Promise.resolve(this);
@@ -4419,86 +4493,17 @@ class WunderbaumNode {
4419
4493
  renderColInfosById: renderColInfosById,
4420
4494
  };
4421
4495
  }
4422
- _createIcon(iconMap, parentElem, replaceChild, showLoading) {
4423
- let iconSpan;
4424
- let icon = this.getOption("icon");
4425
- if (this._errorInfo) {
4426
- icon = iconMap.error;
4427
- }
4428
- else if (this._isLoading && showLoading) {
4429
- // Status nodes, or nodes without expander (< minExpandLevel) should
4430
- // display the 'loading' status with the i.wb-icon span
4431
- icon = iconMap.loading;
4432
- }
4433
- if (icon === false) {
4434
- return null; // explicitly disabled: don't try default icons
4435
- }
4436
- if (typeof icon === "string") ;
4437
- else if (this.statusNodeType) {
4438
- icon = iconMap[this.statusNodeType];
4439
- }
4440
- else if (this.expanded) {
4441
- icon = iconMap.folderOpen;
4442
- }
4443
- else if (this.children) {
4444
- icon = iconMap.folder;
4445
- }
4446
- else if (this.lazy) {
4447
- icon = iconMap.folderLazy;
4448
- }
4449
- else {
4450
- icon = iconMap.doc;
4451
- }
4452
- // this.log("_createIcon: " + icon);
4453
- if (!icon) {
4454
- iconSpan = document.createElement("i");
4455
- iconSpan.className = "wb-icon";
4456
- }
4457
- else if (icon.indexOf("<") >= 0) {
4458
- // HTML
4459
- iconSpan = elemFromHtml(icon);
4460
- }
4461
- else if (TEST_IMG.test(icon)) {
4462
- // Image URL
4463
- iconSpan = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
4464
- }
4465
- else {
4466
- // Class name
4467
- iconSpan = document.createElement("i");
4468
- iconSpan.className = "wb-icon " + icon;
4469
- }
4470
- if (replaceChild) {
4471
- parentElem.replaceChild(iconSpan, replaceChild);
4472
- }
4473
- else {
4474
- parentElem.appendChild(iconSpan);
4475
- }
4476
- // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
4477
- const cbRes = this._callEvent("iconBadge", { iconSpan: iconSpan });
4478
- let badge = null;
4479
- if (cbRes != null && cbRes !== false) {
4480
- let classes = "";
4481
- let tooltip = "";
4482
- if (isPlainObject(cbRes)) {
4483
- badge = "" + cbRes.badge;
4484
- classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
4485
- tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
4486
- }
4487
- else if (typeof cbRes === "number") {
4488
- 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);
4489
4501
  }
4490
4502
  else {
4491
- badge = cbRes; // string or HTMLSpanElement
4492
- }
4493
- if (typeof badge === "string") {
4494
- badge = elemFromHtml(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
4495
- }
4496
- if (badge) {
4497
- iconSpan.append(badge);
4503
+ parentElem.appendChild(iconElem);
4498
4504
  }
4499
4505
  }
4500
- // this.log("_createIcon: ", iconSpan);
4501
- return iconSpan;
4506
+ return iconElem;
4502
4507
  }
4503
4508
  /**
4504
4509
  * Create a whole new `<div class="wb-row">` element.
@@ -4553,7 +4558,7 @@ class WunderbaumNode {
4553
4558
  }
4554
4559
  // Render the icon (show a 'loading' icon if we do not have an expander that
4555
4560
  // we would prefer).
4556
- const iconSpan = this._createIcon(tree.iconMap, nodeElem, null, !expanderSpan);
4561
+ const iconSpan = this._createIcon(nodeElem, null, !expanderSpan);
4557
4562
  if (iconSpan) {
4558
4563
  ofsTitlePx += ICON_WIDTH;
4559
4564
  }
@@ -4785,7 +4790,7 @@ class WunderbaumNode {
4785
4790
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
4786
4791
  const iconSpan = nodeElem.querySelector("i.wb-icon");
4787
4792
  if (iconSpan) {
4788
- this._createIcon(tree.iconMap, nodeElem, iconSpan, !expanderSpan);
4793
+ this._createIcon(nodeElem, iconSpan, !expanderSpan);
4789
4794
  }
4790
4795
  }
4791
4796
  // Adjust column width
@@ -5163,7 +5168,8 @@ class WunderbaumNode {
5163
5168
  case undefined:
5164
5169
  changed = this.selected || !this._partsel;
5165
5170
  this.selected = false;
5166
- this._partsel = true;
5171
+ // #110: end nodess cannot have a `_partsel` flag
5172
+ this._partsel = this.hasChildren() ? true : false;
5167
5173
  break;
5168
5174
  default:
5169
5175
  error(`Invalid state: ${state}`);
@@ -5333,7 +5339,7 @@ class WunderbaumNode {
5333
5339
  assert(data.statusNodeType, "Not a status node");
5334
5340
  assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node");
5335
5341
  statusNode = this.addNode(data, "prependChild");
5336
- statusNode.match = true;
5342
+ statusNode.match = -1; // Mark as 'match' to avoid hiding
5337
5343
  tree.update(ChangeType.structure);
5338
5344
  return statusNode;
5339
5345
  };
@@ -5624,8 +5630,8 @@ WunderbaumNode.sequence = 0;
5624
5630
 
5625
5631
  /*!
5626
5632
  * Wunderbaum - ext-edit
5627
- * Copyright (c) 2021-2024, Martin Wendt. Released under the MIT license.
5628
- * v0.12.0, Sun, 12 Jan 2025 10:51:41 GMT (https://github.com/mar10/wunderbaum)
5633
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
5634
+ * v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
5629
5635
  */
5630
5636
  // const START_MARKER = "\uFFF7";
5631
5637
  class EditExtension extends WunderbaumExtension {
@@ -5944,7 +5950,7 @@ class EditExtension extends WunderbaumExtension {
5944
5950
  newNode.setClass("wb-edit-new");
5945
5951
  this.relatedNode = node;
5946
5952
  // Don't filter new nodes:
5947
- newNode.match = true;
5953
+ newNode.match = -1;
5948
5954
  newNode.makeVisible({ noAnimation: true }).then(() => {
5949
5955
  this.startEditTitle(newNode);
5950
5956
  });
@@ -5956,12 +5962,12 @@ class EditExtension extends WunderbaumExtension {
5956
5962
  *
5957
5963
  * A treegrid control.
5958
5964
  *
5959
- * Copyright (c) 2021-2024, Martin Wendt (https://wwWendt.de).
5965
+ * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de).
5960
5966
  * https://github.com/mar10/wunderbaum
5961
5967
  *
5962
5968
  * Released under the MIT license.
5963
- * @version v0.12.0
5964
- * @date Sun, 12 Jan 2025 10:51:41 GMT
5969
+ * @version v0.13.0
5970
+ * @date Sat, 08 Mar 2025 14:16:31 GMT
5965
5971
  */
5966
5972
  // import "./wunderbaum.scss";
5967
5973
  class WbSystemRoot extends WunderbaumNode {
@@ -5982,7 +5988,7 @@ class WbSystemRoot extends WunderbaumNode {
5982
5988
  */
5983
5989
  class Wunderbaum {
5984
5990
  /** Currently active node if any.
5985
- * Use @link {WunderbaumNode.setActive|setActive} to modify.
5991
+ * Use {@link WunderbaumNode.setActive|setActive} to modify.
5986
5992
  */
5987
5993
  get activeNode() {
5988
5994
  var _a;
@@ -5990,7 +5996,7 @@ class Wunderbaum {
5990
5996
  return ((_a = this._activeNode) === null || _a === void 0 ? void 0 : _a.tree) ? this._activeNode : null;
5991
5997
  }
5992
5998
  /** Current node hat has keyboard focus if any.
5993
- * Use @link {WunderbaumNode.setFocus|setFocus()} to modify.
5999
+ * Use {@link WunderbaumNode.setFocus|setFocus()} to modify.
5994
6000
  */
5995
6001
  get focusNode() {
5996
6002
  var _a;
@@ -6022,6 +6028,9 @@ class Wunderbaum {
6022
6028
  // --- SELECT ---
6023
6029
  // /** @internal */
6024
6030
  // public selectRangeAnchor: WunderbaumNode | null = null;
6031
+ // --- BREADCRUMB ---
6032
+ /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6033
+ this.breadcrumb = null;
6025
6034
  // --- FILTER ---
6026
6035
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
6027
6036
  this.filterMode = null;
@@ -6056,10 +6065,10 @@ class Wunderbaum {
6056
6065
  emptyChildListExpandable: false,
6057
6066
  // updateThrottleWait: 200,
6058
6067
  skeleton: false,
6059
- connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
6068
+ connectTopBreadcrumb: null,
6060
6069
  selectMode: "multi", // SelectModeType
6061
6070
  // --- KeyNav ---
6062
- navigationModeOption: null, // NavModeEnum.startRow,
6071
+ navigationModeOption: null, // NavModeEnum,
6063
6072
  quicksearch: true,
6064
6073
  // --- Events ---
6065
6074
  iconBadge: null,
@@ -6071,8 +6080,11 @@ class Wunderbaum {
6071
6080
  strings: {
6072
6081
  loadError: "Error",
6073
6082
  loading: "Loading...",
6074
- // loading: "Loading&hellip;",
6075
6083
  noData: "No data",
6084
+ breadcrumbDelimiter: " » ",
6085
+ queryResult: "Found ${matches} of ${count}",
6086
+ noMatch: "No results",
6087
+ matchIndex: "${match} of ${matches}",
6076
6088
  },
6077
6089
  }, options));
6078
6090
  const readyDeferred = new Deferred();
@@ -6140,7 +6152,7 @@ class Wunderbaum {
6140
6152
  const wantHeader = opts.header == null ? this.columns.length > 1 : !!opts.header;
6141
6153
  if (this.headerElement) {
6142
6154
  // User existing header markup to define `this.columns`
6143
- assert(!this.columns, "`opts.columns` must not be set if markup already contains a header");
6155
+ assert(!this.columns, "`opts.columns` must not be set if table markup already contains a header");
6144
6156
  this.columns = [];
6145
6157
  const rowElement = this.headerElement.querySelector("div.wb-row");
6146
6158
  for (const colDiv of rowElement.querySelectorAll("div")) {
@@ -6178,6 +6190,19 @@ class Wunderbaum {
6178
6190
  this.headerElement =
6179
6191
  this.element.querySelector("div.wb-header");
6180
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
+ }
6181
6206
  this._initExtensions();
6182
6207
  // --- apply initial options
6183
6208
  ["enabled", "fixedCol"].forEach((optName) => {
@@ -6188,8 +6213,7 @@ class Wunderbaum {
6188
6213
  // --- Load initial data
6189
6214
  if (opts.source) {
6190
6215
  if (opts.showSpinner) {
6191
- this.nodeListElement.innerHTML =
6192
- "<progress class='spinner'>loading...</progress>";
6216
+ this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
6193
6217
  }
6194
6218
  this.load(opts.source)
6195
6219
  .then(() => {
@@ -6542,7 +6566,10 @@ class Wunderbaum {
6542
6566
  });
6543
6567
  return node;
6544
6568
  }
6545
- /** 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
+ */
6546
6573
  getTopmostVpNode(complete = true) {
6547
6574
  const rowHeight = this.options.rowHeightPx;
6548
6575
  const gracePx = 1; // ignore subpixel scrolling
@@ -6575,24 +6602,19 @@ class Wunderbaum {
6575
6602
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
6576
6603
  return this._getNodeByRowIdx(bottomIdx);
6577
6604
  }
6578
- /** Return preceeding visible node in the viewport. */
6579
- _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);
6580
6609
  this.visitRows((n) => {
6581
6610
  node = n;
6582
- if (ofs-- <= 0) {
6611
+ if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) {
6583
6612
  return false;
6584
6613
  }
6585
- }, { reverse: true, start: node || this.getActiveNode() });
6586
- return node;
6587
- }
6588
- /** Return following visible node in the viewport. */
6589
- _getNextNodeInView(node, ofs = 1) {
6590
- this.visitRows((n) => {
6591
- node = n;
6592
6614
  if (ofs-- <= 0) {
6593
6615
  return false;
6594
6616
  }
6595
- }, { reverse: false, start: node || this.getActiveNode() });
6617
+ }, { reverse: reverse, start: node || this.getActiveNode() });
6596
6618
  return node;
6597
6619
  }
6598
6620
  /**
@@ -6712,9 +6734,11 @@ class Wunderbaum {
6712
6734
  case "first":
6713
6735
  case "last":
6714
6736
  case "left":
6737
+ case "nextMatch":
6715
6738
  case "pageDown":
6716
6739
  case "pageUp":
6717
6740
  case "parent":
6741
+ case "prevMatch":
6718
6742
  case "right":
6719
6743
  case "up":
6720
6744
  return node.navigate(cmd);
@@ -6906,6 +6930,11 @@ class Wunderbaum {
6906
6930
  count(visible = false) {
6907
6931
  return visible ? this.treeRowCount : this.keyMap.size;
6908
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
+ }
6909
6938
  /** @internal sanity check. */
6910
6939
  _check() {
6911
6940
  let i = 0;
@@ -6964,12 +6993,14 @@ class Wunderbaum {
6964
6993
  * and wrap-around at the end.
6965
6994
  * Used by quicksearch and keyboard navigation.
6966
6995
  */
6967
- findNextNode(match, startNode) {
6996
+ findNextNode(match, startNode, reverse = false) {
6968
6997
  //, visibleOnly) {
6969
6998
  let res = null;
6970
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;
6971
7002
  const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
6972
- startNode = startNode || firstNode;
7003
+ startNode = startNode || (reverse ? lastNode : firstNode);
6973
7004
  function _checkNode(n) {
6974
7005
  // console.log("_check " + n)
6975
7006
  if (matcher(n)) {
@@ -6982,12 +7013,14 @@ class Wunderbaum {
6982
7013
  this.visitRows(_checkNode, {
6983
7014
  start: startNode,
6984
7015
  includeSelf: false,
7016
+ reverse: reverse,
6985
7017
  });
6986
7018
  // Wrap around search
6987
7019
  if (!res && startNode !== firstNode) {
6988
7020
  this.visitRows(_checkNode, {
6989
- start: firstNode,
7021
+ start: reverse ? lastNode : firstNode,
6990
7022
  includeSelf: true,
7023
+ reverse: reverse,
6991
7024
  });
6992
7025
  }
6993
7026
  return res;
@@ -7054,7 +7087,7 @@ class Wunderbaum {
7054
7087
  // }
7055
7088
  break;
7056
7089
  case "up":
7057
- res = this._getPrevNodeInView(node);
7090
+ res = this._getNextNodeInView(node, { reverse: true });
7058
7091
  break;
7059
7092
  case "down":
7060
7093
  res = this._getNextNodeInView(node);
@@ -7067,7 +7100,10 @@ class Wunderbaum {
7067
7100
  res = bottomNode;
7068
7101
  }
7069
7102
  else {
7070
- res = this._getNextNodeInView(node, pageSize);
7103
+ res = this._getNextNodeInView(node, {
7104
+ reverse: false,
7105
+ ofs: pageSize,
7106
+ });
7071
7107
  }
7072
7108
  }
7073
7109
  break;
@@ -7082,10 +7118,23 @@ class Wunderbaum {
7082
7118
  res = topNode;
7083
7119
  }
7084
7120
  else {
7085
- res = this._getPrevNodeInView(node, pageSize);
7121
+ res = this._getNextNodeInView(node, {
7122
+ reverse: true,
7123
+ ofs: pageSize,
7124
+ });
7086
7125
  }
7087
7126
  }
7088
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;
7089
7138
  default:
7090
7139
  this.logWarn("Unknown relation '" + where + "'.");
7091
7140
  }
@@ -7147,6 +7196,12 @@ class Wunderbaum {
7147
7196
  getFirstChild() {
7148
7197
  return this.root.getFirstChild();
7149
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
+ }
7150
7205
  /**
7151
7206
  * Return the node that currently has keyboard focus or null.
7152
7207
  * Alias for {@link Wunderbaum.focusNode}.
@@ -7474,6 +7529,51 @@ class Wunderbaum {
7474
7529
  _setFocusNode(node) {
7475
7530
  this._focusNode = node;
7476
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
+ }
7477
7577
  update(change, node, options) {
7478
7578
  // this.log(`update(${change}) node=${node}`);
7479
7579
  if (!(node instanceof WunderbaumNode)) {
@@ -7755,11 +7855,11 @@ class Wunderbaum {
7755
7855
  // }
7756
7856
  return modified;
7757
7857
  }
7758
- _insertIcon(icon, elem) {
7759
- const iconElem = document.createElement("i");
7760
- iconElem.className = icon;
7761
- elem.appendChild(iconElem);
7762
- }
7858
+ // protected _insertIcon(icon: string, elem: HTMLElement) {
7859
+ // const iconElem = document.createElement("i");
7860
+ // iconElem.className = icon;
7861
+ // elem.appendChild(iconElem);
7862
+ // }
7763
7863
  /** Create/update header markup from `this.columns` definition.
7764
7864
  * @internal
7765
7865
  */
@@ -7857,6 +7957,104 @@ class Wunderbaum {
7857
7957
  this._updateViewportImmediately();
7858
7958
  }
7859
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
+ }
7860
8058
  /**
7861
8059
  * This is the actual update method, which is wrapped inside a throttle method.
7862
8060
  * It calls `updateColumns()` and `_updateRows()`.
@@ -7867,7 +8065,6 @@ class Wunderbaum {
7867
8065
  * @internal
7868
8066
  */
7869
8067
  _updateViewportImmediately() {
7870
- var _a;
7871
8068
  if (this._disableUpdateCount) {
7872
8069
  this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`);
7873
8070
  this._disableUpdateIgnoreCount++;
@@ -7914,11 +8111,8 @@ class Wunderbaum {
7914
8111
  this._updateRows();
7915
8112
  // console.profileEnd(`_updateViewportImmediately()`)
7916
8113
  }
7917
- if (this.options.connectTopBreadcrumb) {
7918
- assert(this.options.connectTopBreadcrumb.textContent != null, `Invalid 'connectTopBreadcrumb' option (input element expected).`);
7919
- let path = (_a = this.getTopmostVpNode(true)) === null || _a === void 0 ? void 0 : _a.getPath(false, "title", " > ");
7920
- path = path ? path + " >" : "";
7921
- this.options.connectTopBreadcrumb.textContent = path;
8114
+ if (this.breadcrumb) {
8115
+ this._updateTopBreadcrumb();
7922
8116
  }
7923
8117
  this._callEvent("update");
7924
8118
  }
@@ -7984,8 +8178,9 @@ class Wunderbaum {
7984
8178
  // this.debug("render", opts);
7985
8179
  const obsoleteNodes = new Set();
7986
8180
  this.nodeListElement.childNodes.forEach((elem) => {
7987
- const tr = elem;
7988
- obsoleteNodes.add(tr._wb_node);
8181
+ if (elem._wb_node) {
8182
+ obsoleteNodes.add(elem._wb_node);
8183
+ }
7989
8184
  });
7990
8185
  let idx = 0;
7991
8186
  let top = 0;
@@ -8286,7 +8481,7 @@ class Wunderbaum {
8286
8481
  }
8287
8482
  Wunderbaum.sequence = 0;
8288
8483
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
8289
- Wunderbaum.version = "v0.12.0"; // Set to semver by 'grunt release'
8484
+ Wunderbaum.version = "v0.13.0"; // Set to semver by 'grunt release'
8290
8485
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
8291
8486
  Wunderbaum.util = util;
8292
8487