wunderbaum 0.0.4 → 0.0.7

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
@@ -9,25 +9,31 @@ import * as util from "./util";
9
9
 
10
10
  import { Wunderbaum } from "./wunderbaum";
11
11
  import {
12
- NavigationMode,
12
+ AddNodeType,
13
+ ApplyCommandType,
13
14
  ChangeType,
14
- iconMap,
15
- ICON_WIDTH,
16
- KEY_TO_ACTION_DICT,
17
- makeNodeTitleMatcher,
15
+ ColumnEventInfos,
16
+ MakeVisibleOptions,
18
17
  MatcherType,
19
18
  NodeAnyCallback,
20
19
  NodeStatusType,
21
20
  NodeVisitCallback,
22
21
  NodeVisitResponse,
23
- ROW_EXTRA_PAD,
24
- ROW_HEIGHT,
25
- TEST_IMG,
26
- ApplyCommandType,
27
- AddNodeType,
22
+ ScrollIntoViewOptions,
28
23
  SetActiveOptions,
29
24
  SetExpandedOptions,
30
25
  SetSelectedOptions,
26
+ SetStatusOptions,
27
+ } from "./types";
28
+ import {
29
+ iconMap,
30
+ ICON_WIDTH,
31
+ KEY_TO_ACTION_DICT,
32
+ makeNodeTitleMatcher,
33
+ RESERVED_TREE_SOURCE_KEYS,
34
+ TITLE_SPAN_PAD_Y,
35
+ ROW_HEIGHT,
36
+ TEST_IMG,
31
37
  } from "./common";
32
38
  import { Deferred } from "./deferred";
33
39
  import { WbNodeData } from "./wb_options";
@@ -50,7 +56,7 @@ const NODE_PROPS = new Set<string>([
50
56
  const NODE_ATTRS = new Set<string>([
51
57
  "checkbox",
52
58
  "expanded",
53
- "extraClasses", // TODO: rename to classes
59
+ "classes",
54
60
  "folder",
55
61
  "icon",
56
62
  "iconTooltip",
@@ -96,6 +102,7 @@ export class WunderbaumNode {
96
102
  public readonly refKey: string | undefined = undefined;
97
103
  public children: WunderbaumNode[] | null = null;
98
104
  public checkbox?: boolean;
105
+ /** If true, (in grid mode) no cells are rendered, except for the node title.*/
99
106
  public colspan?: boolean;
100
107
  public icon?: boolean | string;
101
108
  public lazy: boolean = false;
@@ -108,8 +115,8 @@ export class WunderbaumNode {
108
115
  public type?: string;
109
116
  public tooltip?: string;
110
117
  /** Additional classes added to `div.wb-row`.
111
- * @see {@link addClass}, {@link removeClass}, {@link toggleClass}. */
112
- public extraClasses = new Set<string>();
118
+ * @see {@link hasClass}, {@link setClass}. */
119
+ public classes: Set<string> | null = null; //new Set<string>();
113
120
  /** Custom data that was passed to the constructor */
114
121
  public data: any = {};
115
122
  // --- Node Status ---
@@ -150,9 +157,7 @@ export class WunderbaumNode {
150
157
  this.lazy = data.lazy === true;
151
158
  this.selected = data.selected === true;
152
159
  if (data.classes) {
153
- for (const c of data.classes.split(" ")) {
154
- this.extraClasses.add(c.trim());
155
- }
160
+ this.setClass(data.classes);
156
161
  }
157
162
  // Store custom fields as `node.data`
158
163
  for (const [key, value] of Object.entries(data)) {
@@ -172,7 +177,7 @@ export class WunderbaumNode {
172
177
  * @internal
173
178
  */
174
179
  toString() {
175
- return "WunderbaumNode@" + this.key + "<'" + this.title + "'>";
180
+ return `WunderbaumNode@${this.key}<'${this.title}'>`;
176
181
  }
177
182
 
178
183
  // /** Return an option value. */
@@ -220,6 +225,8 @@ export class WunderbaumNode {
220
225
  * @returns first child added
221
226
  */
222
227
  addChildren(nodeData: any, options?: any): WunderbaumNode {
228
+ const tree = this.tree;
229
+ const level = options ? options.level : this.getLevel();
223
230
  let insertBefore: WunderbaumNode | string | number = options
224
231
  ? options.before
225
232
  : null,
@@ -227,19 +234,21 @@ export class WunderbaumNode {
227
234
  nodeList = [];
228
235
 
229
236
  try {
230
- this.tree.enableUpdate(false);
237
+ tree.enableUpdate(false);
231
238
 
232
239
  if (util.isPlainObject(nodeData)) {
233
240
  nodeData = [nodeData];
234
241
  }
242
+ const forceExpand = level < tree.options.minExpandLevel!;
235
243
  for (let child of nodeData) {
236
244
  let subChildren = child.children;
237
245
  delete child.children;
238
246
 
239
- let n = new WunderbaumNode(this.tree, this, child);
247
+ let n = new WunderbaumNode(tree, this, child);
248
+ if (forceExpand && !n.lazy) n.expanded = true;
240
249
  nodeList.push(n);
241
250
  if (subChildren) {
242
- n.addChildren(subChildren, { redraw: false });
251
+ n.addChildren(subChildren, { redraw: false, level: level + 1 });
243
252
  }
244
253
  }
245
254
 
@@ -256,14 +265,14 @@ export class WunderbaumNode {
256
265
  this.children.splice(pos, 0, ...nodeList);
257
266
  }
258
267
  // TODO:
259
- // if (this.tree.options.selectMode === 3) {
268
+ // if (tree.options.selectMode === 3) {
260
269
  // this.fixSelection3FromEndNodes();
261
270
  // }
262
271
  // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
263
- this.tree.setModified(ChangeType.structure);
272
+ tree.setModified(ChangeType.structure);
264
273
  return nodeList[0];
265
274
  } finally {
266
- this.tree.enableUpdate(true);
275
+ tree.enableUpdate(true);
267
276
  }
268
277
  }
269
278
 
@@ -307,31 +316,42 @@ export class WunderbaumNode {
307
316
  return this.tree.applyCommand(cmd, this, opts);
308
317
  }
309
318
 
310
- addClass(className: string | string[] | Set<string>) {
311
- const cnSet = util.toSet(className);
312
- cnSet.forEach((cn) => {
313
- this.extraClasses.add(cn);
314
- this._rowElem?.classList.add(cn);
315
- });
316
- }
317
-
318
- removeClass(className: string | string[] | Set<string>) {
319
- const cnSet = util.toSet(className);
320
- cnSet.forEach((cn) => {
321
- this.extraClasses.delete(cn);
322
- this._rowElem?.classList.remove(cn);
323
- });
324
- }
325
-
326
- toggleClass(className: string | string[] | Set<string>, flag: boolean) {
319
+ /**
320
+ * Add/remove one or more classes to `<div class='wb-row'>`.
321
+ *
322
+ * This also maintains `node.classes`, so the class will survive a re-render.
323
+ *
324
+ * @param className one or more class names. Multiple classes can be passed
325
+ * as space-separated string, array of strings, or set of strings.
326
+ */
327
+ setClass(
328
+ className: string | string[] | Set<string>,
329
+ flag: boolean = true
330
+ ): void {
327
331
  const cnSet = util.toSet(className);
328
- cnSet.forEach((cn) => {
329
- flag ? this.extraClasses.add(cn) : this.extraClasses.delete(cn);
330
- this._rowElem?.classList.toggle(cn, flag);
331
- });
332
+ if (flag) {
333
+ if (this.classes === null) {
334
+ this.classes = new Set<string>();
335
+ }
336
+ cnSet.forEach((cn) => {
337
+ this.classes!.add(cn);
338
+ this._rowElem?.classList.toggle(cn, flag);
339
+ });
340
+ } else {
341
+ if (this.classes === null) {
342
+ return;
343
+ }
344
+ cnSet.forEach((cn) => {
345
+ this.classes!.delete(cn);
346
+ this._rowElem?.classList.toggle(cn, flag);
347
+ });
348
+ if (this.classes.size === 0) {
349
+ this.classes = null;
350
+ }
351
+ }
332
352
  }
333
353
 
334
- /** */
354
+ /** Call `setExpanded()` on al child nodes*/
335
355
  async expandAll(flag: boolean = true) {
336
356
  this.visit((node) => {
337
357
  node.setExpanded(flag);
@@ -527,6 +547,11 @@ export class WunderbaumNode {
527
547
  return !!(this.children && this.children.length);
528
548
  }
529
549
 
550
+ /** Return true if node has className set. */
551
+ hasClass(className: string): boolean {
552
+ return this.classes ? this.classes.has(className) : false;
553
+ }
554
+
530
555
  /** Return true if this node is the currently active tree node. */
531
556
  isActive() {
532
557
  return this.tree.activeNode === this;
@@ -539,6 +564,13 @@ export class WunderbaumNode {
539
564
  return this.parent && this.parent === other;
540
565
  }
541
566
 
567
+ /** Return true if this node's title spans all columns, i.e. the node has no
568
+ * grid cells.
569
+ */
570
+ isColspan() {
571
+ return !!this.getOption("colspan");
572
+ }
573
+
542
574
  /** Return true if this node is a direct or indirect sub node of `other`.
543
575
  * (See also [[isChildOf]].)
544
576
  */
@@ -690,9 +722,11 @@ export class WunderbaumNode {
690
722
  return true;
691
723
  }
692
724
 
693
- protected _loadSourceObject(source: any) {
725
+ protected _loadSourceObject(source: any, level?: number) {
694
726
  const tree = this.tree;
695
727
 
728
+ level ??= this.getLevel();
729
+
696
730
  // Let caller modify the parsed JSON response:
697
731
  this._callEvent("receive", { response: source });
698
732
 
@@ -705,21 +739,40 @@ export class WunderbaumNode {
705
739
  "If `source` is an object, it must have a `children` property"
706
740
  );
707
741
  if (source.types) {
708
- // TODO: convert types.classes to Set()
709
- util.extend(tree.types, source.types);
742
+ tree.logInfo("Redefine types", source.columns);
743
+ tree.setTypes(source.types, false);
744
+ delete source.types;
745
+ }
746
+ if (source.columns) {
747
+ tree.logInfo("Redefine columns", source.columns);
748
+ tree.columns = source.columns;
749
+ delete source.columns;
750
+ tree.updateColumns({ calculateCols: false });
710
751
  }
752
+
711
753
  this.addChildren(source.children);
712
754
 
755
+ // Add extra data to `tree.data`
756
+ for (const [key, value] of Object.entries(source)) {
757
+ if (!RESERVED_TREE_SOURCE_KEYS.has(key)) {
758
+ tree.data[key] = value;
759
+ tree.logDebug(`Add source.${key} to tree.data.${key}`);
760
+ }
761
+ }
762
+
713
763
  this._callEvent("load");
714
764
  }
715
765
 
716
766
  /** Download data from the cloud, then call `.update()`. */
717
767
  async load(source: any) {
718
768
  const tree = this.tree;
719
- // const opts = tree.options;
720
769
  const requestId = Date.now();
721
770
  const prevParent = this.parent;
722
771
  const url = typeof source === "string" ? source : source.url;
772
+ const start = Date.now();
773
+ let elap = 0,
774
+ elapLoad = 0,
775
+ elapProcess = 0;
723
776
 
724
777
  // Check for overlapping requests
725
778
  if (this._requestId) {
@@ -730,11 +783,12 @@ export class WunderbaumNode {
730
783
  }
731
784
  this._requestId = requestId;
732
785
 
733
- const timerLabel = tree.logTime(this + ".load()");
786
+ // const timerLabel = tree.logTime(this + ".load()");
734
787
 
735
788
  try {
736
789
  if (!url) {
737
790
  this._loadSourceObject(source);
791
+ elapProcess = Date.now() - start;
738
792
  } else {
739
793
  this.setStatus(NodeStatusType.loading);
740
794
  const response = await fetch(url, { method: "GET" });
@@ -742,6 +796,7 @@ export class WunderbaumNode {
742
796
  util.error(`GET ${url} returned ${response.status}, ${response}`);
743
797
  }
744
798
  const data = await response.json();
799
+ elapLoad = Date.now() - start;
745
800
 
746
801
  if (this._requestId && this._requestId > requestId) {
747
802
  this.logWarn(
@@ -758,23 +813,32 @@ export class WunderbaumNode {
758
813
  return;
759
814
  }
760
815
  this.setStatus(NodeStatusType.ok);
761
- if (data.columns) {
762
- tree.logInfo("Re-define columns", data.columns);
763
- util.assert(!this.parent);
764
- tree.columns = data.columns;
765
- delete data.columns;
766
- tree.updateColumns({ calculateCols: false });
767
- }
816
+ // if (data.columns) {
817
+ // tree.logInfo("Re-define columns", data.columns);
818
+ // util.assert(!this.parent);
819
+ // tree.columns = data.columns;
820
+ // delete data.columns;
821
+ // tree.updateColumns({ calculateCols: false });
822
+ // }
823
+ const startProcess = Date.now();
768
824
  this._loadSourceObject(data);
825
+ elapProcess = Date.now() - startProcess;
769
826
  }
770
827
  } catch (error) {
771
828
  this.logError("Error during load()", source, error);
772
829
  this._callEvent("error", { error: error });
773
- this.setStatus(NodeStatusType.error, "" + error);
830
+ this.setStatus(NodeStatusType.error, { message: "" + error });
774
831
  throw error;
775
832
  } finally {
776
833
  this._requestId = 0;
777
- tree.logTimeEnd(timerLabel);
834
+ elap = Date.now() - start;
835
+ if (tree.options.debugLevel >= 3) {
836
+ tree.logInfo(
837
+ `Load source took ${elap / 1000} seconds (transfer: ${
838
+ elapLoad / 1000
839
+ }s, processing: ${elapProcess / 1000}s)`
840
+ );
841
+ }
778
842
  }
779
843
  }
780
844
 
@@ -815,7 +879,7 @@ export class WunderbaumNode {
815
879
  } catch (e) {
816
880
  this.logError("Error during loadLazy()", e);
817
881
  this._callEvent("error", { error: e });
818
- this.setStatus(NodeStatusType.error, "" + e);
882
+ this.setStatus(NodeStatusType.error, { message: "" + e });
819
883
  }
820
884
  return;
821
885
  }
@@ -862,25 +926,29 @@ export class WunderbaumNode {
862
926
  * @param {object} [opts] passed to `setExpanded()`.
863
927
  * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
864
928
  */
865
- async makeVisible(opts: any) {
929
+ async makeVisible(opts?: MakeVisibleOptions) {
866
930
  let i,
867
931
  dfd = new Deferred(),
868
932
  deferreds = [],
869
933
  parents = this.getParentList(false, false),
870
934
  len = parents.length,
871
- effects = !(opts && opts.noAnimation === true),
935
+ // effects = !(opts && opts.noAnimation === true),
872
936
  scroll = !(opts && opts.scrollIntoView === false);
873
937
 
874
938
  // Expand bottom-up, so only the top node is animated
875
939
  for (i = len - 1; i >= 0; i--) {
876
940
  // self.debug("pushexpand" + parents[i]);
877
- deferreds.push(parents[i].setExpanded(true, opts));
941
+ const seOpts = { noAnimation: opts?.noAnimation };
942
+ deferreds.push(parents[i].setExpanded(true, seOpts));
878
943
  }
879
944
  Promise.all(deferreds).then(() => {
880
945
  // All expands have finished
881
946
  // self.debug("expand DONE", scroll);
882
- if (scroll) {
883
- this.scrollIntoView(effects).then(() => {
947
+ // Note: this.tree may be none when switching demo trees
948
+ if (scroll && this.tree) {
949
+ // Make sure markup and _rowIdx is updated before we do the scroll calculations
950
+ this.tree.updatePendingModifications();
951
+ this.scrollIntoView().then(() => {
884
952
  // self.debug("scroll DONE");
885
953
  dfd.resolve();
886
954
  });
@@ -1074,25 +1142,35 @@ export class WunderbaumNode {
1074
1142
  }
1075
1143
  }
1076
1144
 
1077
- protected _getRenderInfo() {
1078
- let colInfosById: { [key: string]: any } = {};
1079
- let idx = 0;
1080
- let colElems = this._rowElem
1145
+ protected _getRenderInfo(): any {
1146
+ const allColInfosById: ColumnEventInfos = {};
1147
+ const renderColInfosById: ColumnEventInfos = {};
1148
+ const isColspan = this.isColspan();
1149
+
1150
+ const colElems = this._rowElem
1081
1151
  ? ((<unknown>(
1082
1152
  this._rowElem.querySelectorAll("span.wb-col")
1083
- )) as HTMLElement[])
1153
+ )) as HTMLSpanElement[])
1084
1154
  : null;
1085
1155
 
1156
+ let idx = 0;
1086
1157
  for (let col of this.tree.columns) {
1087
- colInfosById[col.id] = {
1158
+ allColInfosById[col.id] = {
1088
1159
  id: col.id,
1089
1160
  idx: idx,
1090
1161
  elem: colElems ? colElems[idx] : null,
1091
1162
  info: col,
1092
1163
  };
1164
+ // renderColInfosById only contains columns that need rendering:
1165
+ if (!isColspan && col.id !== "*") {
1166
+ renderColInfosById[col.id] = allColInfosById[col.id];
1167
+ }
1093
1168
  idx++;
1094
1169
  }
1095
- return colInfosById;
1170
+ return {
1171
+ allColInfosById: allColInfosById,
1172
+ renderColInfosById: renderColInfosById,
1173
+ };
1096
1174
  }
1097
1175
 
1098
1176
  protected _createIcon(
@@ -1118,6 +1196,8 @@ export class WunderbaumNode {
1118
1196
  icon = iconMap.folderOpen;
1119
1197
  } else if (this.children) {
1120
1198
  icon = iconMap.folder;
1199
+ } else if (this.lazy) {
1200
+ icon = iconMap.folderLazy;
1121
1201
  } else {
1122
1202
  icon = iconMap.doc;
1123
1203
  }
@@ -1145,7 +1225,7 @@ export class WunderbaumNode {
1145
1225
 
1146
1226
  /**
1147
1227
  * Create a whole new `<div class="wb-row">` element.
1148
- * @see {@link Wunderbaumode.render}
1228
+ * @see {@link WunderbaumNode.render}
1149
1229
  */
1150
1230
  protected _render_markup(opts: any) {
1151
1231
  const tree = this.tree;
@@ -1160,8 +1240,7 @@ export class WunderbaumNode {
1160
1240
  let checkboxSpan: HTMLElement | null = null;
1161
1241
  let iconSpan: HTMLElement | null;
1162
1242
  let expanderSpan: HTMLElement | null = null;
1163
- const activeColIdx =
1164
- tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
1243
+ const activeColIdx = tree.isRowNav() ? null : tree.activeColIdx;
1165
1244
 
1166
1245
  const isNew = !rowDiv;
1167
1246
  util.assert(isNew);
@@ -1229,7 +1308,9 @@ export class WunderbaumNode {
1229
1308
  }
1230
1309
 
1231
1310
  // Render columns
1232
- if (!this.colspan && columns.length > 1) {
1311
+ const isColspan = this.isColspan();
1312
+
1313
+ if (!isColspan && columns.length > 1) {
1233
1314
  let colIdx = 0;
1234
1315
  for (let col of columns) {
1235
1316
  colIdx++;
@@ -1257,11 +1338,6 @@ export class WunderbaumNode {
1257
1338
  }
1258
1339
  }
1259
1340
  }
1260
-
1261
- // Now go on and fill in data and update classes
1262
- opts.isNew = true;
1263
- this._render_data(opts);
1264
-
1265
1341
  // Attach to DOM as late as possible
1266
1342
  const after = opts ? opts.after : "last";
1267
1343
  switch (after) {
@@ -1274,12 +1350,15 @@ export class WunderbaumNode {
1274
1350
  default:
1275
1351
  opts.after.after(rowDiv);
1276
1352
  }
1353
+ // Now go on and fill in data and update classes
1354
+ opts.isNew = true;
1355
+ this._render_data(opts);
1277
1356
  }
1278
1357
 
1279
1358
  /**
1280
1359
  * Render `node.title`, `.icon` into an existing row.
1281
1360
  *
1282
- * @see {@link Wunderbaumode.render}
1361
+ * @see {@link WunderbaumNode.render}
1283
1362
  */
1284
1363
  protected _render_data(opts: any) {
1285
1364
  util.assert(this._rowElem);
@@ -1289,7 +1368,7 @@ export class WunderbaumNode {
1289
1368
  const rowDiv = this._rowElem!;
1290
1369
  const isNew = !!opts.isNew; // Called by _render_markup()?
1291
1370
  const columns = tree.columns;
1292
- const typeInfo = this.type ? tree.types[this.type] : null;
1371
+ const isColspan = this.isColspan();
1293
1372
 
1294
1373
  // Row markup already exists
1295
1374
  const nodeElem = rowDiv.querySelector("span.wb-node") as HTMLSpanElement;
@@ -1305,15 +1384,15 @@ export class WunderbaumNode {
1305
1384
 
1306
1385
  // Set the width of the title span, so overflow ellipsis work
1307
1386
  if (!treeOptions.skeleton) {
1308
- if (this.colspan) {
1387
+ if (isColspan) {
1309
1388
  let vpWidth = tree.element.clientWidth;
1310
1389
  titleSpan.style.width =
1311
- vpWidth - (<any>nodeElem)._ofsTitlePx - ROW_EXTRA_PAD + "px";
1390
+ vpWidth - (<any>nodeElem)._ofsTitlePx - TITLE_SPAN_PAD_Y + "px";
1312
1391
  } else {
1313
1392
  titleSpan.style.width =
1314
- columns[0]._widthPx -
1393
+ columns[0]._widthPx! -
1315
1394
  (<any>nodeElem)._ofsTitlePx -
1316
- ROW_EXTRA_PAD +
1395
+ TITLE_SPAN_PAD_Y +
1317
1396
  "px";
1318
1397
  }
1319
1398
  }
@@ -1330,19 +1409,22 @@ export class WunderbaumNode {
1330
1409
  });
1331
1410
  } else if (this.parent) {
1332
1411
  // Skip root node
1412
+ const renderInfo = this._getRenderInfo();
1413
+
1333
1414
  this._callEvent("render", {
1334
1415
  isNew: isNew,
1335
- isDataChange: true,
1416
+ isColspan: isColspan,
1417
+ // isDataChange: true,
1336
1418
  nodeElem: nodeElem,
1337
- typeInfo: typeInfo,
1338
- colInfosById: this._getRenderInfo(),
1419
+ allColInfosById: renderInfo.allColInfosById,
1420
+ renderColInfosById: renderInfo.renderColInfosById,
1339
1421
  });
1340
1422
  }
1341
1423
  }
1342
1424
 
1343
1425
  /**
1344
1426
  * Update row classes to reflect active, focuses, etc.
1345
- * @see {@link Wunderbaumode.render}
1427
+ * @see {@link WunderbaumNode.render}
1346
1428
  */
1347
1429
  protected _render_status(opts: any) {
1348
1430
  // this.log("_render_status", opts);
@@ -1359,8 +1441,6 @@ export class WunderbaumNode {
1359
1441
  const checkboxSpan = nodeElem.querySelector(
1360
1442
  "i.wb-checkbox"
1361
1443
  ) as HTMLLIElement;
1362
- // TODO: update icon (if not opts.isNew)
1363
- // const iconSpan = nodeElem.querySelector("i.wb-icon") as HTMLElement;
1364
1444
 
1365
1445
  let rowClasses = ["wb-row"];
1366
1446
  this.expanded ? rowClasses.push("wb-expanded") : 0;
@@ -1370,6 +1450,7 @@ export class WunderbaumNode {
1370
1450
  this === tree.focusNode ? rowClasses.push("wb-focus") : 0;
1371
1451
  this._errorInfo ? rowClasses.push("wb-error") : 0;
1372
1452
  this._isLoading ? rowClasses.push("wb-loading") : 0;
1453
+ this.isColspan() ? rowClasses.push("wb-colspan") : 0;
1373
1454
  this.statusNodeType
1374
1455
  ? rowClasses.push("wb-status-" + this.statusNodeType)
1375
1456
  : 0;
@@ -1381,8 +1462,8 @@ export class WunderbaumNode {
1381
1462
  // Replace previous classes:
1382
1463
  rowDiv.className = rowClasses.join(" ");
1383
1464
 
1384
- // Add classes from `node.extraClasses`
1385
- rowDiv.classList.add(...this.extraClasses);
1465
+ // Add classes from `node.classes`
1466
+ this.classes ? rowDiv.classList.add(...this.classes) : 0;
1386
1467
 
1387
1468
  // Add classes from `tree.types[node.type]`
1388
1469
  if (typeInfo && typeInfo.classes) {
@@ -1417,6 +1498,11 @@ export class WunderbaumNode {
1417
1498
  for (let colSpan of rowDiv.children) {
1418
1499
  colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx);
1419
1500
  }
1501
+ // Update icon (if not opts.isNew, which would rebuild markup anyway)
1502
+ const iconSpan = nodeElem.querySelector("i.wb-icon") as HTMLElement;
1503
+ if (iconSpan) {
1504
+ this._createIcon(nodeElem, iconSpan);
1505
+ }
1420
1506
  }
1421
1507
  }
1422
1508
 
@@ -1574,8 +1660,9 @@ export class WunderbaumNode {
1574
1660
  /** Make sure that this node is visible in the viewport.
1575
1661
  * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo()}
1576
1662
  */
1577
- async scrollIntoView(options?: any) {
1578
- return this.tree.scrollTo(this);
1663
+ async scrollIntoView(options?: ScrollIntoViewOptions) {
1664
+ const opts = Object.assign({ node: this }, options);
1665
+ return this.tree.scrollTo(opts);
1579
1666
  }
1580
1667
 
1581
1668
  /**
@@ -1584,31 +1671,29 @@ export class WunderbaumNode {
1584
1671
  async setActive(flag: boolean = true, options?: SetActiveOptions) {
1585
1672
  const tree = this.tree;
1586
1673
  const prev = tree.activeNode;
1587
- const retrigger = options?.retrigger;
1588
- const noEvents = options?.noEvents;
1674
+ const retrigger = options?.retrigger; // Default: false
1675
+ const focusTree = options?.focusTree; // Default: false
1676
+ const focusNode = options?.focusNode !== false; // Default: true
1677
+ const noEvents = options?.noEvents; // Default: false
1678
+ const orgEvent = options?.event; // Default: false
1589
1679
 
1590
1680
  if (!noEvents) {
1591
- let orgEvent = options?.event;
1592
1681
  if (flag) {
1593
1682
  if (prev !== this || retrigger) {
1594
1683
  if (
1595
1684
  prev?._callEvent("deactivate", {
1596
1685
  nextNode: this,
1597
1686
  orgEvent: orgEvent,
1598
- }) === false
1599
- ) {
1600
- return;
1601
- }
1602
- if (
1603
- this._callEvent("activate", {
1687
+ }) === false ||
1688
+ this._callEvent("beforeActivate", {
1604
1689
  prevNode: prev,
1605
1690
  orgEvent: orgEvent,
1606
1691
  }) === false
1607
1692
  ) {
1608
- tree.activeNode = null;
1609
- prev?.setModified();
1610
1693
  return;
1611
1694
  }
1695
+ tree.activeNode = null;
1696
+ prev?.setModified(ChangeType.status);
1612
1697
  }
1613
1698
  } else if (prev === this || retrigger) {
1614
1699
  this._callEvent("deactivate", { nextNode: null, orgEvent: orgEvent });
@@ -1616,7 +1701,11 @@ export class WunderbaumNode {
1616
1701
  }
1617
1702
 
1618
1703
  if (prev !== this) {
1619
- tree.activeNode = this;
1704
+ if (flag) {
1705
+ tree.activeNode = this;
1706
+ if (focusNode || focusTree) tree.focusNode = this;
1707
+ if (focusTree) tree.setFocus();
1708
+ }
1620
1709
  prev?.setModified(ChangeType.status);
1621
1710
  this.setModified(ChangeType.status);
1622
1711
  }
@@ -1624,42 +1713,52 @@ export class WunderbaumNode {
1624
1713
  options &&
1625
1714
  options.colIdx != null &&
1626
1715
  options.colIdx !== tree.activeColIdx &&
1627
- tree.navMode !== NavigationMode.row
1716
+ tree.isCellNav()
1628
1717
  ) {
1629
1718
  tree.setColumn(options.colIdx);
1630
1719
  }
1631
- // requestAnimationFrame(() => {
1632
- // this.scrollIntoView();
1633
- // })
1634
- return this.scrollIntoView();
1720
+ if (flag && !noEvents) {
1721
+ this._callEvent("activate", { prevNode: prev, orgEvent: orgEvent });
1722
+ }
1723
+ return this.makeVisible();
1635
1724
  }
1636
1725
 
1637
1726
  /**
1638
1727
  * Expand or collapse this node.
1639
1728
  */
1640
1729
  async setExpanded(flag: boolean = true, options?: SetExpandedOptions) {
1641
- // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
1642
1730
  if (
1643
1731
  !flag &&
1644
1732
  this.isExpanded() &&
1645
- this.getLevel() < this.getOption("minExpandLevel") &&
1733
+ this.getLevel() <= this.tree.getOption("minExpandLevel") &&
1646
1734
  !util.getOption(options, "force")
1647
1735
  ) {
1648
1736
  this.logDebug("Ignored collapse request below expandLevel.");
1649
1737
  return;
1650
1738
  }
1739
+ if (!flag === !this.expanded) {
1740
+ return; // Nothing to do
1741
+ }
1651
1742
  if (flag && this.lazy && this.children == null) {
1652
1743
  await this.loadLazy();
1653
1744
  }
1654
1745
  this.expanded = flag;
1655
- this.tree.setModified(ChangeType.structure);
1746
+ const updateOpts = { immediate: !!util.getOption(options, "immediate") };
1747
+ this.tree.setModified(ChangeType.structure, updateOpts);
1748
+ if (util.getOption(options, "scrollIntoView") !== false) {
1749
+ const lastChild = this.getLastChild();
1750
+ if (lastChild) {
1751
+ lastChild.scrollIntoView({ topNode: this });
1752
+ }
1753
+ }
1656
1754
  }
1657
1755
 
1658
1756
  /**
1659
1757
  * Set keyboard focus here.
1660
1758
  * @see {@link setActive}
1661
1759
  */
1662
- setFocus(flag: boolean = true, options?: any) {
1760
+ setFocus(flag: boolean = true) {
1761
+ util.assert(!!flag, "blur is not yet implemented");
1663
1762
  const prev = this.tree.focusNode;
1664
1763
  this.tree.focusNode = this;
1665
1764
  prev?.setModified();
@@ -1702,10 +1801,12 @@ export class WunderbaumNode {
1702
1801
  /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
1703
1802
  setStatus(
1704
1803
  status: NodeStatusType,
1705
- message?: string,
1706
- details?: string
1804
+ options?: SetStatusOptions
1707
1805
  ): WunderbaumNode | null {
1708
1806
  const tree = this.tree;
1807
+ const message = options?.message;
1808
+ const details = options?.details;
1809
+
1709
1810
  let statusNode: WunderbaumNode | null = null;
1710
1811
 
1711
1812
  const _clearStatusNode = () => {
@@ -1823,9 +1924,9 @@ export class WunderbaumNode {
1823
1924
  * @param {object} [extra]
1824
1925
  */
1825
1926
  triggerModify(operation: string, extra?: any) {
1826
- if (!this.parent) {
1827
- return;
1828
- }
1927
+ // if (!this.parent) {
1928
+ // return;
1929
+ // }
1829
1930
  this.parent.triggerModifyChild(operation, this, extra);
1830
1931
  }
1831
1932