wunderbaum 0.3.4 → 0.4.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.
package/src/wb_node.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
7
- import "./wunderbaum.scss";
8
7
  import * as util from "./util";
9
8
 
10
9
  import { Wunderbaum } from "./wunderbaum";
@@ -33,6 +32,7 @@ import {
33
32
  SortCallback,
34
33
  NodeToDictCallback,
35
34
  WbNodeData,
35
+ TristateType,
36
36
  } from "./types";
37
37
  import {
38
38
  iconMap,
@@ -48,31 +48,18 @@ import {
48
48
  } from "./common";
49
49
  import { Deferred } from "./deferred";
50
50
 
51
- /** Top-level properties that can be passed with `data`. */
51
+ /** WunderbaumNode properties that can be passed with source data.
52
+ * (Any other source properties will be stored as `node.data.PROP`.)
53
+ */
52
54
  const NODE_PROPS = new Set<string>([
53
- // TODO: use NODE_ATTRS instead?
54
- "classes",
55
- "expanded",
56
- "icon",
57
- "key",
58
- "lazy",
59
- "refKey",
60
- "selected",
61
- "title",
62
- "tooltip",
63
- "type",
64
- ]);
65
-
66
- const NODE_ATTRS = new Set<string>([
67
55
  "checkbox",
68
- "expanded",
69
56
  "classes",
70
- "folder",
57
+ "expanded",
71
58
  "icon",
72
59
  "iconTooltip",
73
60
  "key",
74
61
  "lazy",
75
- "partsel",
62
+ "_partsel",
76
63
  "radiogroup",
77
64
  "refKey",
78
65
  "selected",
@@ -81,9 +68,16 @@ const NODE_ATTRS = new Set<string>([
81
68
  "tooltip",
82
69
  "type",
83
70
  "unselectable",
84
- "unselectableIgnore",
85
- "unselectableStatus",
71
+ // "unselectableIgnore",
72
+ // "unselectableStatus",
86
73
  ]);
74
+ /** WunderbaumNode properties that will be returned by `node.toDict()`.)
75
+ */
76
+ const NODE_DICT_PROPS = new Set<string>(NODE_PROPS);
77
+ NODE_DICT_PROPS.delete("_partsel");
78
+ NODE_DICT_PROPS.delete("unselectable");
79
+ // NODE_DICT_PROPS.delete("unselectableIgnore");
80
+ // NODE_DICT_PROPS.delete("unselectableStatus");
87
81
 
88
82
  /**
89
83
  * A single tree node.
@@ -112,6 +106,7 @@ export class WunderbaumNode {
112
106
  public readonly refKey: string | undefined = undefined;
113
107
  public children: WunderbaumNode[] | null = null;
114
108
  public checkbox?: boolean;
109
+ public radiogroup?: boolean;
115
110
  /** If true, (in grid mode) no cells are rendered, except for the node title.*/
116
111
  public colspan?: boolean;
117
112
  public icon?: boolean | string;
@@ -120,8 +115,11 @@ export class WunderbaumNode {
120
115
  * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */
121
116
  public expanded: boolean = false;
122
117
  /** Selection state.
123
- * @see {@link isSelected}, {@link setSelected}. */
118
+ * @see {@link isSelected}, {@link setSelected}, {@link toggleSelected}. */
124
119
  public selected: boolean = false;
120
+ public unselectable?: boolean;
121
+ // public unselectableStatus?: boolean;
122
+ // public unselectableIgnore?: boolean;
125
123
  public type?: string;
126
124
  public tooltip?: string;
127
125
  /** Additional classes added to `div.wb-row`.
@@ -150,22 +148,32 @@ export class WunderbaumNode {
150
148
  constructor(tree: Wunderbaum, parent: WunderbaumNode, data: any) {
151
149
  util.assert(!parent || parent.tree === tree);
152
150
  util.assert(!data.children);
151
+
153
152
  this.tree = tree;
154
153
  this.parent = parent;
154
+
155
155
  this.key = "" + (data.key ?? ++WunderbaumNode.sequence);
156
156
  this.title = "" + (data.title ?? "<" + this.key + ">");
157
-
158
157
  data.refKey != null ? (this.refKey = "" + data.refKey) : 0;
159
- data.statusNodeType != null
160
- ? (this.statusNodeType = "" + data.statusNodeType)
161
- : 0;
162
158
  data.type != null ? (this.type = "" + data.type) : 0;
163
- data.checkbox != null ? (this.checkbox = !!data.checkbox) : 0;
164
- data.colspan != null ? (this.colspan = !!data.colspan) : 0;
165
159
  this.expanded = data.expanded === true;
166
160
  data.icon != null ? (this.icon = data.icon) : 0;
167
161
  this.lazy = data.lazy === true;
162
+ data.statusNodeType != null
163
+ ? (this.statusNodeType = "" + data.statusNodeType)
164
+ : 0;
165
+ data.colspan != null ? (this.colspan = !!data.colspan) : 0;
166
+
167
+ // Selection
168
+ data.checkbox != null ? (this.checkbox = !!data.checkbox) : 0;
169
+ data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0;
168
170
  this.selected = data.selected === true;
171
+ data.unselectable === true ? (this.unselectable = true) : 0;
172
+ // data.unselectableStatus != null
173
+ // ? (this.unselectableStatus = !!data.unselectableStatus)
174
+ // : 0;
175
+ // data.unselectableIgnore === true ? (this.unselectableIgnore = true) : 0;
176
+
169
177
  if (data.classes) {
170
178
  this.setClass(data.classes);
171
179
  }
@@ -311,13 +319,16 @@ export class WunderbaumNode {
311
319
  // insert nodeList after children[pos]
312
320
  this.children.splice(pos, 0, ...nodeList);
313
321
  }
314
- // TODO:
315
- // if (tree.options.selectMode === 3) {
316
- // this.fixSelection3FromEndNodes();
317
- // }
318
322
  // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
319
- tree.setModified(ChangeType.structure);
323
+ tree.update(ChangeType.structure);
320
324
  } finally {
325
+ // if (tree.options.selectMode === "hier") {
326
+ // if (this.parent && this.parent.children) {
327
+ // this.fixSelection3FromEndNodes();
328
+ // } else {
329
+ // // my happen when loading __root__;
330
+ // }
331
+ // }
321
332
  tree.enableUpdate(true);
322
333
  }
323
334
  // if(isTopCall && loadLazy){
@@ -369,6 +380,18 @@ export class WunderbaumNode {
369
380
  return this.tree.applyCommand(cmd, this, options);
370
381
  }
371
382
 
383
+ /**
384
+ * Collapse all expanded sibling nodes if any.
385
+ * (Automatically called when `autoCollapse` is true.)
386
+ */
387
+ collapseSiblings(options?: SetExpandedOptions): any {
388
+ for (let node of this.parent.children!) {
389
+ if (node !== this && node.expanded) {
390
+ node.setExpanded(false, options);
391
+ }
392
+ }
393
+ }
394
+
372
395
  /**
373
396
  * Add/remove one or more classes to `<div class='wb-row'>`.
374
397
  *
@@ -410,7 +433,7 @@ export class WunderbaumNode {
410
433
  const minExpandLevel = this.tree.options.minExpandLevel;
411
434
  let { depth = 99, loadLazy, force } = options ?? {};
412
435
 
413
- const expand_opts = {
436
+ const expandOpts = {
414
437
  scrollIntoView: false,
415
438
  force: force,
416
439
  loadLazy: loadLazy,
@@ -434,7 +457,7 @@ export class WunderbaumNode {
434
457
  // Node is collapsed and may be expanded (i.e. has children or is lazy)
435
458
  // Expanding may be async, so we store the promise.
436
459
  // Also the recursion is delayed until expansion finished.
437
- const p = cn.setExpanded(true, expand_opts);
460
+ const p = cn.setExpanded(true, expandOpts);
438
461
  promises.push(p);
439
462
  p.then(async () => {
440
463
  await _iter(cn, level_1);
@@ -448,7 +471,7 @@ export class WunderbaumNode {
448
471
  // Collapsing is always synchronous, so no promises required
449
472
  if (!minExpandLevel || force || cn.getLevel() > minExpandLevel) {
450
473
  // Do not collapse until minExpandLevel
451
- cn.setExpanded(false, expand_opts);
474
+ cn.setExpanded(false, expandOpts);
452
475
  }
453
476
  _iter(cn, level_1); // recursion, even if cn was already collapsed
454
477
  }
@@ -874,9 +897,11 @@ export class WunderbaumNode {
874
897
  return this.tree.root === this;
875
898
  }
876
899
 
877
- /** Return true if this node is selected, i.e. the checkbox is set. */
878
- isSelected(): boolean {
879
- return !!this.selected;
900
+ /** Return true if this node is selected, i.e. the checkbox is set.
901
+ * `undefined` if partly selected (tri-state), false otherwise.
902
+ */
903
+ isSelected(): TristateType {
904
+ return this.selected ? true : this._partsel ? undefined : false;
880
905
  }
881
906
 
882
907
  /** Return true if this node is a temporarily generated system node like
@@ -968,7 +993,7 @@ export class WunderbaumNode {
968
993
  tree.logInfo("Redefine columns", source.columns);
969
994
  tree.columns = source.columns;
970
995
  delete source.columns;
971
- tree.setModified(ChangeType.colStructure);
996
+ tree.update(ChangeType.colStructure);
972
997
  }
973
998
 
974
999
  this.addChildren(source.children);
@@ -980,6 +1005,9 @@ export class WunderbaumNode {
980
1005
  tree.logDebug(`Add source.${key} to tree.data.${key}`);
981
1006
  }
982
1007
  }
1008
+ if (tree.options.selectMode === "hier") {
1009
+ this.fixSelection3FromEndNodes();
1010
+ }
983
1011
 
984
1012
  this._callEvent("load");
985
1013
  }
@@ -1131,9 +1159,9 @@ export class WunderbaumNode {
1131
1159
 
1132
1160
  if (wasExpanded) {
1133
1161
  this.expanded = true;
1134
- this.tree.setModified(ChangeType.structure);
1162
+ this.tree.update(ChangeType.structure);
1135
1163
  } else {
1136
- this.setModified(); // Fix expander icon to 'loaded'
1164
+ this.update(); // Fix expander icon to 'loaded'
1137
1165
  }
1138
1166
  } catch (e) {
1139
1167
  this.logError("Error during loadLazy()", e);
@@ -1306,7 +1334,7 @@ export class WunderbaumNode {
1306
1334
  // Fix node.tree for all source nodes
1307
1335
  // util.assert(false, "Cross-tree move is not yet implemented.");
1308
1336
  this.logWarn("Cross-tree moveTo is experimental!");
1309
- this.visit(function (n) {
1337
+ this.visit((n) => {
1310
1338
  // TODO: fix selection state and activation, ...
1311
1339
  n.tree = targetNode.tree;
1312
1340
  }, true);
@@ -1315,7 +1343,7 @@ export class WunderbaumNode {
1315
1343
  // DragAndDrop to generate a dragend event on the source node
1316
1344
  setTimeout(() => {
1317
1345
  // Even indentation may have changed:
1318
- tree.setModified(ChangeType.any);
1346
+ tree.update(ChangeType.any);
1319
1347
  }, 0);
1320
1348
  // TODO: fix selection state
1321
1349
  // TODO: fix active state
@@ -1364,7 +1392,7 @@ export class WunderbaumNode {
1364
1392
  n.removeMarkup();
1365
1393
  tree._unregisterNode(n);
1366
1394
  }, true);
1367
- tree.setModified(ChangeType.structure);
1395
+ tree.update(ChangeType.structure);
1368
1396
  }
1369
1397
 
1370
1398
  /** Remove all descendants of this node. */
@@ -1397,7 +1425,7 @@ export class WunderbaumNode {
1397
1425
  if (!this.isRootNode()) {
1398
1426
  this.expanded = false;
1399
1427
  }
1400
- this.tree.setModified(ChangeType.structure);
1428
+ this.tree.update(ChangeType.structure);
1401
1429
  }
1402
1430
 
1403
1431
  /** Remove all HTML markup from the DOM. */
@@ -1498,12 +1526,13 @@ export class WunderbaumNode {
1498
1526
 
1499
1527
  /**
1500
1528
  * Create a whole new `<div class="wb-row">` element.
1501
- * @see {@link WunderbaumNode.render}
1529
+ * @see {@link WunderbaumNode._render}
1502
1530
  */
1503
1531
  protected _render_markup(opts: RenderOptions) {
1504
1532
  const tree = this.tree;
1505
1533
  const treeOptions = tree.options;
1506
- const checkbox = this.getOption("checkbox") !== false;
1534
+ const checkbox = this.getOption("checkbox");
1535
+ // const checkbox = this.getOption("checkbox") !== false;
1507
1536
  const columns = tree.columns;
1508
1537
  const level = this.getLevel();
1509
1538
  let elem: HTMLElement;
@@ -1542,6 +1571,9 @@ export class WunderbaumNode {
1542
1571
  if (checkbox) {
1543
1572
  checkboxSpan = document.createElement("i");
1544
1573
  checkboxSpan.classList.add("wb-checkbox");
1574
+ if (checkbox === "radio" || this.parent.radiogroup) {
1575
+ checkboxSpan.classList.add("wb-radio");
1576
+ }
1545
1577
  nodeElem.appendChild(checkboxSpan);
1546
1578
  ofsTitlePx += ICON_WIDTH;
1547
1579
  }
@@ -1633,7 +1665,7 @@ export class WunderbaumNode {
1633
1665
  /**
1634
1666
  * Render `node.title`, `.icon` into an existing row.
1635
1667
  *
1636
- * @see {@link WunderbaumNode.render}
1668
+ * @see {@link WunderbaumNode._render}
1637
1669
  */
1638
1670
  protected _render_data(opts: RenderOptions) {
1639
1671
  util.assert(this._rowElem);
@@ -1706,7 +1738,7 @@ export class WunderbaumNode {
1706
1738
 
1707
1739
  /**
1708
1740
  * Update row classes to reflect active, focuses, etc.
1709
- * @see {@link WunderbaumNode.render}
1741
+ * @see {@link WunderbaumNode._render}
1710
1742
  */
1711
1743
  protected _render_status(opts: RenderOptions) {
1712
1744
  // this.log("_render_status", opts);
@@ -1728,6 +1760,7 @@ export class WunderbaumNode {
1728
1760
  this.expanded ? rowClasses.push("wb-expanded") : 0;
1729
1761
  this.lazy ? rowClasses.push("wb-lazy") : 0;
1730
1762
  this.selected ? rowClasses.push("wb-selected") : 0;
1763
+ this._partsel ? rowClasses.push("wb-partsel") : 0;
1731
1764
  this === tree.activeNode ? rowClasses.push("wb-active") : 0;
1732
1765
  this === tree.focusNode ? rowClasses.push("wb-focus") : 0;
1733
1766
  this._errorInfo ? rowClasses.push("wb-error") : 0;
@@ -1768,11 +1801,26 @@ export class WunderbaumNode {
1768
1801
  }
1769
1802
  }
1770
1803
  if (checkboxSpan) {
1771
- if (this.selected) {
1772
- checkboxSpan.className = "wb-checkbox " + iconMap.checkChecked;
1804
+ let cbclass = "wb-checkbox ";
1805
+ if (this.parent.radiogroup) {
1806
+ cbclass += "wb-radio ";
1807
+ if (this.selected) {
1808
+ cbclass += iconMap.radioChecked;
1809
+ // } else if (this._partsel) {
1810
+ // cbclass += iconMap.radioUnknown;
1811
+ } else {
1812
+ cbclass += iconMap.radioUnchecked;
1813
+ }
1773
1814
  } else {
1774
- checkboxSpan.className = "wb-checkbox " + iconMap.checkUnchecked;
1815
+ if (this.selected) {
1816
+ cbclass += iconMap.checkChecked;
1817
+ } else if (this._partsel) {
1818
+ cbclass += iconMap.checkUnknown;
1819
+ } else {
1820
+ cbclass += iconMap.checkUnchecked;
1821
+ }
1775
1822
  }
1823
+ checkboxSpan.className = cbclass;
1776
1824
  }
1777
1825
  // Fix active cell in cell-nav mode
1778
1826
  if (!opts.isNew) {
@@ -1801,7 +1849,7 @@ export class WunderbaumNode {
1801
1849
  }
1802
1850
  }
1803
1851
 
1804
- /**
1852
+ /*
1805
1853
  * Create or update node's markup.
1806
1854
  *
1807
1855
  * `options.change` defaults to ChangeType.data, which updates the title,
@@ -1812,10 +1860,10 @@ export class WunderbaumNode {
1812
1860
  * `options.change` should be set to ChangeType.status instead for best
1813
1861
  * efficiency.
1814
1862
  *
1815
- * Calling `setModified` instead may be a better alternative.
1816
- * @see {@link WunderbaumNode.setModified}
1863
+ * Calling `update()` is almost always a better alternative.
1864
+ * @see {@link WunderbaumNode.update}
1817
1865
  */
1818
- render(options?: RenderOptions) {
1866
+ _render(options?: RenderOptions) {
1819
1867
  // this.log("render", options);
1820
1868
  const opts = Object.assign({ change: ChangeType.data }, options);
1821
1869
  if (!this._rowElem) {
@@ -1846,7 +1894,7 @@ export class WunderbaumNode {
1846
1894
  this.expanded = false;
1847
1895
  this.lazy = true;
1848
1896
  this.children = null;
1849
- this.tree.setModified(ChangeType.structure);
1897
+ this.tree.update(ChangeType.structure);
1850
1898
  }
1851
1899
 
1852
1900
  /** Convert node (or whole branch) into a plain object.
@@ -1863,7 +1911,7 @@ export class WunderbaumNode {
1863
1911
  toDict(recursive = false, callback?: NodeToDictCallback): WbNodeData {
1864
1912
  const dict: any = {};
1865
1913
 
1866
- NODE_ATTRS.forEach((propName: string) => {
1914
+ NODE_DICT_PROPS.forEach((propName: string) => {
1867
1915
  const val = (<any>this)[propName];
1868
1916
 
1869
1917
  if (val instanceof Set) {
@@ -1996,7 +2044,7 @@ export class WunderbaumNode {
1996
2044
  return;
1997
2045
  }
1998
2046
  tree.activeNode = null;
1999
- prev?.setModified(ChangeType.status);
2047
+ prev?.update(ChangeType.status);
2000
2048
  }
2001
2049
  } else if (prev === this || retrigger) {
2002
2050
  this._callEvent("deactivate", { nextNode: null, event: orgEvent });
@@ -2009,8 +2057,8 @@ export class WunderbaumNode {
2009
2057
  if (focusNode || focusTree) tree.focusNode = this;
2010
2058
  if (focusTree) tree.setFocus();
2011
2059
  }
2012
- prev?.setModified(ChangeType.status);
2013
- this.setModified(ChangeType.status);
2060
+ prev?.update(ChangeType.status);
2061
+ this.update(ChangeType.status);
2014
2062
  }
2015
2063
  if (
2016
2064
  options &&
@@ -2044,13 +2092,16 @@ export class WunderbaumNode {
2044
2092
  return; // Nothing to do
2045
2093
  }
2046
2094
  // this.log("setExpanded()");
2095
+ if (flag && this.getOption("autoCollapse")) {
2096
+ this.collapseSiblings(options);
2097
+ }
2047
2098
  if (flag && this.lazy && this.children == null) {
2048
2099
  await this.loadLazy();
2049
2100
  }
2050
2101
  this.expanded = flag;
2051
2102
  const updateOpts = { immediate: immediate };
2052
2103
  // const updateOpts = { immediate: !!util.getOption(options, "immediate") };
2053
- this.tree.setModified(ChangeType.structure, updateOpts);
2104
+ this.tree.update(ChangeType.structure, updateOpts);
2054
2105
  if (flag && scrollIntoView !== false) {
2055
2106
  const lastChild = this.getLastChild();
2056
2107
  if (lastChild) {
@@ -2068,14 +2119,14 @@ export class WunderbaumNode {
2068
2119
  util.assert(!!flag, "blur is not yet implemented");
2069
2120
  const prev = this.tree.focusNode;
2070
2121
  this.tree.focusNode = this;
2071
- prev?.setModified();
2072
- this.setModified();
2122
+ prev?.update();
2123
+ this.update();
2073
2124
  }
2074
2125
 
2075
2126
  /** Set a new icon path or class. */
2076
2127
  setIcon(icon: string) {
2077
2128
  this.icon = icon;
2078
- this.setModified();
2129
+ this.update();
2079
2130
  }
2080
2131
 
2081
2132
  /** Change node's {@link key} and/or {@link refKey}. */
@@ -2083,6 +2134,13 @@ export class WunderbaumNode {
2083
2134
  throw new Error("Not yet implemented");
2084
2135
  }
2085
2136
 
2137
+ /**
2138
+ * @deprecated since v0.3.6: use `update()` instead.
2139
+ */
2140
+ setModified(change: ChangeType = ChangeType.data): void {
2141
+ this.logWarn("setModified() is deprecated: use update() instead.");
2142
+ return this.update(change);
2143
+ }
2086
2144
  /**
2087
2145
  * Trigger a repaint, typically after a status or data change.
2088
2146
  *
@@ -2090,23 +2148,244 @@ export class WunderbaumNode {
2090
2148
  * and column content. It can be reduced to 'ChangeType.status' if only
2091
2149
  * active/focus/selected state has changed.
2092
2150
  *
2093
- * This method will eventually call {@link WunderbaumNode.render()} with
2151
+ * This method will eventually call {@link WunderbaumNode._render()} with
2094
2152
  * default options, but may be more consistent with the tree's
2095
- * {@link Wunderbaum.setModified()} API.
2153
+ * {@link Wunderbaum.update()} API.
2096
2154
  */
2097
- setModified(change: ChangeType = ChangeType.data) {
2155
+ update(change: ChangeType = ChangeType.data) {
2098
2156
  util.assert(change === ChangeType.status || change === ChangeType.data);
2099
- this.tree.setModified(change, this);
2157
+ this.tree.update(change, this);
2158
+ }
2159
+
2160
+ /**
2161
+ * Return an array of selected nodes.
2162
+ * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier')
2163
+ */
2164
+ getSelectedNodes(stopOnParents: boolean = false): WunderbaumNode[] {
2165
+ let nodeList: WunderbaumNode[] = [];
2166
+ this.visit((node) => {
2167
+ if (node.selected) {
2168
+ nodeList.push(node);
2169
+ if (stopOnParents === true) {
2170
+ return "skip"; // stop processing this branch
2171
+ }
2172
+ }
2173
+ });
2174
+ return nodeList;
2175
+ }
2176
+
2177
+ /** Toggle the check/uncheck state. */
2178
+ toggleSelected(options?: SetSelectedOptions): TristateType {
2179
+ let flag = this.isSelected();
2180
+ if (flag === undefined) {
2181
+ flag = this._anySelectable();
2182
+ } else {
2183
+ flag = !flag;
2184
+ }
2185
+ return this.setSelected(flag, options);
2186
+ }
2187
+
2188
+ /** Return true if at least on selectable descendant end-node is unselected. @internal */
2189
+ _anySelectable(): boolean {
2190
+ let found = false;
2191
+ this.visit((node) => {
2192
+ if (
2193
+ node.selected === false &&
2194
+ !node.unselectable &&
2195
+ !node.hasChildren() &&
2196
+ !node.parent.radiogroup
2197
+ ) {
2198
+ found = true;
2199
+ return false; // Stop iteration
2200
+ }
2201
+ });
2202
+ return found;
2203
+ }
2204
+
2205
+ /* Apply selection state to a single node. */
2206
+ protected _changeSelectStatusProps(state: TristateType): boolean {
2207
+ let changed = false;
2208
+ switch (state) {
2209
+ case false:
2210
+ changed = this.selected || this._partsel;
2211
+ this.selected = false;
2212
+ this._partsel = false;
2213
+ break;
2214
+ case true:
2215
+ changed = !this.selected || !this._partsel;
2216
+ this.selected = true;
2217
+ this._partsel = true;
2218
+ break;
2219
+ case undefined:
2220
+ changed = this.selected || !this._partsel;
2221
+ this.selected = false;
2222
+ this._partsel = true;
2223
+ break;
2224
+ default:
2225
+ util.error(`Invalid state: ${state}`);
2226
+ }
2227
+ if (changed) {
2228
+ this.update();
2229
+ }
2230
+ return changed;
2231
+ }
2232
+ /**
2233
+ * Fix selection status, after this node was (de)selected in `selectMode: 'hier'`.
2234
+ * This includes (de)selecting all descendants.
2235
+ */
2236
+ fixSelection3AfterClick(opts?: SetSelectedOptions): void {
2237
+ const force = !!opts?.force;
2238
+ let flag = this.isSelected();
2239
+
2240
+ this.visit((node) => {
2241
+ if (node.radiogroup) {
2242
+ return "skip"; // Don't (de)select this branch
2243
+ }
2244
+ if (force || !node.getOption("unselectable")) {
2245
+ node._changeSelectStatusProps(flag);
2246
+ }
2247
+ });
2248
+ this.fixSelection3FromEndNodes();
2249
+ }
2250
+
2251
+ /**
2252
+ * Fix selection status for multi-hier mode.
2253
+ * Only end-nodes are considered to update the descendants branch and parents.
2254
+ * Should be called after this node has loaded new children or after
2255
+ * children have been modified using the API.
2256
+ */
2257
+ fixSelection3FromEndNodes(opts?: SetSelectedOptions): void {
2258
+ const force = !!opts?.force;
2259
+ util.assert(
2260
+ this.tree.options.selectMode === "hier",
2261
+ "expected selectMode 'hier'"
2262
+ );
2263
+
2264
+ // Visit all end nodes and adjust their parent's `selected` and `_partsel`
2265
+ // attributes. Return selection state true, false, or undefined.
2266
+ const _walk = (node: WunderbaumNode) => {
2267
+ let state;
2268
+ const children = node.children;
2269
+
2270
+ if (children && children.length) {
2271
+ // check all children recursively
2272
+ let allSelected = true;
2273
+ let someSelected = false;
2274
+
2275
+ for (let i = 0, l = children.length; i < l; i++) {
2276
+ const child = children[i];
2277
+ // the selection state of a node is not relevant; we need the end-nodes
2278
+ const s = _walk(child);
2279
+ if (s !== false) {
2280
+ someSelected = true;
2281
+ }
2282
+ if (s !== true) {
2283
+ allSelected = false;
2284
+ }
2285
+ }
2286
+ state = allSelected ? true : someSelected ? undefined : false;
2287
+ } else {
2288
+ // This is an end-node: simply report the status
2289
+ state = !!node.selected;
2290
+ }
2291
+ // #939: Keep a `_partsel` flag that was explicitly set on a lazy node
2292
+ if (
2293
+ node._partsel &&
2294
+ !node.selected &&
2295
+ node.lazy &&
2296
+ node.children == null
2297
+ ) {
2298
+ state = undefined;
2299
+ }
2300
+ if (force || !node.getOption("unselectable")) {
2301
+ node._changeSelectStatusProps(state);
2302
+ }
2303
+ return state;
2304
+ };
2305
+ _walk(this);
2306
+
2307
+ // Update parent's state
2308
+ this.visitParents((node) => {
2309
+ let state;
2310
+ const children = node.children!;
2311
+ let allSelected = true;
2312
+ let someSelected = false;
2313
+
2314
+ for (let i = 0, l = children.length; i < l; i++) {
2315
+ const child = children[i];
2316
+
2317
+ state = !!child.selected;
2318
+ // When fixing the parents, we trust the sibling status (i.e. we don't recurse)
2319
+ if (state || child._partsel) {
2320
+ someSelected = true;
2321
+ }
2322
+ if (!state) {
2323
+ allSelected = false;
2324
+ }
2325
+ }
2326
+ state = allSelected ? true : someSelected ? undefined : false;
2327
+ node._changeSelectStatusProps(state);
2328
+ });
2100
2329
  }
2101
2330
 
2102
2331
  /** Modify the check/uncheck state. */
2103
- setSelected(flag: boolean = true, options?: SetSelectedOptions) {
2104
- const prev = this.selected;
2105
- if (!!flag !== prev) {
2332
+ setSelected(
2333
+ flag: boolean = true,
2334
+ options?: SetSelectedOptions
2335
+ ): TristateType {
2336
+ const tree = this.tree;
2337
+ const sendEvents = !options?.noEvents; // Default: send events
2338
+ const prev = this.isSelected();
2339
+ const isRadio = this.parent && this.parent.radiogroup;
2340
+ const selectMode = tree.options.selectMode;
2341
+ const canSelect = options?.force || !this.getOption("unselectable");
2342
+
2343
+ flag = !!flag;
2344
+ // this.logDebug(`setSelected(${flag})`, this);
2345
+ if (!canSelect) {
2346
+ return prev;
2347
+ }
2348
+ if (options?.propagateDown && selectMode === "multi") {
2349
+ tree.runWithDeferredUpdate(() => {
2350
+ this.visit((node) => {
2351
+ node.setSelected(flag);
2352
+ });
2353
+ });
2354
+ return prev;
2355
+ }
2356
+
2357
+ if (
2358
+ flag === prev ||
2359
+ (sendEvents && this._callEvent("beforeSelect", { flag: flag }) === false)
2360
+ ) {
2361
+ return prev;
2362
+ }
2363
+
2364
+ tree.runWithDeferredUpdate(() => {
2365
+ if (isRadio) {
2366
+ // Radiobutton Group
2367
+ if (!flag && !options?.force) {
2368
+ return prev; // don't uncheck radio buttons
2369
+ }
2370
+ for (let sibling of this.parent.children!) {
2371
+ sibling.selected = sibling === this;
2372
+ }
2373
+ } else {
2374
+ this.selected = flag;
2375
+ if (selectMode === "hier") {
2376
+ this.fixSelection3AfterClick();
2377
+ } else if (selectMode === "single") {
2378
+ tree.visit((n) => {
2379
+ n.selected = false;
2380
+ });
2381
+ }
2382
+ }
2383
+ });
2384
+
2385
+ if (sendEvents) {
2106
2386
  this._callEvent("select", { flag: flag });
2107
2387
  }
2108
- this.selected = !!flag;
2109
- this.setModified();
2388
+ return prev;
2110
2389
  }
2111
2390
 
2112
2391
  /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
@@ -2141,7 +2420,7 @@ export class WunderbaumNode {
2141
2420
 
2142
2421
  statusNode = this.addNode(data, "prependChild");
2143
2422
  statusNode.match = true;
2144
- tree.setModified(ChangeType.structure);
2423
+ tree.update(ChangeType.structure);
2145
2424
 
2146
2425
  return statusNode;
2147
2426
  };
@@ -2157,7 +2436,7 @@ export class WunderbaumNode {
2157
2436
  this._isLoading = true;
2158
2437
  this._errorInfo = null;
2159
2438
  if (this.parent) {
2160
- this.setModified(ChangeType.status);
2439
+ this.update(ChangeType.status);
2161
2440
  } else {
2162
2441
  // If this is the invisible root, add a visible top-level node
2163
2442
  _setStatusNode({
@@ -2170,7 +2449,7 @@ export class WunderbaumNode {
2170
2449
  tooltip: details,
2171
2450
  });
2172
2451
  }
2173
- // this.render();
2452
+ // this.update();
2174
2453
  break;
2175
2454
  case "error":
2176
2455
  _setStatusNode({
@@ -2200,14 +2479,14 @@ export class WunderbaumNode {
2200
2479
  default:
2201
2480
  util.error("invalid node status " + status);
2202
2481
  }
2203
- tree.setModified(ChangeType.structure);
2482
+ tree.update(ChangeType.structure);
2204
2483
  return statusNode;
2205
2484
  }
2206
2485
 
2207
2486
  /** Rename this node. */
2208
2487
  setTitle(title: string): void {
2209
2488
  this.title = title;
2210
- this.setModified();
2489
+ this.update();
2211
2490
  // this.triggerModify("rename"); // TODO
2212
2491
  }
2213
2492
 
@@ -2238,7 +2517,7 @@ export class WunderbaumNode {
2238
2517
  deep: boolean = false
2239
2518
  ): void {
2240
2519
  this._sortChildren(cmp || nodeTitleSorter, deep);
2241
- this.tree.setModified(ChangeType.structure);
2520
+ this.tree.update(ChangeType.structure);
2242
2521
  // this.triggerModify("sort"); // TODO
2243
2522
  }
2244
2523
 
@@ -2275,7 +2554,7 @@ export class WunderbaumNode {
2275
2554
  }
2276
2555
 
2277
2556
  /**
2278
- * Call `callback(node)` for all child nodes in hierarchical order (depth-first, pre-order).
2557
+ * Call `callback(node)` for all descendant nodes in hierarchical order (depth-first, pre-order).
2279
2558
  *
2280
2559
  * Stop iteration, if fn() returns false. Skip current branch, if fn()
2281
2560
  * returns "skip".<br>