wunderbaum 0.13.0 → 0.14.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
@@ -25,11 +25,6 @@ import {
25
25
  ApplyCommandType,
26
26
  ChangeType,
27
27
  ColumnDefinitionList,
28
- DynamicBoolOption,
29
- DynamicCheckboxOption,
30
- DynamicIconOption,
31
- DynamicStringOption,
32
- DynamicTooltipOption,
33
28
  ExpandAllOptions,
34
29
  FilterModeType,
35
30
  FilterNodesOptions,
@@ -51,7 +46,6 @@ import {
51
46
  SetColumnOptions,
52
47
  SetStateOptions,
53
48
  SetStatusOptions,
54
- SortByPropertyOptions,
55
49
  SortCallback,
56
50
  SourceType,
57
51
  TreeStateDefinition,
@@ -59,20 +53,29 @@ import {
59
53
  VisitRowsOptions,
60
54
  WbEventInfo,
61
55
  WbNodeData,
56
+ FilterOptionsType,
57
+ EditOptionsType,
58
+ DndOptionsType,
59
+ SortOptions,
60
+ DeprecationOptions,
61
+ SortByPropertyOptions,
62
+ ReloadOptions,
63
+ LoadLazyNodesOptions,
62
64
  } from "./types";
63
65
  import {
64
66
  DEFAULT_DEBUGLEVEL,
65
- iconMaps,
67
+ defaultIconMaps,
66
68
  makeNodeTitleStartMatcher,
67
69
  nodeTitleSorter,
68
70
  RENDER_MAX_PREFETCH,
69
71
  DEFAULT_ROW_HEIGHT,
70
- TEST_IMG,
72
+ TEST_FILE_PATH,
73
+ TEST_HTML,
71
74
  } from "./common";
72
75
  import { WunderbaumNode } from "./wb_node";
73
76
  import { Deferred } from "./deferred";
74
77
  import { EditExtension } from "./wb_ext_edit";
75
- import { WunderbaumOptions } from "./wb_options";
78
+ import { InitWunderbaumOptions, WunderbaumOptions } from "./wb_options";
76
79
  import { DebouncedFunction } from "./debounce";
77
80
 
78
81
  class WbSystemRoot extends WunderbaumNode {
@@ -116,7 +119,7 @@ export class Wunderbaum {
116
119
 
117
120
  protected readonly _updateViewportThrottled: DebouncedFunction<() => void>;
118
121
  protected extensionList: WunderbaumExtension<any>[] = [];
119
- protected extensions: ExtensionsDict = {};
122
+ protected extensions: ExtensionsDict = <ExtensionsDict>{};
120
123
 
121
124
  /** Merged options from constructor args and tree- and extension defaults. */
122
125
  public options: WunderbaumOptions;
@@ -129,6 +132,7 @@ export class Wunderbaum {
129
132
 
130
133
  protected _activeNode: WunderbaumNode | null = null;
131
134
  protected _focusNode: WunderbaumNode | null = null;
135
+ protected _initialSource: SourceType | null = null;
132
136
 
133
137
  /** Currently active node if any.
134
138
  * Use {@link WunderbaumNode.setActive|setActive} to modify.
@@ -148,18 +152,7 @@ export class Wunderbaum {
148
152
  /** Shared properties, referenced by `node.type`. */
149
153
  public types: NodeTypeDefinitionMap = {};
150
154
  /** List of column definitions. */
151
- public columns: ColumnDefinitionList = []; // any[] = [];
152
- /** Show/hide a checkbox or radiobutton. */
153
- public checkbox?: DynamicCheckboxOption;
154
- /** Show/hide a node icon. */
155
- public icon?: DynamicIconOption;
156
- /** Show/hide a tooltip for the node icon. */
157
- public iconTooltip?: DynamicStringOption;
158
- /** Show/hide a tooltip. */
159
- public tooltip?: DynamicTooltipOption;
160
- /** Define a node checkbox as readonly. */
161
- public unselectable?: DynamicBoolOption;
162
-
155
+ public columns: ColumnDefinitionList = [];
163
156
  protected _columnsById: { [key: string]: any } = {};
164
157
  protected resizeObserver: ResizeObserver;
165
158
 
@@ -170,11 +163,22 @@ export class Wunderbaum {
170
163
  public readonly ready: Promise<any>;
171
164
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
172
165
  public static util = util;
166
+ /** A map of default iconMaps.
167
+ * May be used as default, when passing partial icon definition maps:
168
+ * ```js
169
+ * const tree = new mar10.Wunderbaum({
170
+ * ...
171
+ * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
172
+ * folder: "bi bi-archive",
173
+ * }),
174
+ * });
175
+ * ```
176
+ */
177
+ public static iconMaps = defaultIconMaps;
173
178
  /** Expose some useful methods of the util.ts module as `tree._util`. */
174
179
  public _util = util;
175
180
 
176
181
  // --- SELECT ---
177
- // /** @internal */
178
182
  // public selectRangeAnchor: WunderbaumNode | null = null;
179
183
 
180
184
  // --- BREADCRUMB ---
@@ -198,39 +202,50 @@ export class Wunderbaum {
198
202
  // --- EDIT ---
199
203
  protected lastClickTime = 0;
200
204
 
201
- constructor(options: WunderbaumOptions) {
202
- const opts = (this.options = util.extend(
205
+ constructor(options: InitWunderbaumOptions) {
206
+ // Set default options and merge with user options
207
+ const initOptions = Object.assign<
208
+ InitWunderbaumOptions,
209
+ InitWunderbaumOptions
210
+ >(
203
211
  {
204
- id: null,
205
- source: null, // URL for GET/PUT, Ajax options, or callback
206
- element: null, // <div class="wunderbaum">
212
+ id: undefined,
213
+ source: [], // URL for GET/PUT, Ajax options, or callback
214
+ element: util.unsafeCast<string>(null),
207
215
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
208
216
  header: null, // Show/hide header (pass bool or string)
209
- // headerHeightPx: ROW_HEIGHT,
210
217
  rowHeightPx: DEFAULT_ROW_HEIGHT,
211
218
  iconMap: "bootstrap",
212
- columns: null,
213
- types: null,
214
- // escapeTitles: true,
219
+ columns: [], //util.unsafeCast<ColumnDefinitionList>(null),
220
+ types: {},
215
221
  enabled: true,
216
222
  fixedCol: false,
217
223
  showSpinner: false,
218
224
  checkbox: false,
219
225
  minExpandLevel: 0,
220
226
  emptyChildListExpandable: false,
221
- // updateThrottleWait: 200,
222
227
  skeleton: false,
228
+ autoCollapse: false,
229
+ adjustHeight: true,
223
230
  connectTopBreadcrumb: null,
231
+ columnsFilterable: false,
232
+ columnsMenu: false,
233
+ columnsResizable: false,
234
+ columnsSortable: false,
224
235
  selectMode: "multi", // SelectModeType
236
+ scrollIntoViewOnExpandClick: true,
237
+ // --- Extensions (actually set by exensions on init)
238
+ dnd: util.unsafeCast<DndOptionsType>(null),
239
+ edit: util.unsafeCast<EditOptionsType>(null),
240
+ filter: util.unsafeCast<FilterOptionsType>(null),
225
241
  // --- KeyNav ---
226
- navigationModeOption: null, // NavModeEnum,
242
+ navigationModeOption: util.unsafeCast<NavModeEnum>(null),
227
243
  quicksearch: true,
228
244
  // --- Events ---
229
- iconBadge: null,
230
- change: null,
231
- // enhanceTitle: null,
232
- error: null,
233
- receive: null,
245
+ // iconBadge: null,
246
+ // change: null,
247
+ // ...
248
+
234
249
  // --- Strings ---
235
250
  strings: {
236
251
  loadError: "Error",
@@ -243,7 +258,9 @@ export class Wunderbaum {
243
258
  },
244
259
  },
245
260
  options
246
- ));
261
+ );
262
+ const opts = initOptions as WunderbaumOptions;
263
+ this.options = opts;
247
264
 
248
265
  const readyDeferred = new Deferred();
249
266
  this.ready = readyDeferred.promise();
@@ -270,7 +287,8 @@ export class Wunderbaum {
270
287
  }
271
288
  });
272
289
 
273
- this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
290
+ this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence;
291
+ delete initOptions.id;
274
292
  this.root = new WbSystemRoot(this);
275
293
 
276
294
  this._registerExtension(new KeynavExtension(this));
@@ -286,21 +304,25 @@ export class Wunderbaum {
286
304
  );
287
305
 
288
306
  // --- Evaluate options
289
- this.columns = opts.columns;
290
- delete opts.columns;
307
+ this.columns = initOptions.columns || [];
308
+ delete initOptions.columns;
291
309
  if (!this.columns || !this.columns.length) {
292
310
  const title = typeof opts.header === "string" ? opts.header : this.id;
293
311
  this.columns = [{ id: "*", title: title, width: "*" }];
294
312
  }
295
313
 
296
- if (opts.types) {
297
- this.setTypes(opts.types, true);
314
+ if (initOptions.types) {
315
+ this.setTypes(initOptions.types, true);
298
316
  }
299
- delete opts.types;
317
+ delete initOptions.types;
300
318
 
301
319
  // --- Create Markup
302
- this.element = util.elemFromSelector<HTMLDivElement>(opts.element)!;
303
- util.assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
320
+ this.element = util.elemFromSelector<HTMLDivElement>(initOptions.element)!;
321
+ util.assert(
322
+ !!this.element,
323
+ `Invalid 'element' option: ${initOptions.element}`
324
+ );
325
+ delete (<any>initOptions).element;
304
326
 
305
327
  this.element.classList.add("wunderbaum");
306
328
  if (!this.element.getAttribute("tabindex")) {
@@ -403,17 +425,17 @@ export class Wunderbaum {
403
425
 
404
426
  // --- apply initial options
405
427
  ["enabled", "fixedCol"].forEach((optName) => {
406
- if (opts[optName] != null) {
407
- this.setOption(optName, opts[optName]);
428
+ if ((opts as any)[optName] != null) {
429
+ this.setOption(optName, (opts as any)[optName]);
408
430
  }
409
431
  });
410
432
 
411
433
  // --- Load initial data
412
- if (opts.source) {
434
+ if (initOptions.source) {
413
435
  if (opts.showSpinner) {
414
436
  this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
415
437
  }
416
- this.load(opts.source)
438
+ this.load(initOptions.source)
417
439
  .then(() => {
418
440
  // The source may have defined columns, so we may adjust the nav mode
419
441
  if (opts.navigationModeOption == null) {
@@ -444,16 +466,20 @@ export class Wunderbaum {
444
466
  this.update(ChangeType.any);
445
467
 
446
468
  // --- Bind listeners
447
- this.element.addEventListener("scroll", (e: Event) => {
448
- // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
449
- this.update(ChangeType.scroll);
450
- });
469
+ this._registerEventHandlers();
451
470
 
452
471
  this.resizeObserver = new ResizeObserver((entries) => {
453
472
  // this.log("ResizeObserver: Size changed", entries);
454
473
  this.update(ChangeType.resize);
455
474
  });
456
475
  this.resizeObserver.observe(this.element);
476
+ }
477
+
478
+ private _registerEventHandlers() {
479
+ this.element.addEventListener("scroll", (e: Event) => {
480
+ // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
481
+ this.update(ChangeType.scroll);
482
+ });
457
483
 
458
484
  util.onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
459
485
  const info = Wunderbaum.getEventInfo(e);
@@ -473,10 +499,6 @@ export class Wunderbaum {
473
499
 
474
500
  // this.log("click", info);
475
501
 
476
- // if (this._selectRange(info) === false) {
477
- // return;
478
- // }
479
-
480
502
  if (
481
503
  this._callEvent("click", { event: e, node: node, info: info }) === false
482
504
  ) {
@@ -500,18 +522,18 @@ export class Wunderbaum {
500
522
  node.startEditTitle();
501
523
  }
502
524
 
503
- if (info.colIdx >= 0) {
504
- node.setActive(true, { colIdx: info.colIdx, event: e });
505
- } else {
506
- node.setActive(true, { event: e });
507
- }
508
-
509
525
  if (info.region === NodeRegion.expander) {
510
526
  node.setExpanded(!node.isExpanded(), {
511
- scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
527
+ scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
512
528
  });
513
529
  } else if (info.region === NodeRegion.checkbox) {
514
530
  node.toggleSelected();
531
+ } else {
532
+ if (info.colIdx >= 0) {
533
+ node.setActive(true, { colIdx: info.colIdx, event: e });
534
+ } else {
535
+ node.setActive(true, { event: e });
536
+ }
515
537
  }
516
538
  }
517
539
  this.lastClickTime = Date.now();
@@ -559,7 +581,7 @@ export class Wunderbaum {
559
581
  this._callEvent("focus", { flag: flag, event: e });
560
582
 
561
583
  if (flag && this.isRowNav() && !this.isEditingTitle()) {
562
- if ((opts.navigationModeOption as NavModeEnum) === NavModeEnum.row) {
584
+ if (this.options.navigationModeOption === NavModeEnum.row) {
563
585
  targetNode?.setActive();
564
586
  } else {
565
587
  this.setCellNav();
@@ -573,7 +595,6 @@ export class Wunderbaum {
573
595
  }
574
596
  });
575
597
  }
576
-
577
598
  /**
578
599
  * Return a Wunderbaum instance, from element, id, index, or event.
579
600
  *
@@ -627,14 +648,16 @@ export class Wunderbaum {
627
648
 
628
649
  /**
629
650
  * Return the icon-function -> icon-definition mapping.
651
+ * @deprecated Use {@link Wunderbaum.iconMaps}
630
652
  */
631
653
  get iconMap(): IconMapType {
632
- const map = this.options.iconMap!;
654
+ const map = this.options.iconMap;
633
655
  if (typeof map === "string") {
634
- return iconMaps[map];
656
+ return defaultIconMaps[map as keyof typeof defaultIconMaps];
635
657
  }
636
658
  return map;
637
659
  }
660
+
638
661
  /**
639
662
  * Return a WunderbaumNode instance from element or event.
640
663
  */
@@ -687,7 +710,42 @@ export class Wunderbaum {
687
710
  }
688
711
  }
689
712
 
690
- /** Add node to tree's bookkeeping data structures. */
713
+ /**
714
+ * Calculate a *stable*, unique key for a node from its refKey (or title).
715
+ * We also add information from the parent, because a refKey may occur multiple
716
+ * times in a tree (but not as child of the same parent).
717
+ * @internal
718
+ */
719
+ _calculateKey(data: WbNodeData, parent?: WunderbaumNode): string {
720
+ if (data.key) {
721
+ // Always use an explicitly passed key
722
+ return data.key;
723
+ }
724
+ // Auto-keys are optional, use a monotonic counter by default:
725
+ if (!this.options.autoKeys) {
726
+ return "" + ++WunderbaumNode.sequence;
727
+ }
728
+ // Add the parent's key to the hash. Assuming this was generated by the
729
+ // same algorithm, this should incorporate the whole path:
730
+ const s = (parent ? parent.key : "") + (data.refKey || data.title);
731
+ // 32-bit has a high probability of collisions, so we pump up to 64-bit
732
+ // https://security.stackexchange.com/q/209882/207588
733
+ const h1 = util.murmurHash3(s, true);
734
+ let key = "id_" + h1 + util.murmurHash3(h1 + s, true);
735
+ // Check for collisions
736
+ // (Most likely if the same title occurs multiple in the same parent).
737
+ const existingNode = this.keyMap.get(key);
738
+ if (existingNode) {
739
+ key += "." + ++Wunderbaum.sequence;
740
+ this.logWarn(
741
+ `Node with existing key: '${existingNode}', using ${key}.`,
742
+ data
743
+ );
744
+ }
745
+ return key;
746
+ }
747
+
748
+ /** Add node to tree's bookkeeping data structures. @internal */
691
749
  _registerNode(node: WunderbaumNode): void {
692
750
  const key = node.key;
693
751
  util.assert(key != null, `Missing key: '${node}'.`);
@@ -704,7 +762,7 @@ export class Wunderbaum {
704
762
  }
705
763
  }
706
764
 
707
- /** Remove node from tree's bookkeeping data structures. */
765
+ /** Remove node from tree's bookkeeping data structures. @internal */
708
766
  _unregisterNode(node: WunderbaumNode): void {
709
767
  // Remove refKey reference from map (if any)
710
768
  const rk = node.refKey;
@@ -807,7 +865,7 @@ export class Wunderbaum {
807
865
  * pixel is visible.
808
866
  */
809
867
  getTopmostVpNode(complete = true) {
810
- const rowHeight = this.options.rowHeightPx!;
868
+ const rowHeight = this.options.rowHeightPx;
811
869
  const gracePx = 1; // ignore subpixel scrolling
812
870
  const scrollParent = this.element;
813
871
  // const headerHeight = this.headerElement.clientHeight; // May be 0
@@ -824,7 +882,7 @@ export class Wunderbaum {
824
882
 
825
883
  /** Return the lowest visible node in the viewport. */
826
884
  getLowestVpNode(complete = true) {
827
- const rowHeight = this.options.rowHeightPx!;
885
+ const rowHeight = this.options.rowHeightPx;
828
886
  const scrollParent = this.element;
829
887
  const headerHeight = this.headerElement.clientHeight; // May be 0
830
888
  const scrollTop = scrollParent.scrollTop;
@@ -840,6 +898,20 @@ export class Wunderbaum {
840
898
  return this._getNodeByRowIdx(bottomIdx)!;
841
899
  }
842
900
 
901
+ /** Return preceding visible node in the viewport. */
902
+ protected _getPrevNodeInView(node?: WunderbaumNode, ofs = 1) {
903
+ this.visitRows(
904
+ (n) => {
905
+ node = n;
906
+ if (ofs-- <= 0) {
907
+ return false;
908
+ }
909
+ },
910
+ { reverse: true, start: node || this.getActiveNode() }
911
+ );
912
+ return node;
913
+ }
914
+
843
915
  /** Return following visible node in the viewport. */
844
916
  protected _getNextNodeInView(
845
917
  node?: WunderbaumNode,
@@ -1138,24 +1210,40 @@ export class Wunderbaum {
1138
1210
  /** Run code, but defer rendering of viewport until done.
1139
1211
  *
1140
1212
  * ```js
1141
- * tree.runWithDeferredUpdate(() => {
1142
- * return someFuncThatWouldUpdateManyNodes();
1213
+ * const res = tree.runWithDeferredUpdate(() => {
1214
+ * return someFunctionThatWouldUpdateManyNodes();
1143
1215
  * });
1144
1216
  * ```
1145
1217
  */
1146
- runWithDeferredUpdate(func: () => any, hint = null): any {
1218
+ runWithDeferredUpdate<T>(func: () => util.NotPromise<T>): T {
1147
1219
  try {
1148
1220
  this.enableUpdate(false);
1149
1221
  const res = func();
1150
1222
  util.assert(
1151
1223
  !(res instanceof Promise),
1152
- `Promise return not allowed: ${res}`
1224
+ `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`
1153
1225
  );
1154
1226
  return res;
1155
1227
  } finally {
1156
1228
  this.enableUpdate(true);
1157
1229
  }
1158
1230
  }
1231
+ /** Run code, but defer rendering of viewport until done.
1232
+ *
1233
+ * ```js
1234
+ * const res = await tree.runWithDeferredUpdate(async () => {
1235
+ * return someAsyncFunctionThatWouldUpdateManyNodes();
1236
+ * });
1237
+ * ```
1238
+ */
1239
+ async runWithDeferredUpdateAsync<T>(func: () => Promise<T>): Promise<T> {
1240
+ try {
1241
+ this.enableUpdate(false);
1242
+ return await func();
1243
+ } finally {
1244
+ this.enableUpdate(true);
1245
+ }
1246
+ }
1159
1247
 
1160
1248
  /** Recursively expand all expandable nodes (triggers lazy load if needed). */
1161
1249
  async expandAll(flag: boolean = true, options?: ExpandAllOptions) {
@@ -1180,6 +1268,18 @@ export class Wunderbaum {
1180
1268
  return this.root.getSelectedNodes(stopOnParents);
1181
1269
  }
1182
1270
 
1271
+ /**
1272
+ * Return an array of refKey values.
1273
+ *
1274
+ * RefKeys are unique identifiers for a node data, and are used to identify
1275
+ * clones.
1276
+ * If more than one node has the same refKey, it is only returned once.
1277
+ * @param selected if true, only return refKeys of selected nodes.
1278
+ */
1279
+ getRefKeys(selected = false): string[] {
1280
+ return this.root.getRefKeys(selected);
1281
+ }
1282
+
1183
1283
  /*
1184
1284
  * Return an array of selected nodes.
1185
1285
  */
@@ -1348,7 +1448,7 @@ export class Wunderbaum {
1348
1448
  where: NavigationType,
1349
1449
  includeHidden = false
1350
1450
  ) {
1351
- const rowHeight = this.options.rowHeightPx!;
1451
+ const rowHeight = this.options.rowHeightPx;
1352
1452
  let res = null;
1353
1453
  const pageSize = Math.floor(
1354
1454
  this.listContainerElement.clientHeight / rowHeight
@@ -1495,6 +1595,20 @@ export class Wunderbaum {
1495
1595
  return this.root.format(name_cb, connectors);
1496
1596
  }
1497
1597
 
1598
+ /**
1599
+ * Always returns null (so a tree instance behaves as `tree.root`).
1600
+ */
1601
+ get parent(): null {
1602
+ return null;
1603
+ }
1604
+
1605
+ /**
1606
+ * Return a list of top-level nodes.
1607
+ */
1608
+ get children(): WunderbaumNode[] {
1609
+ return this.root.children || [];
1610
+ }
1611
+
1498
1612
  /**
1499
1613
  * Return the active cell (`span.wb-col`) of the currently active node or null.
1500
1614
  */
@@ -1617,7 +1731,7 @@ export class Wunderbaum {
1617
1731
 
1618
1732
  /** Return true if any node title or grid cell is currently beeing edited.
1619
1733
  *
1620
- * See also {@link Wunderbaum.isEditingTitle}.
1734
+ * See also {@link isEditingTitle}.
1621
1735
  */
1622
1736
  isEditing(): boolean {
1623
1737
  const focusElem = this.nodeListElement.querySelector(
@@ -1628,7 +1742,7 @@ export class Wunderbaum {
1628
1742
 
1629
1743
  /** Return true if any node is currently in edit-title mode.
1630
1744
  *
1631
- * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
1745
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
1632
1746
  */
1633
1747
  isEditingTitle(): boolean {
1634
1748
  return this._callMethod("edit.isEditingTitle");
@@ -1651,7 +1765,7 @@ export class Wunderbaum {
1651
1765
  }
1652
1766
 
1653
1767
  /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
1654
- * @see {@link Wunderbaum.logDebug}
1768
+ * @see {@link logDebug}
1655
1769
  */
1656
1770
  log(...args: any[]) {
1657
1771
  if (this.options.debugLevel! >= 4) {
@@ -1661,7 +1775,7 @@ export class Wunderbaum {
1661
1775
 
1662
1776
  /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
1663
1777
  * and browser console level includes debug/verbose messages.
1664
- * @see {@link Wunderbaum.log}
1778
+ * @see {@link log}
1665
1779
  */
1666
1780
  logDebug(...args: any[]) {
1667
1781
  if (this.options.debugLevel! >= 4) {
@@ -1705,6 +1819,20 @@ export class Wunderbaum {
1705
1819
  }
1706
1820
  }
1707
1821
 
1822
+ /** Emit a warning for deprecated methods. @internal */
1823
+ logDeprecate(method: string, options?: DeprecationOptions) {
1824
+ if (this.options.debugLevel! >= 2) {
1825
+ let msg = `${this}: ${method} is deprecated`;
1826
+ if (options?.since) {
1827
+ msg += ` since ${options.since}`;
1828
+ }
1829
+ if (options?.hint) {
1830
+ msg += ` (${options.since})`;
1831
+ }
1832
+ console.warn(msg + "."); // eslint-disable-line no-console
1833
+ }
1834
+ }
1835
+
1708
1836
  /** Reset column widths to default. @since 0.10.0 */
1709
1837
  resetColumns() {
1710
1838
  this.columns.forEach((col) => {
@@ -1739,7 +1867,7 @@ export class Wunderbaum {
1739
1867
  }
1740
1868
  util.assert(node && node._rowIdx != null, `Invalid node: ${node}`);
1741
1869
 
1742
- const rowHeight = this.options.rowHeightPx!;
1870
+ const rowHeight = this.options.rowHeightPx;
1743
1871
  const scrollParent = this.element;
1744
1872
  const headerHeight = this.headerElement.clientHeight; // May be 0
1745
1873
  const scrollTop = scrollParent.scrollTop;
@@ -1895,47 +2023,69 @@ export class Wunderbaum {
1895
2023
  }
1896
2024
 
1897
2025
  /** Return the current selection/expansion/activation status. @experimental */
1898
- getState(options: GetStateOptions): TreeStateDefinition {
1899
- let expandedKeys = undefined;
1900
- if (options.expandedKeys !== false) {
1901
- expandedKeys = [];
2026
+ getState(options: GetStateOptions = {}): TreeStateDefinition {
2027
+ const {
2028
+ activeKey = true,
2029
+ expandedKeys = false,
2030
+ selectedKeys = false,
2031
+ } = options;
2032
+
2033
+ const expandSet = new Set<string>();
2034
+
2035
+ if (expandedKeys) {
1902
2036
  for (const node of this) {
1903
- if (node.expanded) {
1904
- expandedKeys.push(node.key);
2037
+ if (node.isExpanded() && node.hasChildren()) {
2038
+ expandSet.add(node.key);
1905
2039
  }
1906
2040
  }
1907
2041
  }
2042
+ // Parents of active node are always expanded
2043
+ if (activeKey && this.activeNode) {
2044
+ this.activeNode.visitParents((n) => {
2045
+ if (n.parent) {
2046
+ expandSet.add(n.key);
2047
+ }
2048
+ }, false);
2049
+ }
1908
2050
 
1909
2051
  const state: TreeStateDefinition = {
2052
+ expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
1910
2053
  activeKey: this.activeNode?.key ?? null,
1911
2054
  activeColIdx: this.activeColIdx,
1912
- selectedKeys:
1913
- options.selectedKeys === false
1914
- ? undefined
1915
- : this.getSelectedNodes().flatMap((n) => n.key),
1916
- expandedKeys: expandedKeys,
2055
+ selectedKeys: selectedKeys
2056
+ ? this.getSelectedNodes().flatMap((n) => n.key)
2057
+ : undefined,
1917
2058
  };
1918
2059
  return state;
1919
2060
  }
1920
2061
 
1921
2062
  /** Apply selection/expansion/activation status. @experimental */
1922
- setState(state: TreeStateDefinition, options: SetStateOptions) {
1923
- this.runWithDeferredUpdate(() => {
2063
+ async setState(state: TreeStateDefinition, options: SetStateOptions = {}) {
2064
+ const { expandLazy = true } = options;
2065
+ return this.runWithDeferredUpdateAsync(async () => {
2066
+ if (state.expandedKeys && state.expandedKeys.length) {
2067
+ if (expandLazy) {
2068
+ // Expand all keys recursively, even if they are not in the tree yet
2069
+ await this._loadLazyNodes(state.expandedKeys, {
2070
+ expand: true,
2071
+ noEvents: true,
2072
+ });
2073
+ } else {
2074
+ for (const key of state.expandedKeys) {
2075
+ this.findKey(key)?.setExpanded(true);
2076
+ }
2077
+ }
2078
+ }
2079
+ if (state.activeKey) {
2080
+ this.setActiveNode(state.activeKey);
2081
+ }
1924
2082
  if (state.selectedKeys) {
1925
2083
  this.selectAll(false);
1926
2084
  for (const key of state.selectedKeys) {
1927
2085
  this.findKey(key)?.setSelected(true);
1928
2086
  }
1929
2087
  }
1930
- if (state.expandedKeys) {
1931
- for (const key of state.expandedKeys) {
1932
- this.findKey(key)?.setExpanded(true);
1933
- }
1934
- }
1935
- if (state.activeKey) {
1936
- this.setActiveNode(state.activeKey);
1937
- }
1938
- if (state.activeColIdx != null) {
2088
+ if (this.isCellNav() && state.activeColIdx != null) {
1939
2089
  this.setColumn(state.activeColIdx);
1940
2090
  }
1941
2091
  });
@@ -2136,23 +2286,39 @@ export class Wunderbaum {
2136
2286
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
2137
2287
  * (defaults to sorting by title).
2138
2288
  * @param {boolean} deep pass true to sort all descendant nodes recursively
2289
+ * @deprecated use {@link sort}
2139
2290
  */
2140
2291
  sortChildren(
2141
2292
  cmp: SortCallback | null = nodeTitleSorter,
2142
2293
  deep: boolean = false
2143
2294
  ): void {
2144
- this.root.sortChildren(cmp, deep);
2295
+ this.logDeprecate("sortChildren()", { since: "0.14.0" });
2296
+ return this.sort({
2297
+ cmp: cmp ? cmp : undefined,
2298
+ deep: deep,
2299
+ propName: "title",
2300
+ });
2145
2301
  }
2146
2302
 
2147
2303
  /**
2148
2304
  * Convenience method to implement column sorting.
2149
2305
  * @see {@link WunderbaumNode.sortByProperty}.
2150
2306
  * @since 0.11.0
2307
+ * @deprecated use {@link sort}
2151
2308
  */
2152
2309
  sortByProperty(options: SortByPropertyOptions) {
2310
+ this.logDeprecate("sortByProperty()", { since: "0.14.0" });
2153
2311
  this.root.sortByProperty(options);
2154
2312
  }
2155
2313
 
2314
+ /**
2315
+ * Sort nodes list by title or custom criteria.
2316
+ * @since 0.14.0
2317
+ */
2318
+ sort(options: SortOptions): void {
2319
+ this.root.sort(options);
2320
+ }
2321
+
2156
2322
  /** Convert tree to an array of plain objects.
2157
2323
  *
2158
2324
  * @param callback is called for every node, in order to allow
@@ -2419,11 +2585,9 @@ export class Wunderbaum {
2419
2585
  if (!icon) {
2420
2586
  iconElem = document.createElement("i");
2421
2587
  iconElem.className = "wb-icon";
2422
- } else if (icon.indexOf("<") >= 0) {
2423
- // HTML
2588
+ } else if (TEST_HTML.test(icon)) {
2424
2589
  iconElem = util.elemFromHtml(icon);
2425
- } else if (TEST_IMG.test(icon)) {
2426
- // Image URL
2590
+ } else if (TEST_FILE_PATH.test(icon)) {
2427
2591
  iconElem = util.elemFromHtml(
2428
2592
  `<i class="wb-icon" style="background-image: url('${icon}');">`
2429
2593
  );
@@ -2480,7 +2644,7 @@ export class Wunderbaum {
2480
2644
  part.href = "#";
2481
2645
  part.classList.add("wb-breadcrumb");
2482
2646
  part.dataset.key = n.key;
2483
- breadcrumb.append(part, this.options.strings!.breadcrumbDelimiter);
2647
+ breadcrumb.append(part, this.options.strings.breadcrumbDelimiter);
2484
2648
  }
2485
2649
  } else {
2486
2650
  breadcrumb.innerHTML = "&nbsp;";
@@ -2604,7 +2768,7 @@ export class Wunderbaum {
2604
2768
  options = Object.assign({ newNodesOnly: false }, options);
2605
2769
  const newNodesOnly = !!options.newNodesOnly;
2606
2770
 
2607
- const rowHeight = this.options.rowHeightPx!;
2771
+ const rowHeight = this.options.rowHeightPx;
2608
2772
  const vpHeight = this.element.clientHeight;
2609
2773
  const prefetch = RENDER_MAX_PREFETCH;
2610
2774
  // const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH;
@@ -2684,7 +2848,8 @@ export class Wunderbaum {
2684
2848
 
2685
2849
  /**
2686
2850
  * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
2687
- * @see {@link IterableIterator<WunderbaumNode>}, {@link WunderbaumNode.visit}.
2851
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
2852
+ * @see {@link WunderbaumNode.visit}.
2688
2853
  *
2689
2854
  * @param {function} callback the callback function.
2690
2855
  * Return false to stop iteration, return "skip" to skip this node and
@@ -2856,12 +3021,77 @@ export class Wunderbaum {
2856
3021
  *
2857
3022
  * Previous data is cleared. Note that also column- and type defintions may
2858
3023
  * be passed with the `source` object.
3024
+ * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request
3025
+ * and restore the previous state.
2859
3026
  */
2860
- load(source: SourceType) {
3027
+ async load(source: SourceType) {
2861
3028
  this.clear();
3029
+ this._initialSource = source;
2862
3030
  return this.root.load(source);
2863
3031
  }
2864
3032
 
3033
+ /** Reload the tree and optionally restore state.
3034
+ * Source defaults to last ajax url if any.
3035
+ * Restoring the active node requires stable keys
3036
+ * @see {@link WunderbaumOptions.autoKeys}
3037
+ * @see {@link Wunderbaum.load}
3038
+ * @experimental
3039
+ */
3040
+ async reload(options: ReloadOptions = {}) {
3041
+ const { source = this._initialSource, reactivate = true } = options;
3042
+ if (!source) {
3043
+ this.logWarn("No previous ajax source to reload.");
3044
+ return;
3045
+ }
3046
+ if (!reactivate) {
3047
+ return this.load(source);
3048
+ }
3049
+ const state = this.getState();
3050
+ await this.load(source);
3051
+ return this.setState(state);
3052
+ }
3053
+
3054
+ /**
3055
+ * Make sure that all nodes in the given keyList are accessible.
3056
+ * This may include loading lazy parent nodes.
3057
+ * Recursively load (and optionally expand) all requested node paths.
3058
+ */
3059
+ protected async _loadLazyNodes(
3060
+ keyList: string[],
3061
+ options: LoadLazyNodesOptions = {}
3062
+ ) {
3063
+ const { expand = true } = options;
3064
+ const keySet = new Set<string>(keyList);
3065
+
3066
+ // Make sure that all parent nodes are loaded (and expand if requested)
3067
+ while (keySet.size > 0) {
3068
+ const pendingNodes: Promise<void>[] = [];
3069
+ const curSet = new Set(keySet);
3070
+ for (const key of curSet) {
3071
+ const node = this.findKey(key);
3072
+ if (!node) {
3073
+ continue; // key not yet found (need to load lazy parent?)
3074
+ }
3075
+ keySet.delete(key);
3076
+ if (expand) {
3077
+ pendingNodes.push(node.setExpanded(true));
3078
+ } else if (node.isUnloaded()) {
3079
+ pendingNodes.push(node.loadLazy());
3080
+ }
3081
+ if (node._rowElem) {
3082
+ node._render(); // show spinner even is update is suppressed
3083
+ }
3084
+ }
3085
+ if (pendingNodes.length === 0) {
3086
+ // will not load any more nodes, so if if there are still keys
3087
+ // left in the set, we will never find them
3088
+ this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet);
3089
+ break;
3090
+ }
3091
+ await Promise.allSettled(pendingNodes);
3092
+ }
3093
+ }
3094
+
2865
3095
  /**
2866
3096
  * Disable render requests during operations that would trigger many updates.
2867
3097
  *
@@ -2929,10 +3159,7 @@ export class Wunderbaum {
2929
3159
  filter: string | RegExp | NodeFilterCallback,
2930
3160
  options: FilterNodesOptions
2931
3161
  ): number {
2932
- return (this.extensions.filter as FilterExtension).filterNodes(
2933
- filter,
2934
- options
2935
- );
3162
+ return this.extensions.filter.filterNodes(filter, options);
2936
3163
  }
2937
3164
 
2938
3165
  /**
@@ -2941,7 +3168,7 @@ export class Wunderbaum {
2941
3168
  * @since 0.9.0
2942
3169
  */
2943
3170
  countMatches(): number {
2944
- return (this.extensions.filter as FilterExtension).countMatches();
3171
+ return this.extensions.filter.countMatches();
2945
3172
  }
2946
3173
 
2947
3174
  /**
@@ -2952,17 +3179,14 @@ export class Wunderbaum {
2952
3179
  filter: string | NodeFilterCallback,
2953
3180
  options: FilterNodesOptions
2954
3181
  ) {
2955
- return (this.extensions.filter as FilterExtension).filterBranches(
2956
- filter,
2957
- options
2958
- );
3182
+ return this.extensions.filter.filterBranches(filter, options);
2959
3183
  }
2960
3184
 
2961
3185
  /**
2962
3186
  * Reset the filter.
2963
3187
  */
2964
3188
  clearFilter() {
2965
- return (this.extensions.filter as FilterExtension).clearFilter();
3189
+ return this.extensions.filter.clearFilter();
2966
3190
  }
2967
3191
  /**
2968
3192
  * Return true if a filter is currently applied.
@@ -2974,6 +3198,6 @@ export class Wunderbaum {
2974
3198
  * Re-apply current filter.
2975
3199
  */
2976
3200
  updateFilter() {
2977
- return (this.extensions.filter as FilterExtension).updateFilter();
3201
+ return this.extensions.filter.updateFilter();
2978
3202
  }
2979
3203
  }