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/LICENSE +1 -1
- package/dist/wunderbaum.css +40 -11
- package/dist/wunderbaum.css.map +1 -1
- package/dist/wunderbaum.d.ts +648 -306
- package/dist/wunderbaum.esm.js +920 -356
- package/dist/wunderbaum.esm.min.js +27 -27
- package/dist/wunderbaum.esm.min.js.map +1 -1
- package/dist/wunderbaum.umd.js +920 -356
- package/dist/wunderbaum.umd.min.js +31 -31
- package/dist/wunderbaum.umd.min.js.map +1 -1
- package/package.json +3 -2
- package/src/common.ts +49 -6
- package/src/types.ts +182 -33
- package/src/util.ts +75 -15
- package/src/wb_ext_dnd.ts +21 -20
- package/src/wb_ext_edit.ts +2 -2
- package/src/wb_ext_filter.ts +122 -43
- package/src/wb_ext_grid.ts +1 -1
- package/src/wb_extension_base.ts +19 -3
- package/src/wb_node.ts +239 -195
- package/src/wb_options.ts +172 -117
- package/src/wunderbaum.scss +9 -1
- package/src/wunderbaum.ts +574 -126
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
|
-
|
|
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 = [];
|
|
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:
|
|
192
|
-
|
|
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:
|
|
195
|
-
source:
|
|
196
|
-
element: null,
|
|
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:
|
|
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
|
-
|
|
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,
|
|
242
|
+
navigationModeOption: util.unsafeCast<NavModeEnum>(null),
|
|
217
243
|
quicksearch: true,
|
|
218
244
|
// --- Events ---
|
|
219
|
-
iconBadge: null,
|
|
220
|
-
change: null,
|
|
221
|
-
//
|
|
222
|
-
|
|
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…",
|
|
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 =
|
|
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 =
|
|
277
|
-
delete
|
|
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 (
|
|
284
|
-
this.setTypes(
|
|
314
|
+
if (initOptions.types) {
|
|
315
|
+
this.setTypes(initOptions.types, true);
|
|
285
316
|
}
|
|
286
|
-
delete
|
|
317
|
+
delete initOptions.types;
|
|
287
318
|
|
|
288
319
|
// --- Create Markup
|
|
289
|
-
this.element = util.elemFromSelector<HTMLDivElement>(
|
|
290
|
-
util.assert(
|
|
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 (
|
|
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(
|
|
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.
|
|
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 (
|
|
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():
|
|
601
|
-
const map = this.options.iconMap
|
|
653
|
+
get iconMap(): IconMapType {
|
|
654
|
+
const map = this.options.iconMap;
|
|
602
655
|
if (typeof map === "string") {
|
|
603
|
-
return
|
|
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
|
-
/**
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
1213
|
+
* const res = tree.runWithDeferredUpdate(() => {
|
|
1214
|
+
* return someFunctionThatWouldUpdateManyNodes();
|
|
1108
1215
|
* });
|
|
1109
1216
|
* ```
|
|
1110
1217
|
*/
|
|
1111
|
-
runWithDeferredUpdate(func: () =>
|
|
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(
|
|
1301
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
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 = " ";
|
|
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.
|
|
2325
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3201
|
+
return this.extensions.filter.updateFilter();
|
|
2754
3202
|
}
|
|
2755
3203
|
}
|