wunderbaum 0.12.1 → 0.14.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/wunderbaum.ts CHANGED
@@ -25,15 +25,13 @@ 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,
31
+ IconMapType,
32
+ GetStateOptions,
36
33
  MatcherCallback,
34
+ NavigationType,
37
35
  NavModeEnum,
38
36
  NodeFilterCallback,
39
37
  NodeRegion,
@@ -46,27 +44,38 @@ import {
46
44
  ScrollToOptions,
47
45
  SetActiveOptions,
48
46
  SetColumnOptions,
47
+ SetStateOptions,
49
48
  SetStatusOptions,
50
- SortByPropertyOptions,
51
49
  SortCallback,
52
50
  SourceType,
51
+ TreeStateDefinition,
53
52
  UpdateOptions,
54
53
  VisitRowsOptions,
55
54
  WbEventInfo,
56
55
  WbNodeData,
56
+ FilterOptionsType,
57
+ EditOptionsType,
58
+ DndOptionsType,
59
+ SortOptions,
60
+ DeprecationOptions,
61
+ SortByPropertyOptions,
62
+ ReloadOptions,
63
+ LoadLazyNodesOptions,
57
64
  } from "./types";
58
65
  import {
59
66
  DEFAULT_DEBUGLEVEL,
60
- iconMaps,
67
+ defaultIconMaps,
61
68
  makeNodeTitleStartMatcher,
62
69
  nodeTitleSorter,
63
70
  RENDER_MAX_PREFETCH,
64
71
  DEFAULT_ROW_HEIGHT,
72
+ TEST_FILE_PATH,
73
+ TEST_HTML,
65
74
  } from "./common";
66
75
  import { WunderbaumNode } from "./wb_node";
67
76
  import { Deferred } from "./deferred";
68
77
  import { EditExtension } from "./wb_ext_edit";
69
- import { WunderbaumOptions } from "./wb_options";
78
+ import { InitWunderbaumOptions, WunderbaumOptions } from "./wb_options";
70
79
  import { DebouncedFunction } from "./debounce";
71
80
 
72
81
  class WbSystemRoot extends WunderbaumNode {
@@ -110,7 +119,7 @@ export class Wunderbaum {
110
119
 
111
120
  protected readonly _updateViewportThrottled: DebouncedFunction<() => void>;
112
121
  protected extensionList: WunderbaumExtension<any>[] = [];
113
- protected extensions: ExtensionsDict = {};
122
+ protected extensions: ExtensionsDict = <ExtensionsDict>{};
114
123
 
115
124
  /** Merged options from constructor args and tree- and extension defaults. */
116
125
  public options: WunderbaumOptions;
@@ -123,6 +132,7 @@ export class Wunderbaum {
123
132
 
124
133
  protected _activeNode: WunderbaumNode | null = null;
125
134
  protected _focusNode: WunderbaumNode | null = null;
135
+ protected _initialSource: SourceType | null = null;
126
136
 
127
137
  /** Currently active node if any.
128
138
  * Use {@link WunderbaumNode.setActive|setActive} to modify.
@@ -142,18 +152,7 @@ export class Wunderbaum {
142
152
  /** Shared properties, referenced by `node.type`. */
143
153
  public types: NodeTypeDefinitionMap = {};
144
154
  /** List of column definitions. */
145
- public columns: ColumnDefinitionList = []; // any[] = [];
146
- /** Show/hide a checkbox or radiobutton. */
147
- public checkbox?: DynamicCheckboxOption;
148
- /** Show/hide a node icon. */
149
- public icon?: DynamicIconOption;
150
- /** Show/hide a tooltip for the node icon. */
151
- public iconTooltip?: DynamicStringOption;
152
- /** Show/hide a tooltip. */
153
- public tooltip?: DynamicTooltipOption;
154
- /** Define a node checkbox as readonly. */
155
- public unselectable?: DynamicBoolOption;
156
-
155
+ public columns: ColumnDefinitionList = [];
157
156
  protected _columnsById: { [key: string]: any } = {};
158
157
  protected resizeObserver: ResizeObserver;
159
158
 
@@ -164,13 +163,28 @@ export class Wunderbaum {
164
163
  public readonly ready: Promise<any>;
165
164
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
166
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;
167
178
  /** Expose some useful methods of the util.ts module as `tree._util`. */
168
179
  public _util = util;
169
180
 
170
181
  // --- SELECT ---
171
- // /** @internal */
172
182
  // public selectRangeAnchor: WunderbaumNode | null = null;
173
183
 
184
+ // --- BREADCRUMB ---
185
+ /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
186
+ public breadcrumb: HTMLElement | null = null;
187
+
174
188
  // --- FILTER ---
175
189
  /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
176
190
  public filterMode: FilterModeType = null;
@@ -188,49 +202,65 @@ export class Wunderbaum {
188
202
  // --- EDIT ---
189
203
  protected lastClickTime = 0;
190
204
 
191
- constructor(options: WunderbaumOptions) {
192
- 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
+ >(
193
211
  {
194
- id: null,
195
- source: null, // URL for GET/PUT, Ajax options, or callback
196
- element: null, // <div class="wunderbaum">
212
+ id: undefined,
213
+ source: [], // URL for GET/PUT, Ajax options, or callback
214
+ element: util.unsafeCast<string>(null),
197
215
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
198
216
  header: null, // Show/hide header (pass bool or string)
199
- // headerHeightPx: ROW_HEIGHT,
200
217
  rowHeightPx: DEFAULT_ROW_HEIGHT,
201
218
  iconMap: "bootstrap",
202
- columns: null,
203
- types: null,
204
- // escapeTitles: true,
219
+ columns: [], //util.unsafeCast<ColumnDefinitionList>(null),
220
+ types: {},
205
221
  enabled: true,
206
222
  fixedCol: false,
207
223
  showSpinner: false,
208
224
  checkbox: false,
209
225
  minExpandLevel: 0,
210
226
  emptyChildListExpandable: false,
211
- // updateThrottleWait: 200,
212
227
  skeleton: false,
213
- connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
228
+ autoCollapse: false,
229
+ adjustHeight: true,
230
+ connectTopBreadcrumb: null,
231
+ columnsFilterable: false,
232
+ columnsMenu: false,
233
+ columnsResizable: false,
234
+ columnsSortable: false,
214
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),
215
241
  // --- KeyNav ---
216
- navigationModeOption: null, // NavModeEnum.startRow,
242
+ navigationModeOption: util.unsafeCast<NavModeEnum>(null),
217
243
  quicksearch: true,
218
244
  // --- Events ---
219
- iconBadge: null,
220
- change: null,
221
- // enhanceTitle: null,
222
- error: null,
223
- receive: null,
245
+ // iconBadge: null,
246
+ // change: null,
247
+ // ...
248
+
224
249
  // --- Strings ---
225
250
  strings: {
226
251
  loadError: "Error",
227
252
  loading: "Loading...",
228
- // loading: "Loading&hellip;",
229
253
  noData: "No data",
254
+ breadcrumbDelimiter: " » ",
255
+ queryResult: "Found ${matches} of ${count}",
256
+ noMatch: "No results",
257
+ matchIndex: "${match} of ${matches}",
230
258
  },
231
259
  },
232
260
  options
233
- ));
261
+ );
262
+ const opts = initOptions as WunderbaumOptions;
263
+ this.options = opts;
234
264
 
235
265
  const readyDeferred = new Deferred();
236
266
  this.ready = readyDeferred.promise();
@@ -257,7 +287,8 @@ export class Wunderbaum {
257
287
  }
258
288
  });
259
289
 
260
- this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
290
+ this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence;
291
+ delete initOptions.id;
261
292
  this.root = new WbSystemRoot(this);
262
293
 
263
294
  this._registerExtension(new KeynavExtension(this));
@@ -273,21 +304,25 @@ export class Wunderbaum {
273
304
  );
274
305
 
275
306
  // --- Evaluate options
276
- this.columns = opts.columns;
277
- delete opts.columns;
307
+ this.columns = initOptions.columns || [];
308
+ delete initOptions.columns;
278
309
  if (!this.columns || !this.columns.length) {
279
310
  const title = typeof opts.header === "string" ? opts.header : this.id;
280
311
  this.columns = [{ id: "*", title: title, width: "*" }];
281
312
  }
282
313
 
283
- if (opts.types) {
284
- this.setTypes(opts.types, true);
314
+ if (initOptions.types) {
315
+ this.setTypes(initOptions.types, true);
285
316
  }
286
- delete opts.types;
317
+ delete initOptions.types;
287
318
 
288
319
  // --- Create Markup
289
- this.element = util.elemFromSelector<HTMLDivElement>(opts.element)!;
290
- 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;
291
326
 
292
327
  this.element.classList.add("wunderbaum");
293
328
  if (!this.element.getAttribute("tabindex")) {
@@ -368,21 +403,39 @@ export class Wunderbaum {
368
403
 
369
404
  this.element.classList.toggle("wb-grid", this.columns.length > 1);
370
405
 
406
+ if (this.options.connectTopBreadcrumb) {
407
+ this.breadcrumb = util.elemFromSelector(
408
+ this.options.connectTopBreadcrumb
409
+ )!;
410
+ util.assert(
411
+ !this.breadcrumb || this.breadcrumb.innerHTML != null,
412
+ `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`
413
+ );
414
+ this.breadcrumb.addEventListener("click", (e) => {
415
+ // const node = Wunderbaum.getNode(e)!;
416
+ const elem = e.target as HTMLElement;
417
+ if (elem && elem.matches("a.wb-breadcrumb")) {
418
+ const node = this.keyMap.get(elem.dataset.key!);
419
+ node?.setActive();
420
+ e.preventDefault();
421
+ }
422
+ });
423
+ }
371
424
  this._initExtensions();
372
425
 
373
426
  // --- apply initial options
374
427
  ["enabled", "fixedCol"].forEach((optName) => {
375
- if (opts[optName] != null) {
376
- this.setOption(optName, opts[optName]);
428
+ if ((opts as any)[optName] != null) {
429
+ this.setOption(optName, (opts as any)[optName]);
377
430
  }
378
431
  });
379
432
 
380
433
  // --- Load initial data
381
- if (opts.source) {
434
+ if (initOptions.source) {
382
435
  if (opts.showSpinner) {
383
436
  this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
384
437
  }
385
- this.load(opts.source)
438
+ this.load(initOptions.source)
386
439
  .then(() => {
387
440
  // The source may have defined columns, so we may adjust the nav mode
388
441
  if (opts.navigationModeOption == null) {
@@ -413,16 +466,20 @@ export class Wunderbaum {
413
466
  this.update(ChangeType.any);
414
467
 
415
468
  // --- Bind listeners
416
- this.element.addEventListener("scroll", (e: Event) => {
417
- // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
418
- this.update(ChangeType.scroll);
419
- });
469
+ this._registerEventHandlers();
420
470
 
421
471
  this.resizeObserver = new ResizeObserver((entries) => {
422
472
  // this.log("ResizeObserver: Size changed", entries);
423
473
  this.update(ChangeType.resize);
424
474
  });
425
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
+ });
426
483
 
427
484
  util.onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
428
485
  const info = Wunderbaum.getEventInfo(e);
@@ -442,10 +499,6 @@ export class Wunderbaum {
442
499
 
443
500
  // this.log("click", info);
444
501
 
445
- // if (this._selectRange(info) === false) {
446
- // return;
447
- // }
448
-
449
502
  if (
450
503
  this._callEvent("click", { event: e, node: node, info: info }) === false
451
504
  ) {
@@ -469,18 +522,18 @@ export class Wunderbaum {
469
522
  node.startEditTitle();
470
523
  }
471
524
 
472
- if (info.colIdx >= 0) {
473
- node.setActive(true, { colIdx: info.colIdx, event: e });
474
- } else {
475
- node.setActive(true, { event: e });
476
- }
477
-
478
525
  if (info.region === NodeRegion.expander) {
479
526
  node.setExpanded(!node.isExpanded(), {
480
- scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
527
+ scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
481
528
  });
482
529
  } else if (info.region === NodeRegion.checkbox) {
483
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
+ }
484
537
  }
485
538
  }
486
539
  this.lastClickTime = Date.now();
@@ -528,7 +581,7 @@ export class Wunderbaum {
528
581
  this._callEvent("focus", { flag: flag, event: e });
529
582
 
530
583
  if (flag && this.isRowNav() && !this.isEditingTitle()) {
531
- if ((opts.navigationModeOption as NavModeEnum) === NavModeEnum.row) {
584
+ if (this.options.navigationModeOption === NavModeEnum.row) {
532
585
  targetNode?.setActive();
533
586
  } else {
534
587
  this.setCellNav();
@@ -542,7 +595,6 @@ export class Wunderbaum {
542
595
  }
543
596
  });
544
597
  }
545
-
546
598
  /**
547
599
  * Return a Wunderbaum instance, from element, id, index, or event.
548
600
  *
@@ -596,14 +648,16 @@ export class Wunderbaum {
596
648
 
597
649
  /**
598
650
  * Return the icon-function -> icon-definition mapping.
651
+ * @deprecated Use {@link Wunderbaum.iconMaps}
599
652
  */
600
- get iconMap(): { [key: string]: string } {
601
- const map = this.options.iconMap!;
653
+ get iconMap(): IconMapType {
654
+ const map = this.options.iconMap;
602
655
  if (typeof map === "string") {
603
- return iconMaps[map];
656
+ return defaultIconMaps[map as keyof typeof defaultIconMaps];
604
657
  }
605
658
  return map;
606
659
  }
660
+
607
661
  /**
608
662
  * Return a WunderbaumNode instance from element or event.
609
663
  */
@@ -656,7 +710,42 @@ export class Wunderbaum {
656
710
  }
657
711
  }
658
712
 
659
- /** 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 */
660
749
  _registerNode(node: WunderbaumNode): void {
661
750
  const key = node.key;
662
751
  util.assert(key != null, `Missing key: '${node}'.`);
@@ -673,7 +762,7 @@ export class Wunderbaum {
673
762
  }
674
763
  }
675
764
 
676
- /** Remove node from tree's bookkeeping data structures. */
765
+ /** Remove node from tree's bookkeeping data structures. @internal */
677
766
  _unregisterNode(node: WunderbaumNode): void {
678
767
  // Remove refKey reference from map (if any)
679
768
  const rk = node.refKey;
@@ -771,9 +860,12 @@ export class Wunderbaum {
771
860
  return <WunderbaumNode>node!;
772
861
  }
773
862
 
774
- /** Return the topmost visible node in the viewport. */
863
+ /** Return the topmost visible node in the viewport.
864
+ * @param complete If `false`, the node is considered visible if at least one
865
+ * pixel is visible.
866
+ */
775
867
  getTopmostVpNode(complete = true) {
776
- const rowHeight = this.options.rowHeightPx!;
868
+ const rowHeight = this.options.rowHeightPx;
777
869
  const gracePx = 1; // ignore subpixel scrolling
778
870
  const scrollParent = this.element;
779
871
  // const headerHeight = this.headerElement.clientHeight; // May be 0
@@ -790,7 +882,7 @@ export class Wunderbaum {
790
882
 
791
883
  /** Return the lowest visible node in the viewport. */
792
884
  getLowestVpNode(complete = true) {
793
- const rowHeight = this.options.rowHeightPx!;
885
+ const rowHeight = this.options.rowHeightPx;
794
886
  const scrollParent = this.element;
795
887
  const headerHeight = this.headerElement.clientHeight; // May be 0
796
888
  const scrollTop = scrollParent.scrollTop;
@@ -806,7 +898,7 @@ export class Wunderbaum {
806
898
  return this._getNodeByRowIdx(bottomIdx)!;
807
899
  }
808
900
 
809
- /** Return preceeding visible node in the viewport. */
901
+ /** Return preceding visible node in the viewport. */
810
902
  protected _getPrevNodeInView(node?: WunderbaumNode, ofs = 1) {
811
903
  this.visitRows(
812
904
  (n) => {
@@ -821,15 +913,28 @@ export class Wunderbaum {
821
913
  }
822
914
 
823
915
  /** Return following visible node in the viewport. */
824
- protected _getNextNodeInView(node?: WunderbaumNode, ofs = 1) {
916
+ protected _getNextNodeInView(
917
+ node?: WunderbaumNode,
918
+ options?: {
919
+ ofs?: number;
920
+ reverse?: boolean;
921
+ cb?: (n: WunderbaumNode) => boolean;
922
+ }
923
+ ) {
924
+ let ofs = options?.ofs || 1;
925
+ const reverse = !!options?.reverse;
926
+
825
927
  this.visitRows(
826
928
  (n) => {
827
929
  node = n;
930
+ if (options?.cb && options.cb(n)) {
931
+ return false;
932
+ }
828
933
  if (ofs-- <= 0) {
829
934
  return false;
830
935
  }
831
936
  },
832
- { reverse: false, start: node || this.getActiveNode() }
937
+ { reverse: reverse, start: node || this.getActiveNode() }
833
938
  );
834
939
  return node;
835
940
  }
@@ -972,9 +1077,11 @@ export class Wunderbaum {
972
1077
  case "first":
973
1078
  case "last":
974
1079
  case "left":
1080
+ case "nextMatch":
975
1081
  case "pageDown":
976
1082
  case "pageUp":
977
1083
  case "parent":
1084
+ case "prevMatch":
978
1085
  case "right":
979
1086
  case "up":
980
1087
  return node.navigate(cmd);
@@ -1103,24 +1210,40 @@ export class Wunderbaum {
1103
1210
  /** Run code, but defer rendering of viewport until done.
1104
1211
  *
1105
1212
  * ```js
1106
- * tree.runWithDeferredUpdate(() => {
1107
- * return someFuncThatWouldUpdateManyNodes();
1213
+ * const res = tree.runWithDeferredUpdate(() => {
1214
+ * return someFunctionThatWouldUpdateManyNodes();
1108
1215
  * });
1109
1216
  * ```
1110
1217
  */
1111
- runWithDeferredUpdate(func: () => any, hint = null): any {
1218
+ runWithDeferredUpdate<T>(func: () => util.NotPromise<T>): T {
1112
1219
  try {
1113
1220
  this.enableUpdate(false);
1114
1221
  const res = func();
1115
1222
  util.assert(
1116
1223
  !(res instanceof Promise),
1117
- `Promise return not allowed: ${res}`
1224
+ `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`
1118
1225
  );
1119
1226
  return res;
1120
1227
  } finally {
1121
1228
  this.enableUpdate(true);
1122
1229
  }
1123
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
+ }
1124
1247
 
1125
1248
  /** Recursively expand all expandable nodes (triggers lazy load if needed). */
1126
1249
  async expandAll(flag: boolean = true, options?: ExpandAllOptions) {
@@ -1145,6 +1268,18 @@ export class Wunderbaum {
1145
1268
  return this.root.getSelectedNodes(stopOnParents);
1146
1269
  }
1147
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
+
1148
1283
  /*
1149
1284
  * Return an array of selected nodes.
1150
1285
  */
@@ -1190,6 +1325,12 @@ export class Wunderbaum {
1190
1325
  return visible ? this.treeRowCount : this.keyMap.size;
1191
1326
  }
1192
1327
 
1328
+ /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`.
1329
+ */
1330
+ countUnique(): number {
1331
+ return this.refKeyMap.size;
1332
+ }
1333
+
1193
1334
  /** @internal sanity check. */
1194
1335
  _check() {
1195
1336
  let i = 0;
@@ -1255,15 +1396,18 @@ export class Wunderbaum {
1255
1396
  */
1256
1397
  findNextNode(
1257
1398
  match: string | MatcherCallback,
1258
- startNode?: WunderbaumNode | null
1399
+ startNode?: WunderbaumNode | null,
1400
+ reverse = false
1259
1401
  ): WunderbaumNode | null {
1260
1402
  //, visibleOnly) {
1261
1403
  let res: WunderbaumNode | null = null;
1262
1404
  const firstNode = this.getFirstChild()!;
1405
+ // Last visible node (calculation is expensive, so do only if we need it):
1406
+ const lastNode = reverse ? this.findRelatedNode(firstNode, "last")! : null;
1263
1407
 
1264
1408
  const matcher =
1265
1409
  typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
1266
- startNode = startNode || firstNode;
1410
+ startNode = startNode || (reverse ? lastNode : firstNode);
1267
1411
 
1268
1412
  function _checkNode(n: WunderbaumNode) {
1269
1413
  // console.log("_check " + n)
@@ -1277,12 +1421,14 @@ export class Wunderbaum {
1277
1421
  this.visitRows(_checkNode, {
1278
1422
  start: startNode,
1279
1423
  includeSelf: false,
1424
+ reverse: reverse,
1280
1425
  });
1281
1426
  // Wrap around search
1282
1427
  if (!res && startNode !== firstNode) {
1283
1428
  this.visitRows(_checkNode, {
1284
- start: firstNode,
1429
+ start: reverse ? lastNode : firstNode,
1285
1430
  includeSelf: true,
1431
+ reverse: reverse,
1286
1432
  });
1287
1433
  }
1288
1434
  return res;
@@ -1297,8 +1443,12 @@ export class Wunderbaum {
1297
1443
  * e.g. `$.ui.keyCode.LEFT` = 'left'.
1298
1444
  * @param includeHidden Not yet implemented
1299
1445
  */
1300
- findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) {
1301
- const rowHeight = this.options.rowHeightPx!;
1446
+ findRelatedNode(
1447
+ node: WunderbaumNode,
1448
+ where: NavigationType,
1449
+ includeHidden = false
1450
+ ) {
1451
+ const rowHeight = this.options.rowHeightPx;
1302
1452
  let res = null;
1303
1453
  const pageSize = Math.floor(
1304
1454
  this.listContainerElement.clientHeight / rowHeight
@@ -1353,7 +1503,7 @@ export class Wunderbaum {
1353
1503
  // }
1354
1504
  break;
1355
1505
  case "up":
1356
- res = this._getPrevNodeInView(node);
1506
+ res = this._getNextNodeInView(node, { reverse: true });
1357
1507
  break;
1358
1508
  case "down":
1359
1509
  res = this._getNextNodeInView(node);
@@ -1366,7 +1516,10 @@ export class Wunderbaum {
1366
1516
  if (node._rowIdx! < bottomNode._rowIdx!) {
1367
1517
  res = bottomNode;
1368
1518
  } else {
1369
- res = this._getNextNodeInView(node, pageSize);
1519
+ res = this._getNextNodeInView(node, {
1520
+ reverse: false,
1521
+ ofs: pageSize,
1522
+ });
1370
1523
  }
1371
1524
  }
1372
1525
  break;
@@ -1380,10 +1533,28 @@ export class Wunderbaum {
1380
1533
  if (node._rowIdx! > topNode._rowIdx!) {
1381
1534
  res = topNode;
1382
1535
  } else {
1383
- res = this._getPrevNodeInView(node, pageSize);
1536
+ res = this._getNextNodeInView(node, {
1537
+ reverse: true,
1538
+ ofs: pageSize,
1539
+ });
1384
1540
  }
1385
1541
  }
1386
1542
  break;
1543
+
1544
+ case "prevMatch":
1545
+ // fallthrough
1546
+ case "nextMatch":
1547
+ if (!this.isFilterActive) {
1548
+ this.logWarn(`${where}: Filter is not active.`);
1549
+ break;
1550
+ }
1551
+ res = this.findNextNode(
1552
+ (n) => n.isMatched(),
1553
+ node,
1554
+ where === "prevMatch"
1555
+ );
1556
+ res?.setActive();
1557
+ break;
1387
1558
  default:
1388
1559
  this.logWarn("Unknown relation '" + where + "'.");
1389
1560
  }
@@ -1424,6 +1595,20 @@ export class Wunderbaum {
1424
1595
  return this.root.format(name_cb, connectors);
1425
1596
  }
1426
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
+
1427
1612
  /**
1428
1613
  * Return the active cell (`span.wb-col`) of the currently active node or null.
1429
1614
  */
@@ -1454,6 +1639,13 @@ export class Wunderbaum {
1454
1639
  return this.root.getFirstChild();
1455
1640
  }
1456
1641
 
1642
+ /**
1643
+ * Return the last top level node if any (not the invisible root node).
1644
+ */
1645
+ getLastChild() {
1646
+ return this.root.getLastChild();
1647
+ }
1648
+
1457
1649
  /**
1458
1650
  * Return the node that currently has keyboard focus or null.
1459
1651
  * Alias for {@link Wunderbaum.focusNode}.
@@ -1539,7 +1731,7 @@ export class Wunderbaum {
1539
1731
 
1540
1732
  /** Return true if any node title or grid cell is currently beeing edited.
1541
1733
  *
1542
- * See also {@link Wunderbaum.isEditingTitle}.
1734
+ * See also {@link isEditingTitle}.
1543
1735
  */
1544
1736
  isEditing(): boolean {
1545
1737
  const focusElem = this.nodeListElement.querySelector(
@@ -1550,7 +1742,7 @@ export class Wunderbaum {
1550
1742
 
1551
1743
  /** Return true if any node is currently in edit-title mode.
1552
1744
  *
1553
- * See also {@link WunderbaumNode.isEditingTitle} and {@link Wunderbaum.isEditing}.
1745
+ * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
1554
1746
  */
1555
1747
  isEditingTitle(): boolean {
1556
1748
  return this._callMethod("edit.isEditingTitle");
@@ -1573,7 +1765,7 @@ export class Wunderbaum {
1573
1765
  }
1574
1766
 
1575
1767
  /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
1576
- * @see {@link Wunderbaum.logDebug}
1768
+ * @see {@link logDebug}
1577
1769
  */
1578
1770
  log(...args: any[]) {
1579
1771
  if (this.options.debugLevel! >= 4) {
@@ -1583,7 +1775,7 @@ export class Wunderbaum {
1583
1775
 
1584
1776
  /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
1585
1777
  * and browser console level includes debug/verbose messages.
1586
- * @see {@link Wunderbaum.log}
1778
+ * @see {@link log}
1587
1779
  */
1588
1780
  logDebug(...args: any[]) {
1589
1781
  if (this.options.debugLevel! >= 4) {
@@ -1627,6 +1819,20 @@ export class Wunderbaum {
1627
1819
  }
1628
1820
  }
1629
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
+
1630
1836
  /** Reset column widths to default. @since 0.10.0 */
1631
1837
  resetColumns() {
1632
1838
  this.columns.forEach((col) => {
@@ -1661,7 +1867,7 @@ export class Wunderbaum {
1661
1867
  }
1662
1868
  util.assert(node && node._rowIdx != null, `Invalid node: ${node}`);
1663
1869
 
1664
- const rowHeight = this.options.rowHeightPx!;
1870
+ const rowHeight = this.options.rowHeightPx;
1665
1871
  const scrollParent = this.element;
1666
1872
  const headerHeight = this.headerElement.clientHeight; // May be 0
1667
1873
  const scrollTop = scrollParent.scrollTop;
@@ -1816,6 +2022,75 @@ export class Wunderbaum {
1816
2022
  this._focusNode = node;
1817
2023
  }
1818
2024
 
2025
+ /** Return the current selection/expansion/activation status. @experimental */
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) {
2036
+ for (const node of this) {
2037
+ if (node.isExpanded() && node.hasChildren()) {
2038
+ expandSet.add(node.key);
2039
+ }
2040
+ }
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
+ }
2050
+
2051
+ const state: TreeStateDefinition = {
2052
+ expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
2053
+ activeKey: this.activeNode?.key ?? null,
2054
+ activeColIdx: this.activeColIdx,
2055
+ selectedKeys: selectedKeys
2056
+ ? this.getSelectedNodes().flatMap((n) => n.key)
2057
+ : undefined,
2058
+ };
2059
+ return state;
2060
+ }
2061
+
2062
+ /** Apply selection/expansion/activation status. @experimental */
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
+ }
2082
+ if (state.selectedKeys) {
2083
+ this.selectAll(false);
2084
+ for (const key of state.selectedKeys) {
2085
+ this.findKey(key)?.setSelected(true);
2086
+ }
2087
+ }
2088
+ if (this.isCellNav() && state.activeColIdx != null) {
2089
+ this.setColumn(state.activeColIdx);
2090
+ }
2091
+ });
2092
+ }
2093
+
1819
2094
  /**
1820
2095
  * Schedule an update request to reflect a tree change.
1821
2096
  * The render operation is async and debounced unless the `immediate` option
@@ -2011,23 +2286,39 @@ export class Wunderbaum {
2011
2286
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
2012
2287
  * (defaults to sorting by title).
2013
2288
  * @param {boolean} deep pass true to sort all descendant nodes recursively
2289
+ * @deprecated use {@link sort}
2014
2290
  */
2015
2291
  sortChildren(
2016
2292
  cmp: SortCallback | null = nodeTitleSorter,
2017
2293
  deep: boolean = false
2018
2294
  ): void {
2019
- 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
+ });
2020
2301
  }
2021
2302
 
2022
2303
  /**
2023
2304
  * Convenience method to implement column sorting.
2024
2305
  * @see {@link WunderbaumNode.sortByProperty}.
2025
2306
  * @since 0.11.0
2307
+ * @deprecated use {@link sort}
2026
2308
  */
2027
2309
  sortByProperty(options: SortByPropertyOptions) {
2310
+ this.logDeprecate("sortByProperty()", { since: "0.14.0" });
2028
2311
  this.root.sortByProperty(options);
2029
2312
  }
2030
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
+
2031
2322
  /** Convert tree to an array of plain objects.
2032
2323
  *
2033
2324
  * @param callback is called for every node, in order to allow
@@ -2149,11 +2440,11 @@ export class Wunderbaum {
2149
2440
  return modified;
2150
2441
  }
2151
2442
 
2152
- protected _insertIcon(icon: string, elem: HTMLElement) {
2153
- const iconElem = document.createElement("i");
2154
- iconElem.className = icon;
2155
- elem.appendChild(iconElem);
2156
- }
2443
+ // protected _insertIcon(icon: string, elem: HTMLElement) {
2444
+ // const iconElem = document.createElement("i");
2445
+ // iconElem.className = icon;
2446
+ // elem.appendChild(iconElem);
2447
+ // }
2157
2448
 
2158
2449
  /** Create/update header markup from `this.columns` definition.
2159
2450
  * @internal
@@ -2257,6 +2548,109 @@ export class Wunderbaum {
2257
2548
  }
2258
2549
  }
2259
2550
 
2551
+ /** @internal */
2552
+ public _createNodeIcon(
2553
+ node: WunderbaumNode,
2554
+ showLoading: boolean,
2555
+ showBadge: boolean
2556
+ ): HTMLElement | null {
2557
+ const iconMap = this.iconMap;
2558
+ let iconElem;
2559
+ let icon = node.getOption("icon");
2560
+ if (node._errorInfo) {
2561
+ icon = iconMap.error;
2562
+ } else if (node._isLoading && showLoading) {
2563
+ // Status nodes, or nodes without expander (< minExpandLevel) should
2564
+ // display the 'loading' status with the i.wb-icon span
2565
+ icon = iconMap.loading;
2566
+ }
2567
+ if (icon === false) {
2568
+ return null; // explicitly disabled: don't try default icons
2569
+ }
2570
+ if (typeof icon === "string") {
2571
+ // Callback returned an icon definition
2572
+ // icon = icon.trim()
2573
+ } else if (node.statusNodeType) {
2574
+ icon = (<any>iconMap)[node.statusNodeType];
2575
+ } else if (node.expanded) {
2576
+ icon = iconMap.folderOpen;
2577
+ } else if (node.children) {
2578
+ icon = iconMap.folder;
2579
+ } else if (node.lazy) {
2580
+ icon = iconMap.folderLazy;
2581
+ } else {
2582
+ icon = iconMap.doc;
2583
+ }
2584
+
2585
+ if (!icon) {
2586
+ iconElem = document.createElement("i");
2587
+ iconElem.className = "wb-icon";
2588
+ } else if (TEST_HTML.test(icon)) {
2589
+ iconElem = util.elemFromHtml(icon);
2590
+ } else if (TEST_FILE_PATH.test(icon)) {
2591
+ iconElem = util.elemFromHtml(
2592
+ `<i class="wb-icon" style="background-image: url('${icon}');">`
2593
+ );
2594
+ } else {
2595
+ // Class name
2596
+ iconElem = document.createElement("i");
2597
+ iconElem.className = "wb-icon " + icon;
2598
+ }
2599
+
2600
+ // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
2601
+ const cbRes =
2602
+ showBadge && node._callEvent("iconBadge", { iconSpan: iconElem });
2603
+
2604
+ let badge = null;
2605
+ if (cbRes != null && cbRes !== false) {
2606
+ let classes = "";
2607
+ let tooltip = "";
2608
+ if (util.isPlainObject(cbRes)) {
2609
+ badge = "" + cbRes.badge;
2610
+ classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
2611
+ tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
2612
+ } else if (typeof cbRes === "number") {
2613
+ badge = "" + cbRes;
2614
+ } else {
2615
+ badge = cbRes; // string or HTMLSpanElement
2616
+ }
2617
+ if (typeof badge === "string") {
2618
+ badge = util.elemFromHtml(
2619
+ `<span class="wb-badge${classes}"${tooltip}>${util.escapeHtml(
2620
+ badge
2621
+ )}</span>`
2622
+ );
2623
+ }
2624
+ if (badge) {
2625
+ iconElem.append(<HTMLSpanElement>badge);
2626
+ }
2627
+ }
2628
+ return iconElem;
2629
+ }
2630
+
2631
+ private _updateTopBreadcrumb() {
2632
+ const breadcrumb = this.breadcrumb!;
2633
+ const topmost = this.getTopmostVpNode(true);
2634
+ const parentList = topmost?.getParentList(false, false);
2635
+ if (parentList?.length) {
2636
+ breadcrumb.innerHTML = "";
2637
+ for (const n of topmost.getParentList(false, false)) {
2638
+ const icon = this._createNodeIcon(n, false, false);
2639
+ if (icon) {
2640
+ breadcrumb.append(icon, " ");
2641
+ }
2642
+ const part = document.createElement("a");
2643
+ part.textContent = n.title;
2644
+ part.href = "#";
2645
+ part.classList.add("wb-breadcrumb");
2646
+ part.dataset.key = n.key;
2647
+ breadcrumb.append(part, this.options.strings.breadcrumbDelimiter);
2648
+ }
2649
+ } else {
2650
+ breadcrumb.innerHTML = "&nbsp;";
2651
+ }
2652
+ }
2653
+
2260
2654
  /**
2261
2655
  * This is the actual update method, which is wrapped inside a throttle method.
2262
2656
  * It calls `updateColumns()` and `_updateRows()`.
@@ -2321,14 +2715,8 @@ export class Wunderbaum {
2321
2715
  // console.profileEnd(`_updateViewportImmediately()`)
2322
2716
  }
2323
2717
 
2324
- if (this.options.connectTopBreadcrumb) {
2325
- util.assert(
2326
- this.options.connectTopBreadcrumb.textContent != null,
2327
- `Invalid 'connectTopBreadcrumb' option (input element expected).`
2328
- );
2329
- let path = this.getTopmostVpNode(true)?.getPath(false, "title", " > ");
2330
- path = path ? path + " >" : "";
2331
- this.options.connectTopBreadcrumb.textContent = path;
2718
+ if (this.breadcrumb) {
2719
+ this._updateTopBreadcrumb();
2332
2720
  }
2333
2721
  this._callEvent("update");
2334
2722
  }
@@ -2380,7 +2768,7 @@ export class Wunderbaum {
2380
2768
  options = Object.assign({ newNodesOnly: false }, options);
2381
2769
  const newNodesOnly = !!options.newNodesOnly;
2382
2770
 
2383
- const rowHeight = this.options.rowHeightPx!;
2771
+ const rowHeight = this.options.rowHeightPx;
2384
2772
  const vpHeight = this.element.clientHeight;
2385
2773
  const prefetch = RENDER_MAX_PREFETCH;
2386
2774
  // const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH;
@@ -2460,7 +2848,8 @@ export class Wunderbaum {
2460
2848
 
2461
2849
  /**
2462
2850
  * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
2463
- * @see {@link IterableIterator<WunderbaumNode>}, {@link WunderbaumNode.visit}.
2851
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
2852
+ * @see {@link WunderbaumNode.visit}.
2464
2853
  *
2465
2854
  * @param {function} callback the callback function.
2466
2855
  * Return false to stop iteration, return "skip" to skip this node and
@@ -2632,12 +3021,77 @@ export class Wunderbaum {
2632
3021
  *
2633
3022
  * Previous data is cleared. Note that also column- and type defintions may
2634
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.
2635
3026
  */
2636
- load(source: SourceType) {
3027
+ async load(source: SourceType) {
2637
3028
  this.clear();
3029
+ this._initialSource = source;
2638
3030
  return this.root.load(source);
2639
3031
  }
2640
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
+
2641
3095
  /**
2642
3096
  * Disable render requests during operations that would trigger many updates.
2643
3097
  *
@@ -2705,10 +3159,7 @@ export class Wunderbaum {
2705
3159
  filter: string | RegExp | NodeFilterCallback,
2706
3160
  options: FilterNodesOptions
2707
3161
  ): number {
2708
- return (this.extensions.filter as FilterExtension).filterNodes(
2709
- filter,
2710
- options
2711
- );
3162
+ return this.extensions.filter.filterNodes(filter, options);
2712
3163
  }
2713
3164
 
2714
3165
  /**
@@ -2717,7 +3168,7 @@ export class Wunderbaum {
2717
3168
  * @since 0.9.0
2718
3169
  */
2719
3170
  countMatches(): number {
2720
- return (this.extensions.filter as FilterExtension).countMatches();
3171
+ return this.extensions.filter.countMatches();
2721
3172
  }
2722
3173
 
2723
3174
  /**
@@ -2728,17 +3179,14 @@ export class Wunderbaum {
2728
3179
  filter: string | NodeFilterCallback,
2729
3180
  options: FilterNodesOptions
2730
3181
  ) {
2731
- return (this.extensions.filter as FilterExtension).filterBranches(
2732
- filter,
2733
- options
2734
- );
3182
+ return this.extensions.filter.filterBranches(filter, options);
2735
3183
  }
2736
3184
 
2737
3185
  /**
2738
3186
  * Reset the filter.
2739
3187
  */
2740
3188
  clearFilter() {
2741
- return (this.extensions.filter as FilterExtension).clearFilter();
3189
+ return this.extensions.filter.clearFilter();
2742
3190
  }
2743
3191
  /**
2744
3192
  * Return true if a filter is currently applied.
@@ -2750,6 +3198,6 @@ export class Wunderbaum {
2750
3198
  * Re-apply current filter.
2751
3199
  */
2752
3200
  updateFilter() {
2753
- return (this.extensions.filter as FilterExtension).updateFilter();
3201
+ return this.extensions.filter.updateFilter();
2754
3202
  }
2755
3203
  }