wunderbaum 0.7.0 → 0.8.1

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.
package/src/wunderbaum.ts CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  DynamicIconOption,
52
52
  DynamicStringOption,
53
53
  DynamicBoolOption,
54
+ SetColumnOptions,
54
55
  } from "./types";
55
56
  import {
56
57
  DEFAULT_DEBUGLEVEL,
@@ -64,6 +65,7 @@ import { WunderbaumNode } from "./wb_node";
64
65
  import { Deferred } from "./deferred";
65
66
  import { EditExtension } from "./wb_ext_edit";
66
67
  import { WunderbaumOptions } from "./wb_options";
68
+ import { DebouncedFunction } from "./debounce";
67
69
 
68
70
  class WbSystemRoot extends WunderbaumNode {
69
71
  constructor(tree: Wunderbaum) {
@@ -80,7 +82,7 @@ class WbSystemRoot extends WunderbaumNode {
80
82
  /**
81
83
  * A persistent plain object or array.
82
84
  *
83
- * See also [[WunderbaumOptions]].
85
+ * See also {@link WunderbaumOptions}.
84
86
  */
85
87
  export class Wunderbaum {
86
88
  protected static sequence = 0;
@@ -104,7 +106,7 @@ export class Wunderbaum {
104
106
  /** Contains additional data that was sent as response to an Ajax source load request. */
105
107
  public readonly data: { [key: string]: any } = {};
106
108
 
107
- protected readonly _updateViewportThrottled: (...args: any) => void;
109
+ protected readonly _updateViewportThrottled: DebouncedFunction<() => void>;
108
110
  protected extensionList: WunderbaumExtension<any>[] = [];
109
111
  protected extensions: ExtensionsDict = {};
110
112
 
@@ -117,10 +119,23 @@ export class Wunderbaum {
117
119
  protected _disableUpdateCount = 0;
118
120
  protected _disableUpdateIgnoreCount = 0;
119
121
 
120
- /** Currently active node if any. */
121
- public activeNode: WunderbaumNode | null = null;
122
- /** Current node hat has keyboard focus if any. */
123
- public focusNode: WunderbaumNode | null = null;
122
+ protected _activeNode: WunderbaumNode | null = null;
123
+ protected _focusNode: WunderbaumNode | null = null;
124
+
125
+ /** Currently active node if any.
126
+ * Use @link {WunderbaumNode.setActive|setActive} to modify.
127
+ */
128
+ public get activeNode() {
129
+ // Check for deleted node, i.e. node.tree === null
130
+ return this._activeNode?.tree ? this._activeNode : null;
131
+ }
132
+ /** Current node hat has keyboard focus if any.
133
+ * Use @link {WunderbaumNode.setFocus|setFocus()} to modify.
134
+ */
135
+ public get focusNode() {
136
+ // Check for deleted node, i.e. node.tree === null
137
+ return this._focusNode?.tree ? this._focusNode : null;
138
+ }
124
139
 
125
140
  /** Shared properties, referenced by `node.type`. */
126
141
  public types: NodeTypeDefinitionMap = {};
@@ -153,7 +168,7 @@ export class Wunderbaum {
153
168
  public filterMode: FilterModeType = null;
154
169
 
155
170
  // --- KEYNAV ---
156
- /** @internal Use `setColumn()`/`getActiveColElem()`*/
171
+ /** @internal Use `setColumn()`/`getActiveColElem()` to access. */
157
172
  public activeColIdx = 0;
158
173
  /** @internal */
159
174
  public _cellNavMode = false;
@@ -366,6 +381,7 @@ export class Wunderbaum {
366
381
  } else {
367
382
  this.setNavigationOption(opts.navigationModeOption);
368
383
  }
384
+ this.update(ChangeType.structure, { immediate: true });
369
385
  readyDeferred.resolve();
370
386
  })
371
387
  .catch((error) => {
@@ -426,7 +442,7 @@ export class Wunderbaum {
426
442
  node.isActive() &&
427
443
  (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)
428
444
  ) {
429
- this._callMethod("edit.startEditTitle", node);
445
+ node.startEditTitle();
430
446
  }
431
447
 
432
448
  if (info.colIdx >= 0) {
@@ -482,7 +498,7 @@ export class Wunderbaum {
482
498
 
483
499
  this._callEvent("focus", { flag: flag, event: e });
484
500
 
485
- if (flag && this.isRowNav() && !this.isEditing()) {
501
+ if (flag && this.isRowNav() && !this.isEditingTitle()) {
486
502
  if ((opts.navigationModeOption as NavModeEnum) === NavModeEnum.row) {
487
503
  targetNode?.setActive();
488
504
  } else {
@@ -506,7 +522,7 @@ export class Wunderbaum {
506
522
  * getTree(1); // Get second Wunderbaum instance on page
507
523
  * getTree(event); // Get tree for this mouse- or keyboard event
508
524
  * getTree("foo"); // Get tree for this `tree.options.id`
509
- * getTree("#tree"); // Get tree for this matching element
525
+ * getTree("#tree"); // Get tree for first matching element selector
510
526
  * ```
511
527
  */
512
528
  public static getTree(
@@ -614,37 +630,37 @@ export class Wunderbaum {
614
630
  /** Add node to tree's bookkeeping data structures. */
615
631
  _registerNode(node: WunderbaumNode): void {
616
632
  const key = node.key;
617
- util.assert(
618
- key != null && !this.keyMap.has(key),
619
- `Missing or duplicate key: '${key}'.`
620
- );
633
+ util.assert(key != null, `Missing key: '${node}'.`);
634
+ util.assert(!this.keyMap.has(key), `Duplicate key: '${key}': ${node}.`);
621
635
  this.keyMap.set(key, node);
622
636
  const rk = node.refKey;
623
- if (rk) {
637
+ if (rk != null) {
624
638
  const rks = this.refKeyMap.get(rk); // Set of nodes with this refKey
625
639
  if (rks) {
626
640
  rks.add(node);
627
641
  } else {
628
- this.refKeyMap.set(rk, new Set());
642
+ this.refKeyMap.set(rk, new Set([node]));
629
643
  }
630
644
  }
631
645
  }
632
646
 
633
647
  /** Remove node from tree's bookkeeping data structures. */
634
648
  _unregisterNode(node: WunderbaumNode): void {
649
+ // Remove refKey reference from map (if any)
635
650
  const rk = node.refKey;
636
- if (rk) {
651
+ if (rk != null) {
637
652
  const rks = this.refKeyMap.get(rk);
638
653
  if (rks && rks.delete(node) && !rks.size) {
639
654
  // We just removed the last element
640
655
  this.refKeyMap.delete(rk);
641
656
  }
642
657
  }
643
- // mark as disposed
658
+ // Remove key reference from map
659
+ this.keyMap.delete(node.key);
660
+ // Mark as disposed
644
661
  (node.tree as any) = null;
645
662
  (node.parent as any) = null;
646
- // node.title = "DISPOSED: " + node.title
647
- // this.viewNodes.delete(node);
663
+ // Remove HTML markup
648
664
  node.removeMarkup();
649
665
  }
650
666
 
@@ -894,7 +910,7 @@ export class Wunderbaum {
894
910
  this._callMethod("edit.createNode", "after");
895
911
  break;
896
912
  case "rename":
897
- this._callMethod("edit.startEditTitle");
913
+ node.startEditTitle();
898
914
  break;
899
915
  // Simple clipboard simulation:
900
916
  // case "cut":
@@ -942,10 +958,9 @@ export class Wunderbaum {
942
958
  this.root.children = null;
943
959
  this.keyMap.clear();
944
960
  this.refKeyMap.clear();
945
- // this.viewNodes.clear();
946
961
  this.treeRowCount = 0;
947
- this.activeNode = null;
948
- this.focusNode = null;
962
+ this._activeNode = null;
963
+ this._focusNode = null;
949
964
 
950
965
  // this.types = {};
951
966
  // this. columns =[];
@@ -1159,15 +1174,33 @@ export class Wunderbaum {
1159
1174
  /**
1160
1175
  * Find all nodes that match condition.
1161
1176
  *
1177
+ * @param match title string to search for, or a
1178
+ * callback function that returns `true` if a node is matched.
1162
1179
  * @see {@link WunderbaumNode.findAll}
1163
1180
  */
1164
1181
  findAll(match: string | RegExp | MatcherCallback) {
1165
1182
  return this.root.findAll(match);
1166
1183
  }
1167
1184
 
1185
+ /**
1186
+ * Find all nodes with a given _refKey_ (aka a list of clones).
1187
+ *
1188
+ * @param refKey a `node.refKey` value to search for.
1189
+ * @returns an array of matching nodes with at least two element or `[]`
1190
+ * if nothing found.
1191
+ *
1192
+ * @see {@link WunderbaumNode.getCloneList}
1193
+ */
1194
+ findByRefKey(refKey: string): WunderbaumNode[] {
1195
+ const clones = this.refKeyMap.get(refKey);
1196
+ return clones ? Array.from(clones) : [];
1197
+ }
1198
+
1168
1199
  /**
1169
1200
  * Find first node that matches condition.
1170
1201
  *
1202
+ * @param match title string to search for, or a
1203
+ * callback function that returns `true` if a node is matched.
1171
1204
  * @see {@link WunderbaumNode.findFirst}
1172
1205
  */
1173
1206
  findFirst(match: string | RegExp | MatcherCallback) {
@@ -1177,8 +1210,6 @@ export class Wunderbaum {
1177
1210
  /**
1178
1211
  * Find first node that matches condition.
1179
1212
  *
1180
- * @param match title string to search for, or a
1181
- * callback function that returns `true` if a node is matched.
1182
1213
  * @see {@link WunderbaumNode.findFirst}
1183
1214
  *
1184
1215
  */
@@ -1189,6 +1220,7 @@ export class Wunderbaum {
1189
1220
  /**
1190
1221
  * Find the next visible node that starts with `match`, starting at `startNode`
1191
1222
  * and wrap-around at the end.
1223
+ * Used by quicksearch and keyboard navigation.
1192
1224
  */
1193
1225
  findNextNode(
1194
1226
  match: string | MatcherCallback,
@@ -1371,7 +1403,13 @@ export class Wunderbaum {
1371
1403
  }
1372
1404
 
1373
1405
  /**
1374
- * Return the currently active node or null.
1406
+ * Return the currently active node or null (alias for `tree.activeNode`).
1407
+ * Alias for {@link Wunderbaum.activeNode}.
1408
+ *
1409
+ * @see {@link WunderbaumNode.setActive}
1410
+ * @see {@link WunderbaumNode.isActive}
1411
+ * @see {@link Wunderbaum.activeNode}
1412
+ * @see {@link Wunderbaum.focusNode}
1375
1413
  */
1376
1414
  getActiveNode() {
1377
1415
  return this.activeNode;
@@ -1385,7 +1423,12 @@ export class Wunderbaum {
1385
1423
  }
1386
1424
 
1387
1425
  /**
1388
- * Return the currently active node or null.
1426
+ * Return the node that currently has keyboard focus or null.
1427
+ * Alias for {@link Wunderbaum.focusNode}.
1428
+ * @see {@link WunderbaumNode.setFocus}
1429
+ * @see {@link WunderbaumNode.hasFocus}
1430
+ * @see {@link Wunderbaum.activeNode}
1431
+ * @see {@link Wunderbaum.focusNode}
1389
1432
  */
1390
1433
  getFocusNode() {
1391
1434
  return this.focusNode;
@@ -1462,8 +1505,22 @@ export class Wunderbaum {
1462
1505
  return `Wunderbaum<'${this.id}'>`;
1463
1506
  }
1464
1507
 
1465
- /** Return true if any node is currently in edit-title mode. */
1508
+ /** Return true if any node title or grid cell is currently beeing edited.
1509
+ *
1510
+ * See also {@link Wunderbaum.isEditingTitle}.
1511
+ */
1466
1512
  isEditing(): boolean {
1513
+ const focusElem = this.nodeListElement.querySelector(
1514
+ "input:focus,select:focus"
1515
+ );
1516
+ return !!focusElem;
1517
+ }
1518
+
1519
+ /** Return true if any node is currently in edit-title mode.
1520
+ *
1521
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
1522
+ */
1523
+ isEditingTitle(): boolean {
1467
1524
  return this._callMethod("edit.isEditingTitle");
1468
1525
  }
1469
1526
 
@@ -1483,26 +1540,33 @@ export class Wunderbaum {
1483
1540
  return res;
1484
1541
  }
1485
1542
 
1486
- /** Alias for {@link Wunderbaum.logDebug}.
1487
- * @alias Wunderbaum.logDebug
1543
+ /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
1544
+ * @see {@link Wunderbaum.logDebug}
1488
1545
  */
1489
- log = this.logDebug;
1546
+ log(...args: any[]) {
1547
+ if (this.options.debugLevel! >= 4) {
1548
+ console.log(this.toString(), ...args); // eslint-disable-line no-console
1549
+ }
1550
+ }
1490
1551
 
1491
- /** Log to console if opts.debugLevel >= 4 */
1552
+ /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
1553
+ * and browser console level includes debug/verbose messages.
1554
+ * @see {@link Wunderbaum.log}
1555
+ */
1492
1556
  logDebug(...args: any[]) {
1493
1557
  if (this.options.debugLevel! >= 4) {
1494
- console.log(this.toString(), ...args); // eslint-disable-line no-console
1558
+ console.debug(this.toString(), ...args); // eslint-disable-line no-console
1495
1559
  }
1496
1560
  }
1497
1561
 
1498
- /** Log error to console. */
1562
+ /** Write to `console.error` with tree name as prefix. */
1499
1563
  logError(...args: any[]) {
1500
1564
  if (this.options.debugLevel! >= 1) {
1501
1565
  console.error(this.toString(), ...args); // eslint-disable-line no-console
1502
1566
  }
1503
1567
  }
1504
1568
 
1505
- /** Log to console if opts.debugLevel >= 3 */
1569
+ /** Write to `console.info` with tree name as prefix if opts.debugLevel >= 3. */
1506
1570
  logInfo(...args: any[]) {
1507
1571
  if (this.options.debugLevel! >= 3) {
1508
1572
  console.info(this.toString(), ...args); // eslint-disable-line no-console
@@ -1524,7 +1588,7 @@ export class Wunderbaum {
1524
1588
  }
1525
1589
  }
1526
1590
 
1527
- /** Log to console if opts.debugLevel >= 2 */
1591
+ /** Write to `console.warn` with tree name as prefix with if opts.debugLevel >= 2. */
1528
1592
  logWarn(...args: any[]) {
1529
1593
  if (this.options.debugLevel! >= 2) {
1530
1594
  console.warn(this.toString(), ...args); // eslint-disable-line no-console
@@ -1622,11 +1686,24 @@ export class Wunderbaum {
1622
1686
  /**
1623
1687
  * Set column #colIdx to 'active'.
1624
1688
  *
1625
- * This higlights the column header and -cells by adding the `wb-active` class.
1689
+ * This higlights the column header and -cells by adding the `wb-active`
1690
+ * class to all grid cells of the active column. <br>
1626
1691
  * Available in cell-nav mode only.
1692
+ *
1693
+ * If _options.edit_ is true, the embedded input element is focused, or if
1694
+ * colIdx is 0, the node title is put into edit mode.
1627
1695
  */
1628
- setColumn(colIdx: number) {
1629
- util.assert(this.isCellNav(), "Exected cellNav mode");
1696
+ setColumn(colIdx: number | string, options?: SetColumnOptions) {
1697
+ const edit = options?.edit;
1698
+ const scroll = options?.scrollIntoView !== false;
1699
+
1700
+ util.assert(this.isCellNav(), "Expected cellNav mode");
1701
+
1702
+ if (typeof colIdx === "string") {
1703
+ const cid = colIdx;
1704
+ colIdx = this.columns.findIndex((c) => c.id === colIdx);
1705
+ util.assert(colIdx >= 0, `Invalid colId: ${cid}`);
1706
+ }
1630
1707
  util.assert(
1631
1708
  0 <= colIdx && colIdx < this.columns.length,
1632
1709
  `Invalid colIdx: ${colIdx}`
@@ -1652,18 +1729,34 @@ export class Wunderbaum {
1652
1729
  (colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
1653
1730
  }
1654
1731
  }
1655
- // Vertical scroll into view
1656
- // if (this.options.fixedCol) {
1657
- this.scrollToHorz();
1658
- // }
1732
+ // Horizontically scroll into view
1733
+ if (scroll || edit) {
1734
+ this.scrollToHorz();
1735
+ }
1736
+
1737
+ if (edit && this.activeNode) {
1738
+ // this.activeNode.setFocus(); // Blur prev. input if any
1739
+ if (colIdx === 0) {
1740
+ this.activeNode.startEditTitle();
1741
+ } else {
1742
+ this.getActiveColElem()
1743
+ ?.querySelector<HTMLInputElement>("input,select")
1744
+ ?.focus();
1745
+ }
1746
+ }
1747
+ }
1748
+
1749
+ /* Set or remove keyboard focus to the tree container. @internal */
1750
+ _setActiveNode(node: WunderbaumNode | null) {
1751
+ this._activeNode = node;
1659
1752
  }
1660
1753
 
1661
- /** Set or remove keybaord focus to the tree container. */
1754
+ /** Set or remove keyboard focus to the tree container. */
1662
1755
  setActiveNode(key: string, flag: boolean = true, options?: SetActiveOptions) {
1663
1756
  this.findKey(key)?.setActive(flag, options);
1664
1757
  }
1665
1758
 
1666
- /** Set or remove keybaord focus to the tree container. */
1759
+ /** Set or remove keyboard focus to the tree container. */
1667
1760
  setFocus(flag = true) {
1668
1761
  if (flag) {
1669
1762
  this.element.focus();
@@ -1672,10 +1765,16 @@ export class Wunderbaum {
1672
1765
  }
1673
1766
  }
1674
1767
 
1768
+ /* Set or remove keyboard focus to the tree container. @internal */
1769
+ _setFocusNode(node: WunderbaumNode | null) {
1770
+ this._focusNode = node;
1771
+ }
1772
+
1675
1773
  /**
1676
1774
  * Schedule an update request to reflect a tree change.
1677
1775
  * The render operation is async and debounced unless the `immediate` option
1678
1776
  * is set.
1777
+ *
1679
1778
  * Use {@link WunderbaumNode.update()} if only a single node has changed,
1680
1779
  * or {@link WunderbaumNode._render()}) to pass special options.
1681
1780
  */
@@ -1694,26 +1793,27 @@ export class Wunderbaum {
1694
1793
 
1695
1794
  update(
1696
1795
  change: ChangeType,
1697
- node?: WunderbaumNode | any,
1796
+ node?: WunderbaumNode | UpdateOptions,
1698
1797
  options?: UpdateOptions
1699
1798
  ): void {
1700
- if (this._disableUpdateCount) {
1701
- // Assuming that we redraw all when enableUpdate() is re-enabled.
1702
- // this.log(
1703
- // `IGNORED update(${change}) node=${node} (disable level ${this._disableUpdateCount})`
1704
- // );
1705
- this._disableUpdateIgnoreCount++;
1706
- return;
1707
- }
1708
1799
  // this.log(`update(${change}) node=${node}`);
1709
1800
  if (!(node instanceof WunderbaumNode)) {
1710
1801
  options = node;
1711
- node = null;
1802
+ node = undefined;
1712
1803
  }
1804
+
1713
1805
  const immediate = !!util.getOption(options, "immediate");
1714
1806
  const RF = RenderFlag;
1715
1807
  const pending = this.pendingChangeTypes;
1716
1808
 
1809
+ if (this._disableUpdateCount) {
1810
+ // Assuming that we redraw all when enableUpdate() is re-enabled.
1811
+ // this.log(
1812
+ // `IGNORED update(${change}) node=${node} (disable level ${this._disableUpdateCount})`
1813
+ // );
1814
+ this._disableUpdateIgnoreCount++;
1815
+ return;
1816
+ }
1717
1817
  switch (change) {
1718
1818
  case ChangeType.any:
1719
1819
  case ChangeType.colStructure:
@@ -1739,8 +1839,8 @@ export class Wunderbaum {
1739
1839
  util.assert(node, `Option '${change}' requires a node.`);
1740
1840
  // Single nodes are immediately updated if already inside the viewport
1741
1841
  // (otherwise we can ignore)
1742
- if (node._rowElem) {
1743
- node._render({ change: change });
1842
+ if ((<WunderbaumNode>node)!._rowElem) {
1843
+ (<WunderbaumNode>node)!._render({ change: change });
1744
1844
  }
1745
1845
  break;
1746
1846
  default:
@@ -2069,12 +2169,15 @@ export class Wunderbaum {
2069
2169
  protected _updateViewportImmediately() {
2070
2170
  if (this._disableUpdateCount) {
2071
2171
  this.log(
2072
- `_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount})`
2172
+ `_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`
2073
2173
  );
2074
2174
  this._disableUpdateIgnoreCount++;
2075
2175
  return;
2076
2176
  }
2077
-
2177
+ if (this._updateViewportThrottled.pending()) {
2178
+ // this.logWarn(`_updateViewportImmediately() cancel pending timer.`);
2179
+ this._updateViewportThrottled.cancel();
2180
+ }
2078
2181
  // Shorten container height to avoid v-scrollbar
2079
2182
  const FIX_ADJUST_HEIGHT = 1;
2080
2183
  const RF = RenderFlag;
@@ -2189,9 +2292,6 @@ export class Wunderbaum {
2189
2292
  let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
2190
2293
  endIdx = Math.ceil(endIdx);
2191
2294
 
2192
- // const obsoleteViewNodes = this.viewNodes;
2193
- // this.viewNodes = new Set();
2194
- // const viewNodes = this.viewNodes;
2195
2295
  // this.debug("render", opts);
2196
2296
  const obsoleteNodes = new Set<WunderbaumNode>();
2197
2297
  this.nodeListElement.childNodes.forEach((elem) => {
@@ -2485,7 +2585,7 @@ export class Wunderbaum {
2485
2585
  * FILTER
2486
2586
  * -------------------------------------------------------------------------*/
2487
2587
  /**
2488
- * [ext-filter] Dim or hide nodes.
2588
+ * Dim or hide nodes.
2489
2589
  */
2490
2590
  filterNodes(
2491
2591
  filter: string | NodeFilterCallback,
@@ -2498,7 +2598,7 @@ export class Wunderbaum {
2498
2598
  }
2499
2599
 
2500
2600
  /**
2501
- * [ext-filter] Dim or hide whole branches.
2601
+ * Dim or hide whole branches.
2502
2602
  */
2503
2603
  filterBranches(
2504
2604
  filter: string | NodeFilterCallback,
@@ -2511,25 +2611,19 @@ export class Wunderbaum {
2511
2611
  }
2512
2612
 
2513
2613
  /**
2514
- * [ext-filter] Reset the filter.
2515
- *
2516
- * @requires [[FilterExtension]]
2614
+ * Reset the filter.
2517
2615
  */
2518
2616
  clearFilter() {
2519
2617
  return (this.extensions.filter as FilterExtension).clearFilter();
2520
2618
  }
2521
2619
  /**
2522
- * [ext-filter] Return true if a filter is currently applied.
2523
- *
2524
- * @requires [[FilterExtension]]
2620
+ * Return true if a filter is currently applied.
2525
2621
  */
2526
2622
  isFilterActive() {
2527
2623
  return !!this.filterMode;
2528
2624
  }
2529
2625
  /**
2530
- * [ext-filter] Re-apply current filter.
2531
- *
2532
- * @requires [[FilterExtension]]
2626
+ * Re-apply current filter.
2533
2627
  */
2534
2628
  updateFilter() {
2535
2629
  return (this.extensions.filter as FilterExtension).updateFilter();