wunderbaum 0.0.3 → 0.0.6
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/README.md +3 -1
- package/dist/wunderbaum.css +1 -1
- package/dist/wunderbaum.d.ts +663 -332
- package/dist/wunderbaum.esm.js +1263 -694
- package/dist/wunderbaum.esm.min.js +34 -27
- package/dist/wunderbaum.esm.min.js.map +1 -1
- package/dist/wunderbaum.umd.js +1263 -694
- package/dist/wunderbaum.umd.min.js +38 -33
- package/dist/wunderbaum.umd.min.js.map +1 -1
- package/package.json +4 -4
- package/src/common.ts +24 -132
- package/src/deferred.ts +12 -2
- package/src/types.ts +470 -0
- package/src/util.ts +104 -7
- package/src/wb_ext_dnd.ts +12 -163
- package/src/wb_ext_edit.ts +12 -13
- package/src/wb_ext_filter.ts +17 -5
- package/src/wb_ext_keynav.ts +138 -35
- package/src/wb_extension_base.ts +3 -3
- package/src/wb_node.ts +425 -257
- package/src/wb_options.ts +104 -34
- package/src/wunderbaum.scss +253 -102
- package/src/wunderbaum.ts +526 -264
package/src/wunderbaum.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* wunderbaum.ts
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
4
|
+
* A treegrid control.
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
|
|
7
|
-
*
|
|
7
|
+
* https://github.com/mar10/wunderbaum
|
|
8
8
|
*
|
|
9
|
+
* Released under the MIT license.
|
|
9
10
|
* @version @VERSION
|
|
10
11
|
* @date @DATE
|
|
11
12
|
*/
|
|
@@ -18,30 +19,43 @@ import { LoggerExtension } from "./wb_ext_logger";
|
|
|
18
19
|
import { DndExtension } from "./wb_ext_dnd";
|
|
19
20
|
import { GridExtension } from "./wb_ext_grid";
|
|
20
21
|
import { ExtensionsDict, WunderbaumExtension } from "./wb_extension_base";
|
|
21
|
-
|
|
22
22
|
import {
|
|
23
|
-
|
|
23
|
+
ApplyCommandType,
|
|
24
24
|
ChangeType,
|
|
25
|
-
|
|
25
|
+
ColumnDefinitionList,
|
|
26
26
|
FilterModeType,
|
|
27
|
-
makeNodeTitleStartMatcher,
|
|
28
27
|
MatcherType,
|
|
29
|
-
|
|
28
|
+
NavigationOptions,
|
|
30
29
|
NodeStatusType,
|
|
30
|
+
NodeTypeDefinitions,
|
|
31
|
+
ScrollToOptions,
|
|
32
|
+
SetActiveOptions,
|
|
33
|
+
SetModifiedOptions,
|
|
34
|
+
SetStatusOptions,
|
|
35
|
+
TargetType as NodeRegion,
|
|
36
|
+
} from "./types";
|
|
37
|
+
import {
|
|
38
|
+
DEFAULT_DEBUGLEVEL,
|
|
39
|
+
makeNodeTitleStartMatcher,
|
|
31
40
|
RENDER_MAX_PREFETCH,
|
|
32
41
|
ROW_HEIGHT,
|
|
33
|
-
TargetType as NodeRegion,
|
|
34
|
-
ApplyCommandType,
|
|
35
42
|
} from "./common";
|
|
36
43
|
import { WunderbaumNode } from "./wb_node";
|
|
37
44
|
import { Deferred } from "./deferred";
|
|
38
|
-
import { DebouncedFunction, throttle } from "./debounce";
|
|
39
45
|
import { EditExtension } from "./wb_ext_edit";
|
|
40
46
|
import { WunderbaumOptions } from "./wb_options";
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
class WbSystemRoot extends WunderbaumNode {
|
|
49
|
+
constructor(tree: Wunderbaum) {
|
|
50
|
+
super(tree, <WunderbaumNode>(<unknown>null), {
|
|
51
|
+
key: "__root__",
|
|
52
|
+
title: tree.id,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
toString() {
|
|
56
|
+
return `WbSystemRoot@${this.key}<'${this.tree.id}'>`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
45
59
|
|
|
46
60
|
/**
|
|
47
61
|
* A persistent plain object or array.
|
|
@@ -50,6 +64,7 @@ import { WunderbaumOptions } from "./wb_options";
|
|
|
50
64
|
*/
|
|
51
65
|
export class Wunderbaum {
|
|
52
66
|
protected static sequence = 0;
|
|
67
|
+
protected enabled = true;
|
|
53
68
|
|
|
54
69
|
/** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
|
|
55
70
|
public static version: string = "@VERSION"; // Set to semver by 'grunt release'
|
|
@@ -61,15 +76,15 @@ export class Wunderbaum {
|
|
|
61
76
|
/** The `div` container element that was passed to the constructor. */
|
|
62
77
|
public readonly element: HTMLDivElement;
|
|
63
78
|
/** The `div.wb-header` element if any. */
|
|
64
|
-
public readonly headerElement: HTMLDivElement
|
|
79
|
+
public readonly headerElement: HTMLDivElement;
|
|
65
80
|
/** The `div.wb-scroll-container` element that contains the `nodeListElement`. */
|
|
66
|
-
public readonly
|
|
81
|
+
public readonly scrollContainerElement: HTMLDivElement;
|
|
67
82
|
/** The `div.wb-node-list` element that contains all visible div.wb-row child elements. */
|
|
68
83
|
public readonly nodeListElement: HTMLDivElement;
|
|
84
|
+
/** Contains additional data that was sent as response to an Ajax source load request. */
|
|
85
|
+
public readonly data: { [key: string]: any } = {};
|
|
69
86
|
|
|
70
|
-
protected readonly _updateViewportThrottled:
|
|
71
|
-
(...args: any) => void
|
|
72
|
-
>;
|
|
87
|
+
protected readonly _updateViewportThrottled: (...args: any) => void;
|
|
73
88
|
protected extensionList: WunderbaumExtension[] = [];
|
|
74
89
|
protected extensions: ExtensionsDict = {};
|
|
75
90
|
|
|
@@ -78,30 +93,25 @@ export class Wunderbaum {
|
|
|
78
93
|
|
|
79
94
|
protected keyMap = new Map<string, WunderbaumNode>();
|
|
80
95
|
protected refKeyMap = new Map<string, Set<WunderbaumNode>>();
|
|
81
|
-
// protected viewNodes = new Set<WunderbaumNode>();
|
|
82
96
|
protected treeRowCount = 0;
|
|
83
97
|
protected _disableUpdateCount = 0;
|
|
84
98
|
|
|
85
|
-
// protected eventHandlers : Array<function> = [];
|
|
86
|
-
|
|
87
99
|
/** Currently active node if any. */
|
|
88
100
|
public activeNode: WunderbaumNode | null = null;
|
|
89
101
|
/** Current node hat has keyboard focus if any. */
|
|
90
102
|
public focusNode: WunderbaumNode | null = null;
|
|
91
103
|
|
|
92
104
|
/** Shared properties, referenced by `node.type`. */
|
|
93
|
-
public types:
|
|
105
|
+
public types: NodeTypeDefinitions = {};
|
|
94
106
|
/** List of column definitions. */
|
|
95
|
-
public columns: any[] = [];
|
|
107
|
+
public columns: ColumnDefinitionList = []; // any[] = [];
|
|
96
108
|
|
|
97
109
|
protected _columnsById: { [key: string]: any } = {};
|
|
98
110
|
protected resizeObserver: ResizeObserver;
|
|
99
111
|
|
|
100
112
|
// Modification Status
|
|
101
|
-
// protected changedSince = 0;
|
|
102
|
-
// protected changes = new Set<ChangeType>();
|
|
103
|
-
// protected changedNodes = new Set<WunderbaumNode>();
|
|
104
113
|
protected changeRedrawRequestPending = false;
|
|
114
|
+
protected changeScrollRequestPending = false;
|
|
105
115
|
|
|
106
116
|
/** A Promise that is resolved when the tree was initialized (similar to `init(e)` event). */
|
|
107
117
|
public readonly ready: Promise<any>;
|
|
@@ -117,7 +127,7 @@ export class Wunderbaum {
|
|
|
117
127
|
/** @internal Use `setColumn()`/`getActiveColElem()`*/
|
|
118
128
|
public activeColIdx = 0;
|
|
119
129
|
/** @internal */
|
|
120
|
-
public
|
|
130
|
+
public _cellNavMode = false;
|
|
121
131
|
/** @internal */
|
|
122
132
|
public lastQuicksearchTime = 0;
|
|
123
133
|
/** @internal */
|
|
@@ -130,22 +140,25 @@ export class Wunderbaum {
|
|
|
130
140
|
let opts = (this.options = util.extend(
|
|
131
141
|
{
|
|
132
142
|
id: null,
|
|
133
|
-
source: null, // URL for GET/PUT,
|
|
143
|
+
source: null, // URL for GET/PUT, Ajax options, or callback
|
|
134
144
|
element: null, // <div class="wunderbaum">
|
|
135
145
|
debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
|
|
136
146
|
header: null, // Show/hide header (pass bool or string)
|
|
137
|
-
headerHeightPx: ROW_HEIGHT,
|
|
147
|
+
// headerHeightPx: ROW_HEIGHT,
|
|
138
148
|
rowHeightPx: ROW_HEIGHT,
|
|
139
149
|
columns: null,
|
|
140
150
|
types: null,
|
|
141
151
|
// escapeTitles: true,
|
|
152
|
+
enabled: true,
|
|
153
|
+
fixedCol: false,
|
|
142
154
|
showSpinner: false,
|
|
143
|
-
checkbox:
|
|
155
|
+
checkbox: false,
|
|
144
156
|
minExpandLevel: 0,
|
|
145
157
|
updateThrottleWait: 200,
|
|
146
158
|
skeleton: false,
|
|
159
|
+
connectTopBreadcrumb: null, // HTMLElement that receives the top nodes breadcrumb
|
|
147
160
|
// --- KeyNav ---
|
|
148
|
-
|
|
161
|
+
navigationModeOption: null, // NavigationOptions.startRow,
|
|
149
162
|
quicksearch: true,
|
|
150
163
|
// --- Events ---
|
|
151
164
|
change: util.noop,
|
|
@@ -189,10 +202,7 @@ export class Wunderbaum {
|
|
|
189
202
|
});
|
|
190
203
|
|
|
191
204
|
this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
|
|
192
|
-
this.root = new
|
|
193
|
-
key: "__root__",
|
|
194
|
-
// title: "__root__",
|
|
195
|
-
});
|
|
205
|
+
this.root = new WbSystemRoot(this);
|
|
196
206
|
|
|
197
207
|
this._registerExtension(new KeynavExtension(this));
|
|
198
208
|
this._registerExtension(new EditExtension(this));
|
|
@@ -201,41 +211,23 @@ export class Wunderbaum {
|
|
|
201
211
|
this._registerExtension(new GridExtension(this));
|
|
202
212
|
this._registerExtension(new LoggerExtension(this));
|
|
203
213
|
|
|
214
|
+
this._updateViewportThrottled = util.adaptiveThrottle(
|
|
215
|
+
this._updateViewportImmediately.bind(this),
|
|
216
|
+
{}
|
|
217
|
+
);
|
|
218
|
+
|
|
204
219
|
// --- Evaluate options
|
|
205
220
|
this.columns = opts.columns;
|
|
206
221
|
delete opts.columns;
|
|
207
|
-
if (!this.columns) {
|
|
208
|
-
|
|
209
|
-
this.columns = [{ id: "*", title:
|
|
222
|
+
if (!this.columns || !this.columns.length) {
|
|
223
|
+
const title = typeof opts.header === "string" ? opts.header : this.id;
|
|
224
|
+
this.columns = [{ id: "*", title: title, width: "*" }];
|
|
210
225
|
}
|
|
211
226
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// Convert `TYPE.classes` to a Set
|
|
215
|
-
for (let t of Object.values(this.types) as any) {
|
|
216
|
-
if (t.classes) {
|
|
217
|
-
t.classes = util.toSet(t.classes);
|
|
218
|
-
}
|
|
227
|
+
if (opts.types) {
|
|
228
|
+
this.setTypes(opts.types, true);
|
|
219
229
|
}
|
|
220
|
-
|
|
221
|
-
if (this.columns.length === 1) {
|
|
222
|
-
opts.navigationMode = NavigationModeOption.row;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
opts.navigationMode === NavigationModeOption.cell ||
|
|
227
|
-
opts.navigationMode === NavigationModeOption.startCell
|
|
228
|
-
) {
|
|
229
|
-
this.navMode = NavigationMode.cellNav;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
this._updateViewportThrottled = throttle(
|
|
233
|
-
() => {
|
|
234
|
-
this._updateViewport();
|
|
235
|
-
},
|
|
236
|
-
opts.updateThrottleWait,
|
|
237
|
-
{ leading: true, trailing: true }
|
|
238
|
-
);
|
|
230
|
+
delete opts.types;
|
|
239
231
|
|
|
240
232
|
// --- Create Markup
|
|
241
233
|
this.element = util.elemFromSelector(opts.element) as HTMLDivElement;
|
|
@@ -270,11 +262,14 @@ export class Wunderbaum {
|
|
|
270
262
|
) as HTMLDivElement;
|
|
271
263
|
for (const colDiv of rowElement.querySelectorAll("div")) {
|
|
272
264
|
this.columns.push({
|
|
273
|
-
id: colDiv.dataset.id ||
|
|
274
|
-
|
|
265
|
+
id: colDiv.dataset.id || `col_${this.columns.length}`,
|
|
266
|
+
// id: colDiv.dataset.id || null,
|
|
267
|
+
title: "" + colDiv.textContent,
|
|
268
|
+
// text: "" + colDiv.textContent,
|
|
269
|
+
width: "*", // TODO: read from header span
|
|
275
270
|
});
|
|
276
271
|
}
|
|
277
|
-
} else
|
|
272
|
+
} else {
|
|
278
273
|
// We need a row div, the rest will be computed from `this.columns`
|
|
279
274
|
const coldivs = "<span class='wb-col'></span>".repeat(
|
|
280
275
|
this.columns.length
|
|
@@ -285,9 +280,13 @@ export class Wunderbaum {
|
|
|
285
280
|
${coldivs}
|
|
286
281
|
</div>
|
|
287
282
|
</div>`;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
283
|
+
|
|
284
|
+
if (!wantHeader) {
|
|
285
|
+
const he = this.element.querySelector(
|
|
286
|
+
"div.wb-header"
|
|
287
|
+
) as HTMLDivElement;
|
|
288
|
+
he.style.display = "none";
|
|
289
|
+
}
|
|
291
290
|
}
|
|
292
291
|
|
|
293
292
|
//
|
|
@@ -295,22 +294,27 @@ export class Wunderbaum {
|
|
|
295
294
|
<div class="wb-scroll-container">
|
|
296
295
|
<div class="wb-node-list"></div>
|
|
297
296
|
</div>`;
|
|
298
|
-
this.
|
|
297
|
+
this.scrollContainerElement = this.element.querySelector(
|
|
299
298
|
"div.wb-scroll-container"
|
|
300
299
|
) as HTMLDivElement;
|
|
301
|
-
this.nodeListElement = this.
|
|
300
|
+
this.nodeListElement = this.scrollContainerElement.querySelector(
|
|
302
301
|
"div.wb-node-list"
|
|
303
302
|
) as HTMLDivElement;
|
|
304
303
|
this.headerElement = this.element.querySelector(
|
|
305
304
|
"div.wb-header"
|
|
306
305
|
) as HTMLDivElement;
|
|
307
306
|
|
|
308
|
-
|
|
309
|
-
this.element.classList.add("wb-grid");
|
|
310
|
-
}
|
|
307
|
+
this.element.classList.toggle("wb-grid", this.columns.length > 1);
|
|
311
308
|
|
|
312
309
|
this._initExtensions();
|
|
313
310
|
|
|
311
|
+
// --- apply initial options
|
|
312
|
+
["enabled", "fixedCol"].forEach((optName) => {
|
|
313
|
+
if (opts[optName] != null) {
|
|
314
|
+
this.setOption(optName, opts[optName]);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
314
318
|
// --- Load initial data
|
|
315
319
|
if (opts.source) {
|
|
316
320
|
if (opts.showSpinner) {
|
|
@@ -319,6 +323,16 @@ export class Wunderbaum {
|
|
|
319
323
|
}
|
|
320
324
|
this.load(opts.source)
|
|
321
325
|
.then(() => {
|
|
326
|
+
// The source may have defined columns, so we may adjust the nav mode
|
|
327
|
+
if (opts.navigationModeOption == null) {
|
|
328
|
+
if (this.isGrid()) {
|
|
329
|
+
this.setNavigationOption(NavigationOptions.cell);
|
|
330
|
+
} else {
|
|
331
|
+
this.setNavigationOption(NavigationOptions.row);
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
this.setNavigationOption(opts.navigationModeOption);
|
|
335
|
+
}
|
|
322
336
|
readyDeferred.resolve();
|
|
323
337
|
})
|
|
324
338
|
.catch((error) => {
|
|
@@ -332,27 +346,41 @@ export class Wunderbaum {
|
|
|
332
346
|
readyDeferred.resolve();
|
|
333
347
|
}
|
|
334
348
|
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
this.updateViewport();
|
|
339
|
-
}, 50);
|
|
349
|
+
// Async mode is sometimes required, because this.element.clientWidth
|
|
350
|
+
// has a wrong value at start???
|
|
351
|
+
this.setModified(ChangeType.any);
|
|
340
352
|
|
|
341
353
|
// --- Bind listeners
|
|
342
|
-
this.
|
|
354
|
+
this.element.addEventListener("scroll", (e: Event) => {
|
|
355
|
+
// this.log("scroll", e);
|
|
343
356
|
this.setModified(ChangeType.vscroll);
|
|
344
357
|
});
|
|
358
|
+
// this.scrollContainerElement.addEventListener("scroll", (e: Event) => {
|
|
359
|
+
// this.log("scroll", e)
|
|
360
|
+
// this.setModified(ChangeType.vscroll);
|
|
361
|
+
// });
|
|
345
362
|
|
|
346
363
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
347
364
|
this.setModified(ChangeType.vscroll);
|
|
348
|
-
|
|
365
|
+
// this.log("ResizeObserver: Size changed", entries);
|
|
349
366
|
});
|
|
350
367
|
this.resizeObserver.observe(this.element);
|
|
351
368
|
|
|
352
369
|
util.onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
|
|
353
370
|
const info = Wunderbaum.getEventInfo(e);
|
|
354
371
|
const node = info.node;
|
|
355
|
-
|
|
372
|
+
// this.log("click", info, e);
|
|
373
|
+
|
|
374
|
+
// if( (e.target as HTMLElement).matches("input[type=checkbox]")){
|
|
375
|
+
// // Click on an embedded checkbox triggers a change event.
|
|
376
|
+
// // We return here, before the `setActive()` performs a render
|
|
377
|
+
// this.log("click - cb", info, e);
|
|
378
|
+
// // e.preventDefault()
|
|
379
|
+
// setTimeout(()=>{
|
|
380
|
+
// // (e.target as HTMLElement).click()
|
|
381
|
+
// }, 50)
|
|
382
|
+
// // return
|
|
383
|
+
// }
|
|
356
384
|
if (
|
|
357
385
|
this._callEvent("click", { event: e, node: node, info: info }) === false
|
|
358
386
|
) {
|
|
@@ -384,17 +412,17 @@ export class Wunderbaum {
|
|
|
384
412
|
node.setSelected(!node.isSelected());
|
|
385
413
|
}
|
|
386
414
|
}
|
|
387
|
-
// if(e.target.classList.)
|
|
388
|
-
// this.log("click", info);
|
|
389
415
|
this.lastClickTime = Date.now();
|
|
390
416
|
});
|
|
391
417
|
|
|
392
418
|
util.onEvent(this.element, "keydown", (e) => {
|
|
393
419
|
const info = Wunderbaum.getEventInfo(e);
|
|
394
420
|
const eventName = util.eventToString(e);
|
|
421
|
+
const node = info.node || this.getFocusNode();
|
|
422
|
+
|
|
395
423
|
this._callHook("onKeyEvent", {
|
|
396
424
|
event: e,
|
|
397
|
-
node:
|
|
425
|
+
node: node,
|
|
398
426
|
info: info,
|
|
399
427
|
eventName: eventName,
|
|
400
428
|
});
|
|
@@ -582,17 +610,17 @@ export class Wunderbaum {
|
|
|
582
610
|
* tree._callEvent("edit.beforeEdit", {foo: 42})
|
|
583
611
|
* ```
|
|
584
612
|
*/
|
|
585
|
-
_callEvent(
|
|
586
|
-
const [p, n] =
|
|
613
|
+
_callEvent(type: string, extra?: any): any {
|
|
614
|
+
const [p, n] = type.split(".");
|
|
587
615
|
const opts = this.options as any;
|
|
588
616
|
const func = n ? opts[p][n] : opts[p];
|
|
589
617
|
if (func) {
|
|
590
618
|
return func.call(
|
|
591
619
|
this,
|
|
592
|
-
util.extend({
|
|
620
|
+
util.extend({ type: type, tree: this, util: this._util }, extra)
|
|
593
621
|
);
|
|
594
622
|
// } else {
|
|
595
|
-
// this.logError(`Triggering undefined event '${
|
|
623
|
+
// this.logError(`Triggering undefined event '${type}'.`)
|
|
596
624
|
}
|
|
597
625
|
}
|
|
598
626
|
|
|
@@ -610,35 +638,33 @@ export class Wunderbaum {
|
|
|
610
638
|
}
|
|
611
639
|
|
|
612
640
|
/** Return the topmost visible node in the viewport. */
|
|
613
|
-
|
|
641
|
+
getTopmostVpNode(complete = true) {
|
|
642
|
+
const gracePx = 1; // ignore subpixel scrolling
|
|
643
|
+
const scrollParent = this.element;
|
|
644
|
+
// const headerHeight = this.headerElement.clientHeight; // May be 0
|
|
645
|
+
const scrollTop = scrollParent.scrollTop; // + headerHeight;
|
|
614
646
|
let topIdx: number;
|
|
615
|
-
const gracePy = 1; // ignore subpixel scrolling
|
|
616
647
|
|
|
617
648
|
if (complete) {
|
|
618
|
-
topIdx = Math.ceil(
|
|
619
|
-
(this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT
|
|
620
|
-
);
|
|
649
|
+
topIdx = Math.ceil((scrollTop - gracePx) / ROW_HEIGHT);
|
|
621
650
|
} else {
|
|
622
|
-
topIdx = Math.floor(
|
|
651
|
+
topIdx = Math.floor(scrollTop / ROW_HEIGHT);
|
|
623
652
|
}
|
|
624
653
|
return this._getNodeByRowIdx(topIdx)!;
|
|
625
654
|
}
|
|
626
655
|
|
|
627
656
|
/** Return the lowest visible node in the viewport. */
|
|
628
|
-
|
|
657
|
+
getLowestVpNode(complete = true) {
|
|
658
|
+
const scrollParent = this.element;
|
|
659
|
+
const headerHeight = this.headerElement.clientHeight; // May be 0
|
|
660
|
+
const scrollTop = scrollParent.scrollTop;
|
|
661
|
+
const clientHeight = scrollParent.clientHeight - headerHeight;
|
|
629
662
|
let bottomIdx: number;
|
|
663
|
+
|
|
630
664
|
if (complete) {
|
|
631
|
-
bottomIdx =
|
|
632
|
-
Math.floor(
|
|
633
|
-
(this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
|
|
634
|
-
ROW_HEIGHT
|
|
635
|
-
) - 1;
|
|
665
|
+
bottomIdx = Math.floor((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
|
|
636
666
|
} else {
|
|
637
|
-
bottomIdx =
|
|
638
|
-
Math.ceil(
|
|
639
|
-
(this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
|
|
640
|
-
ROW_HEIGHT
|
|
641
|
-
) - 1;
|
|
667
|
+
bottomIdx = Math.ceil((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
|
|
642
668
|
}
|
|
643
669
|
bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
|
|
644
670
|
return this._getNodeByRowIdx(bottomIdx)!;
|
|
@@ -868,7 +894,7 @@ export class Wunderbaum {
|
|
|
868
894
|
* Return `tree.option.NAME` (also resolving if this is a callback).
|
|
869
895
|
*
|
|
870
896
|
* See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
|
|
871
|
-
* to
|
|
897
|
+
* to evaluate `node.NAME` setting and `tree.types[node.type].NAME`.
|
|
872
898
|
*
|
|
873
899
|
* @param name option name (use dot notation to access extension option, e.g.
|
|
874
900
|
* `filter.mode`)
|
|
@@ -889,36 +915,52 @@ export class Wunderbaum {
|
|
|
889
915
|
value = value({ type: "resolve", tree: this });
|
|
890
916
|
}
|
|
891
917
|
// Use value from value options dict, fallback do default
|
|
918
|
+
// console.info(name, value, opts)
|
|
892
919
|
return value ?? defaultValue;
|
|
893
920
|
}
|
|
894
921
|
|
|
895
922
|
/**
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
* @param value
|
|
923
|
+
* Set tree option.
|
|
924
|
+
* Use dot notation to set plugin option, e.g. "filter.mode".
|
|
899
925
|
*/
|
|
900
926
|
setOption(name: string, value: any): void {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// default:
|
|
907
|
-
// break;
|
|
908
|
-
// }
|
|
927
|
+
// this.log(`setOption(${name}, ${value})`);
|
|
928
|
+
if (name.indexOf(".") >= 0) {
|
|
929
|
+
const parts = name.split(".");
|
|
930
|
+
const ext = this.extensions[parts[0]];
|
|
931
|
+
ext!.setPluginOption(parts[1], value);
|
|
909
932
|
return;
|
|
910
933
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
934
|
+
(this.options as any)[name] = value;
|
|
935
|
+
switch (name) {
|
|
936
|
+
case "checkbox":
|
|
937
|
+
this.setModified(ChangeType.any, { removeMarkup: true });
|
|
938
|
+
break;
|
|
939
|
+
case "enabled":
|
|
940
|
+
this.setEnabled(!!value);
|
|
941
|
+
break;
|
|
942
|
+
case "fixedCol":
|
|
943
|
+
this.element.classList.toggle("wb-fixed-col", !!value);
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
914
946
|
}
|
|
915
947
|
|
|
916
|
-
/**Return true if the tree (or one of its nodes) has the input focus. */
|
|
948
|
+
/** Return true if the tree (or one of its nodes) has the input focus. */
|
|
917
949
|
hasFocus() {
|
|
918
950
|
return this.element.contains(document.activeElement);
|
|
919
951
|
}
|
|
920
952
|
|
|
921
|
-
/**
|
|
953
|
+
/**
|
|
954
|
+
* Return true if the tree displays a header. Grids have a header unless the
|
|
955
|
+
* `header` option is set to `false`. Plain trees have a header if the `header`
|
|
956
|
+
* option is a string or `true`.
|
|
957
|
+
*/
|
|
958
|
+
hasHeader() {
|
|
959
|
+
const header = this.options.header;
|
|
960
|
+
return this.isGrid() ? header !== false : !!header;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Run code, but defer rendering of viewport until done. */
|
|
922
964
|
runWithoutUpdate(func: () => any, hint = null): void {
|
|
923
965
|
try {
|
|
924
966
|
this.enableUpdate(false);
|
|
@@ -942,6 +984,18 @@ export class Wunderbaum {
|
|
|
942
984
|
}
|
|
943
985
|
}
|
|
944
986
|
|
|
987
|
+
/** Recursively select all nodes. */
|
|
988
|
+
selectAll(flag: boolean = true) {
|
|
989
|
+
try {
|
|
990
|
+
this.enableUpdate(false);
|
|
991
|
+
this.visit((node) => {
|
|
992
|
+
node.setSelected(flag);
|
|
993
|
+
});
|
|
994
|
+
} finally {
|
|
995
|
+
this.enableUpdate(true);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
945
999
|
/** Return the number of nodes in the data model.*/
|
|
946
1000
|
count(visible = false): number {
|
|
947
1001
|
if (visible) {
|
|
@@ -987,6 +1041,18 @@ export class Wunderbaum {
|
|
|
987
1041
|
return this.root.findFirst(match);
|
|
988
1042
|
}
|
|
989
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* Find first node that matches condition.
|
|
1046
|
+
*
|
|
1047
|
+
* @param match title string to search for, or a
|
|
1048
|
+
* callback function that returns `true` if a node is matched.
|
|
1049
|
+
* @see {@link WunderbaumNode.findFirst}
|
|
1050
|
+
*
|
|
1051
|
+
*/
|
|
1052
|
+
findKey(key: string): WunderbaumNode | undefined {
|
|
1053
|
+
return this.keyMap.get(key);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
990
1056
|
/**
|
|
991
1057
|
* Find the next visible node that starts with `match`, starting at `startNode`
|
|
992
1058
|
* and wrap-around at the end.
|
|
@@ -1037,7 +1103,9 @@ export class Wunderbaum {
|
|
|
1037
1103
|
*/
|
|
1038
1104
|
findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) {
|
|
1039
1105
|
let res = null;
|
|
1040
|
-
const pageSize = Math.floor(
|
|
1106
|
+
const pageSize = Math.floor(
|
|
1107
|
+
this.scrollContainerElement.clientHeight / ROW_HEIGHT
|
|
1108
|
+
);
|
|
1041
1109
|
|
|
1042
1110
|
switch (where) {
|
|
1043
1111
|
case "parent":
|
|
@@ -1094,7 +1162,7 @@ export class Wunderbaum {
|
|
|
1094
1162
|
res = this._getNextNodeInView(node);
|
|
1095
1163
|
break;
|
|
1096
1164
|
case "pageDown":
|
|
1097
|
-
const bottomNode = this.
|
|
1165
|
+
const bottomNode = this.getLowestVpNode();
|
|
1098
1166
|
// this.logDebug(`${where}(${node}) -> ${bottomNode}`);
|
|
1099
1167
|
|
|
1100
1168
|
if (node._rowIdx! < bottomNode._rowIdx!) {
|
|
@@ -1107,7 +1175,7 @@ export class Wunderbaum {
|
|
|
1107
1175
|
if (node._rowIdx === 0) {
|
|
1108
1176
|
res = node;
|
|
1109
1177
|
} else {
|
|
1110
|
-
const topNode = this.
|
|
1178
|
+
const topNode = this.getTopmostVpNode();
|
|
1111
1179
|
// this.logDebug(`${where}(${node}) -> ${topNode}`);
|
|
1112
1180
|
|
|
1113
1181
|
if (node._rowIdx! > topNode._rowIdx!) {
|
|
@@ -1195,6 +1263,9 @@ export class Wunderbaum {
|
|
|
1195
1263
|
parentCol
|
|
1196
1264
|
);
|
|
1197
1265
|
res.colIdx = idx;
|
|
1266
|
+
} else if (cl.contains("wb-row")) {
|
|
1267
|
+
// Plain tree
|
|
1268
|
+
res.region = NodeRegion.title;
|
|
1198
1269
|
} else {
|
|
1199
1270
|
// Somewhere near the title
|
|
1200
1271
|
if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
|
|
@@ -1205,7 +1276,7 @@ export class Wunderbaum {
|
|
|
1205
1276
|
if (res.colIdx === -1) {
|
|
1206
1277
|
res.colIdx = 0;
|
|
1207
1278
|
}
|
|
1208
|
-
res.colDef = tree?.columns[res.colIdx];
|
|
1279
|
+
res.colDef = <any>tree?.columns[res.colIdx];
|
|
1209
1280
|
res.colDef != null ? (res.colId = (<any>res.colDef).id) : 0;
|
|
1210
1281
|
// this.log("Event", event, res);
|
|
1211
1282
|
return res;
|
|
@@ -1225,7 +1296,7 @@ export class Wunderbaum {
|
|
|
1225
1296
|
* @internal
|
|
1226
1297
|
*/
|
|
1227
1298
|
toString() {
|
|
1228
|
-
return
|
|
1299
|
+
return `Wunderbaum<'${this.id}'>`;
|
|
1229
1300
|
}
|
|
1230
1301
|
|
|
1231
1302
|
/** Return true if any node is currently in edit-title mode. */
|
|
@@ -1302,72 +1373,130 @@ export class Wunderbaum {
|
|
|
1302
1373
|
}
|
|
1303
1374
|
|
|
1304
1375
|
/**
|
|
1305
|
-
* Make sure that this node is scrolled into the viewport.
|
|
1306
|
-
*
|
|
1307
|
-
* @param {boolean | PlainObject} [effects=false] animation options.
|
|
1308
|
-
* @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
|
|
1309
|
-
* this node will remain visible in
|
|
1310
|
-
* any case, even if `this` is outside the scroll pane.
|
|
1376
|
+
* Make sure that this node is vertically scrolled into the viewport.
|
|
1311
1377
|
*/
|
|
1312
|
-
scrollTo(
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1378
|
+
scrollTo(nodeOrOpts: ScrollToOptions | WunderbaumNode) {
|
|
1379
|
+
const PADDING = 2; // leave some pixels between viewport bounds
|
|
1380
|
+
|
|
1381
|
+
let node;
|
|
1382
|
+
WunderbaumNode;
|
|
1383
|
+
let opts: ScrollToOptions | undefined;
|
|
1384
|
+
|
|
1385
|
+
if (nodeOrOpts instanceof WunderbaumNode) {
|
|
1386
|
+
node = nodeOrOpts;
|
|
1387
|
+
} else {
|
|
1388
|
+
opts = nodeOrOpts;
|
|
1389
|
+
node = opts.node;
|
|
1390
|
+
}
|
|
1391
|
+
util.assert(node && node._rowIdx != null);
|
|
1392
|
+
|
|
1393
|
+
const scrollParent = this.element;
|
|
1394
|
+
const headerHeight = this.headerElement.clientHeight; // May be 0
|
|
1395
|
+
const scrollTop = scrollParent.scrollTop;
|
|
1396
|
+
const vpHeight = scrollParent.clientHeight;
|
|
1397
|
+
const rowTop = node._rowIdx! * ROW_HEIGHT + headerHeight;
|
|
1398
|
+
const vpTop = headerHeight;
|
|
1399
|
+
const vpRowTop = rowTop - scrollTop;
|
|
1400
|
+
const vpRowBottom = vpRowTop + ROW_HEIGHT;
|
|
1401
|
+
|
|
1402
|
+
// this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts );
|
|
1403
|
+
let newScrollTop: number | null = null;
|
|
1404
|
+
if (vpRowTop >= vpTop) {
|
|
1405
|
+
if (vpRowBottom <= vpHeight) {
|
|
1323
1406
|
// Already in view
|
|
1407
|
+
// this.log("Already in view");
|
|
1324
1408
|
} else {
|
|
1325
1409
|
// Node is below viewport
|
|
1326
|
-
|
|
1410
|
+
// this.log("Below viewport");
|
|
1411
|
+
newScrollTop = rowTop + ROW_HEIGHT - vpHeight + PADDING; // leave some pixels between vieeport bounds
|
|
1327
1412
|
}
|
|
1328
|
-
} else
|
|
1413
|
+
} else {
|
|
1329
1414
|
// Node is above viewport
|
|
1330
|
-
|
|
1415
|
+
// this.log("Above viewport");
|
|
1416
|
+
newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between vieeport bounds
|
|
1331
1417
|
}
|
|
1332
|
-
if (
|
|
1333
|
-
this.log(
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
);
|
|
1337
|
-
this.scrollContainer.scrollTop = newTop;
|
|
1338
|
-
this.setModified(ChangeType.vscroll);
|
|
1418
|
+
if (newScrollTop != null) {
|
|
1419
|
+
this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`);
|
|
1420
|
+
scrollParent.scrollTop = newScrollTop;
|
|
1421
|
+
// this.setModified(ChangeType.vscroll);
|
|
1339
1422
|
}
|
|
1340
1423
|
}
|
|
1341
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* Make sure that this node is horizontally scrolled into the viewport.
|
|
1427
|
+
* Called by {@link setColumn}.
|
|
1428
|
+
*/
|
|
1429
|
+
protected scrollToHorz() {
|
|
1430
|
+
// const PADDING = 1;
|
|
1431
|
+
const fixedWidth = this.columns[0]._widthPx!;
|
|
1432
|
+
const vpWidth = this.element.clientWidth;
|
|
1433
|
+
const scrollLeft = this.element.scrollLeft;
|
|
1434
|
+
// if (scrollLeft <= 0) {
|
|
1435
|
+
// return; // Not scrolled horizontally: Nothing to do
|
|
1436
|
+
// }
|
|
1437
|
+
const colElem = this.getActiveColElem()!;
|
|
1438
|
+
const colLeft = Number.parseInt(colElem?.style.left, 10);
|
|
1439
|
+
const colRight = colLeft + Number.parseInt(colElem?.style.width, 10);
|
|
1440
|
+
let newLeft = scrollLeft;
|
|
1441
|
+
|
|
1442
|
+
if (colLeft - scrollLeft < fixedWidth) {
|
|
1443
|
+
// The current column is scrolled behind the left fixed column
|
|
1444
|
+
newLeft = colLeft - fixedWidth;
|
|
1445
|
+
} else if (colRight - scrollLeft > vpWidth) {
|
|
1446
|
+
// The current column is scrolled outside the right side
|
|
1447
|
+
newLeft = colRight - vpWidth;
|
|
1448
|
+
}
|
|
1449
|
+
// util.assert(node._rowIdx != null);
|
|
1450
|
+
// const curLeft = this.scrollContainer.scrollLeft;
|
|
1451
|
+
this.log(
|
|
1452
|
+
`scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`
|
|
1453
|
+
);
|
|
1454
|
+
// const nodeOfs = node._rowIdx * ROW_HEIGHT;
|
|
1455
|
+
// let newLeft;
|
|
1456
|
+
|
|
1457
|
+
this.element.scrollLeft = newLeft;
|
|
1458
|
+
// this.setModified(ChangeType.vscroll);
|
|
1459
|
+
// }
|
|
1460
|
+
}
|
|
1342
1461
|
/**
|
|
1343
1462
|
* Set column #colIdx to 'active'.
|
|
1344
1463
|
*
|
|
1345
1464
|
* This higlights the column header and -cells by adding the `wb-active` class.
|
|
1346
|
-
* Available in cell-nav
|
|
1465
|
+
* Available in cell-nav mode only.
|
|
1347
1466
|
*/
|
|
1348
1467
|
setColumn(colIdx: number) {
|
|
1349
|
-
util.assert(this.
|
|
1468
|
+
util.assert(this.isCellNav());
|
|
1350
1469
|
util.assert(0 <= colIdx && colIdx < this.columns.length);
|
|
1351
1470
|
this.activeColIdx = colIdx;
|
|
1352
|
-
|
|
1353
|
-
this.setModified(ChangeType.row, this.activeNode);
|
|
1471
|
+
|
|
1354
1472
|
// Update `wb-active` class for all headers
|
|
1355
|
-
if (this.
|
|
1473
|
+
if (this.hasHeader()) {
|
|
1356
1474
|
for (let rowDiv of this.headerElement.children) {
|
|
1357
|
-
// for (let rowDiv of document.querySelector("div.wb-header").children) {
|
|
1358
1475
|
let i = 0;
|
|
1359
1476
|
for (let colDiv of rowDiv.children) {
|
|
1360
1477
|
(colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
|
|
1361
1478
|
}
|
|
1362
1479
|
}
|
|
1363
1480
|
}
|
|
1364
|
-
|
|
1481
|
+
|
|
1482
|
+
this.activeNode?.setModified(ChangeType.status);
|
|
1483
|
+
|
|
1484
|
+
// Update `wb-active` class for all cell spans
|
|
1365
1485
|
for (let rowDiv of this.nodeListElement.children) {
|
|
1366
1486
|
let i = 0;
|
|
1367
1487
|
for (let colDiv of rowDiv.children) {
|
|
1368
1488
|
(colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
|
|
1369
1489
|
}
|
|
1370
1490
|
}
|
|
1491
|
+
// Vertical scroll into view
|
|
1492
|
+
// if (this.options.fixedCol) {
|
|
1493
|
+
this.scrollToHorz();
|
|
1494
|
+
// }
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** Set or remove keybaord focus to the tree container. */
|
|
1498
|
+
setActiveNode(key: string, flag: boolean = true, options?: SetActiveOptions) {
|
|
1499
|
+
this.findKey(key)?.setActive(flag, options);
|
|
1371
1500
|
}
|
|
1372
1501
|
|
|
1373
1502
|
/** Set or remove keybaord focus to the tree container. */
|
|
@@ -1388,7 +1517,7 @@ export class Wunderbaum {
|
|
|
1388
1517
|
setModified(
|
|
1389
1518
|
change: ChangeType,
|
|
1390
1519
|
node?: WunderbaumNode | any,
|
|
1391
|
-
options?:
|
|
1520
|
+
options?: SetModifiedOptions
|
|
1392
1521
|
): void {
|
|
1393
1522
|
if (this._disableUpdateCount) {
|
|
1394
1523
|
// Assuming that we redraw all when enableUpdate() is re-enabled.
|
|
@@ -1402,73 +1531,159 @@ export class Wunderbaum {
|
|
|
1402
1531
|
options = node;
|
|
1403
1532
|
}
|
|
1404
1533
|
const immediate = !!util.getOption(options, "immediate");
|
|
1534
|
+
const removeMarkup = !!util.getOption(options, "removeMarkup");
|
|
1535
|
+
if (removeMarkup) {
|
|
1536
|
+
this.visit((n) => {
|
|
1537
|
+
n.removeMarkup();
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1405
1540
|
|
|
1541
|
+
let callUpdate = false;
|
|
1406
1542
|
switch (change) {
|
|
1407
1543
|
case ChangeType.any:
|
|
1408
1544
|
case ChangeType.structure:
|
|
1409
1545
|
case ChangeType.header:
|
|
1410
1546
|
this.changeRedrawRequestPending = true;
|
|
1411
|
-
|
|
1547
|
+
callUpdate = true;
|
|
1412
1548
|
break;
|
|
1413
1549
|
case ChangeType.vscroll:
|
|
1414
|
-
this.
|
|
1550
|
+
this.changeScrollRequestPending = true;
|
|
1551
|
+
callUpdate = true;
|
|
1415
1552
|
break;
|
|
1416
1553
|
case ChangeType.row:
|
|
1554
|
+
case ChangeType.data:
|
|
1417
1555
|
case ChangeType.status:
|
|
1418
|
-
// Single nodes are
|
|
1556
|
+
// Single nodes are immediately updated if already inside the viewport
|
|
1419
1557
|
// (otherwise we can ignore)
|
|
1420
1558
|
if (node._rowElem) {
|
|
1421
|
-
node.render();
|
|
1559
|
+
node.render({ change: change });
|
|
1422
1560
|
}
|
|
1423
1561
|
break;
|
|
1424
1562
|
default:
|
|
1425
1563
|
util.error(`Invalid change type ${change}`);
|
|
1426
1564
|
}
|
|
1565
|
+
if (callUpdate) {
|
|
1566
|
+
if (immediate) {
|
|
1567
|
+
this._updateViewportImmediately();
|
|
1568
|
+
} else {
|
|
1569
|
+
this._updateViewportThrottled();
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Disable mouse and keyboard interaction (return prev. state). */
|
|
1575
|
+
setEnabled(flag: boolean = true): boolean {
|
|
1576
|
+
const prev = this.enabled;
|
|
1577
|
+
this.enabled = !!flag;
|
|
1578
|
+
this.element.classList.toggle("wb-disabled", !flag);
|
|
1579
|
+
return prev;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/** Return false if tree is disabled. */
|
|
1583
|
+
isEnabled(): boolean {
|
|
1584
|
+
return this.enabled;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/** Return true if tree has more than one column, i.e. has additional data columns. */
|
|
1588
|
+
isGrid(): boolean {
|
|
1589
|
+
return this.columns && this.columns.length > 1;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** Return true if cell-navigation mode is acive. */
|
|
1593
|
+
isCellNav(): boolean {
|
|
1594
|
+
return !!this._cellNavMode;
|
|
1595
|
+
}
|
|
1596
|
+
/** Return true if row-navigation mode is acive. */
|
|
1597
|
+
isRowNav(): boolean {
|
|
1598
|
+
return !this._cellNavMode;
|
|
1427
1599
|
}
|
|
1428
1600
|
|
|
1429
1601
|
/** Set the tree's navigation mode. */
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
//
|
|
1433
|
-
|
|
1602
|
+
setCellNav(flag: boolean = true) {
|
|
1603
|
+
const prev = this._cellNavMode;
|
|
1604
|
+
// if (flag === prev) {
|
|
1605
|
+
// return;
|
|
1606
|
+
// }
|
|
1607
|
+
this._cellNavMode = !!flag;
|
|
1608
|
+
if (flag && !prev) {
|
|
1609
|
+
// switch from row to cell mode
|
|
1610
|
+
this.setColumn(0);
|
|
1611
|
+
}
|
|
1612
|
+
this.element.classList.toggle("wb-cell-mode", flag);
|
|
1613
|
+
this.activeNode?.setModified(ChangeType.status);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/** Set the tree's navigation mode option. */
|
|
1617
|
+
setNavigationOption(mode: NavigationOptions, reset = false) {
|
|
1618
|
+
if (!this.isGrid() && mode !== NavigationOptions.row) {
|
|
1619
|
+
this.logWarn("Plain trees only support row navigation mode.");
|
|
1434
1620
|
return;
|
|
1435
1621
|
}
|
|
1436
|
-
|
|
1437
|
-
const cellMode = mode !== NavigationMode.row;
|
|
1622
|
+
this.options.navigationModeOption = mode;
|
|
1438
1623
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1624
|
+
switch (mode) {
|
|
1625
|
+
case NavigationOptions.cell:
|
|
1626
|
+
this.setCellNav(true);
|
|
1627
|
+
break;
|
|
1628
|
+
case NavigationOptions.row:
|
|
1629
|
+
this.setCellNav(false);
|
|
1630
|
+
break;
|
|
1631
|
+
case NavigationOptions.startCell:
|
|
1632
|
+
if (reset) {
|
|
1633
|
+
this.setCellNav(true);
|
|
1634
|
+
}
|
|
1635
|
+
break;
|
|
1636
|
+
case NavigationOptions.startRow:
|
|
1637
|
+
if (reset) {
|
|
1638
|
+
this.setCellNav(false);
|
|
1639
|
+
}
|
|
1640
|
+
break;
|
|
1641
|
+
default:
|
|
1642
|
+
util.error(`Invalid mode '${mode}'`);
|
|
1442
1643
|
}
|
|
1443
|
-
this.element.classList.toggle("wb-cell-mode", cellMode);
|
|
1444
|
-
this.element.classList.toggle(
|
|
1445
|
-
"wb-cell-edit-mode",
|
|
1446
|
-
mode === NavigationMode.cellEdit
|
|
1447
|
-
);
|
|
1448
|
-
this.setModified(ChangeType.row, this.activeNode);
|
|
1449
1644
|
}
|
|
1450
1645
|
|
|
1451
1646
|
/** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
|
|
1452
1647
|
setStatus(
|
|
1453
1648
|
status: NodeStatusType,
|
|
1454
|
-
|
|
1455
|
-
details?: string
|
|
1649
|
+
options?: SetStatusOptions
|
|
1456
1650
|
): WunderbaumNode | null {
|
|
1457
|
-
return this.root.setStatus(status,
|
|
1651
|
+
return this.root.setStatus(status, options);
|
|
1652
|
+
}
|
|
1653
|
+
/** Add or redefine node type definitions. */
|
|
1654
|
+
setTypes(types: any, replace = true) {
|
|
1655
|
+
util.assert(util.isPlainObject(types));
|
|
1656
|
+
if (replace) {
|
|
1657
|
+
this.types = types;
|
|
1658
|
+
} else {
|
|
1659
|
+
util.extend(this.types, types);
|
|
1660
|
+
}
|
|
1661
|
+
// Convert `TYPE.classes` to a Set
|
|
1662
|
+
for (let t of Object.values(this.types) as any) {
|
|
1663
|
+
if (t.classes) {
|
|
1664
|
+
t.classes = util.toSet(t.classes);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1458
1667
|
}
|
|
1459
|
-
|
|
1460
1668
|
/** Update column headers and width. */
|
|
1461
1669
|
updateColumns(opts?: any) {
|
|
1462
1670
|
opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
|
|
1463
|
-
const
|
|
1671
|
+
const defaultMinWidth = 4;
|
|
1464
1672
|
const vpWidth = this.element.clientWidth;
|
|
1673
|
+
const isGrid = this.isGrid();
|
|
1674
|
+
|
|
1675
|
+
let totalWidth = 0;
|
|
1465
1676
|
let totalWeight = 0;
|
|
1466
1677
|
let fixedWidth = 0;
|
|
1467
|
-
|
|
1468
1678
|
let modified = false;
|
|
1469
1679
|
|
|
1680
|
+
this.element.classList.toggle("wb-grid", isGrid);
|
|
1681
|
+
if (!isGrid && this.isCellNav()) {
|
|
1682
|
+
this.setCellNav(false);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1470
1685
|
if (opts.calculateCols) {
|
|
1471
|
-
// Gather width
|
|
1686
|
+
// Gather width definitions
|
|
1472
1687
|
this._columnsById = {};
|
|
1473
1688
|
for (let col of this.columns) {
|
|
1474
1689
|
this._columnsById[<string>col.id] = col;
|
|
@@ -1489,7 +1704,7 @@ export class Wunderbaum {
|
|
|
1489
1704
|
}
|
|
1490
1705
|
fixedWidth += px;
|
|
1491
1706
|
} else {
|
|
1492
|
-
util.error(
|
|
1707
|
+
util.error(`Invalid column width: ${cw}`);
|
|
1493
1708
|
}
|
|
1494
1709
|
}
|
|
1495
1710
|
// Share remaining space between non-fixed columns
|
|
@@ -1497,7 +1712,17 @@ export class Wunderbaum {
|
|
|
1497
1712
|
let ofsPx = 0;
|
|
1498
1713
|
|
|
1499
1714
|
for (let col of this.columns) {
|
|
1715
|
+
let minWidth: number;
|
|
1716
|
+
|
|
1500
1717
|
if (col._weight) {
|
|
1718
|
+
const cmw = col.minWidth;
|
|
1719
|
+
if (typeof cmw === "number") {
|
|
1720
|
+
minWidth = cmw;
|
|
1721
|
+
} else if (typeof cmw === "string" && cmw.endsWith("px")) {
|
|
1722
|
+
minWidth = parseFloat(cmw.slice(0, -2));
|
|
1723
|
+
} else {
|
|
1724
|
+
minWidth = defaultMinWidth;
|
|
1725
|
+
}
|
|
1501
1726
|
const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
|
|
1502
1727
|
if (col._widthPx != px) {
|
|
1503
1728
|
modified = true;
|
|
@@ -1505,9 +1730,17 @@ export class Wunderbaum {
|
|
|
1505
1730
|
}
|
|
1506
1731
|
}
|
|
1507
1732
|
col._ofsPx = ofsPx;
|
|
1508
|
-
ofsPx += col._widthPx
|
|
1733
|
+
ofsPx += col._widthPx!;
|
|
1509
1734
|
}
|
|
1735
|
+
totalWidth = ofsPx;
|
|
1510
1736
|
}
|
|
1737
|
+
// if (this.options.fixedCol) {
|
|
1738
|
+
// 'position: fixed' requires that the content has the correct size
|
|
1739
|
+
const tw = `${totalWidth}px`;
|
|
1740
|
+
this.headerElement.style.width = tw;
|
|
1741
|
+
this.scrollContainerElement!.style.width = tw;
|
|
1742
|
+
// }
|
|
1743
|
+
|
|
1511
1744
|
// Every column has now a calculated `_ofsPx` and `_widthPx`
|
|
1512
1745
|
// this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
|
|
1513
1746
|
// console.trace();
|
|
@@ -1524,108 +1757,138 @@ export class Wunderbaum {
|
|
|
1524
1757
|
* @internal
|
|
1525
1758
|
*/
|
|
1526
1759
|
protected _renderHeaderMarkup() {
|
|
1527
|
-
|
|
1760
|
+
util.assert(this.headerElement);
|
|
1761
|
+
const wantHeader = this.hasHeader();
|
|
1762
|
+
util.setElemDisplay(this.headerElement, wantHeader);
|
|
1763
|
+
if (!wantHeader) {
|
|
1528
1764
|
return;
|
|
1529
1765
|
}
|
|
1766
|
+
const colCount = this.columns.length;
|
|
1530
1767
|
const headerRow = this.headerElement.querySelector(".wb-row")!;
|
|
1531
1768
|
util.assert(headerRow);
|
|
1532
|
-
headerRow.innerHTML = "<span class='wb-col'></span>".repeat(
|
|
1533
|
-
this.columns.length
|
|
1534
|
-
);
|
|
1769
|
+
headerRow.innerHTML = "<span class='wb-col'></span>".repeat(colCount);
|
|
1535
1770
|
|
|
1536
|
-
for (let i = 0; i <
|
|
1771
|
+
for (let i = 0; i < colCount; i++) {
|
|
1537
1772
|
const col = this.columns[i];
|
|
1538
1773
|
const colElem = <HTMLElement>headerRow.children[i];
|
|
1539
1774
|
|
|
1540
1775
|
colElem.style.left = col._ofsPx + "px";
|
|
1541
1776
|
colElem.style.width = col._widthPx + "px";
|
|
1542
|
-
|
|
1777
|
+
|
|
1543
1778
|
const title = util.escapeHtml(col.title || col.id);
|
|
1544
|
-
|
|
1545
|
-
|
|
1779
|
+
let tooltip = "";
|
|
1780
|
+
if (col.tooltip) {
|
|
1781
|
+
tooltip = util.escapeTooltip(col.tooltip);
|
|
1782
|
+
tooltip = ` title="${tooltip}"`;
|
|
1783
|
+
}
|
|
1784
|
+
let resizer = "";
|
|
1785
|
+
if (i < colCount - 1) {
|
|
1786
|
+
resizer = '<span class="wb-col-resizer"></span>';
|
|
1787
|
+
}
|
|
1788
|
+
colElem.innerHTML = `<span class="wb-col-title"${tooltip}>${title}</span>${resizer}`;
|
|
1546
1789
|
}
|
|
1547
1790
|
}
|
|
1548
1791
|
|
|
1549
|
-
/**
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1792
|
+
/**
|
|
1793
|
+
* Render pending changes that were scheduled using {@link WunderbaumNode.setModified} if any.
|
|
1794
|
+
*
|
|
1795
|
+
* This is hardly ever neccessary, since we normally either
|
|
1796
|
+
* - call `setModified(ChangeType.TYPE)` (async, throttled), or
|
|
1797
|
+
* - call `setModified(ChangeType.TYPE, {immediate: true})` (synchronous)
|
|
1798
|
+
*
|
|
1799
|
+
* `updatePendingModifications()` will only force immediate execution of
|
|
1800
|
+
* pending async changes if any.
|
|
1801
|
+
*/
|
|
1802
|
+
updatePendingModifications() {
|
|
1803
|
+
if (this.changeRedrawRequestPending || this.changeScrollRequestPending) {
|
|
1804
|
+
this._updateViewportImmediately();
|
|
1556
1805
|
}
|
|
1557
1806
|
}
|
|
1558
1807
|
|
|
1559
1808
|
/**
|
|
1560
1809
|
* This is the actual update method, which is wrapped inside a throttle method.
|
|
1561
|
-
* This protected method should not be called directly but via
|
|
1562
|
-
* `tree.updateViewport()` or `tree.setModified()`.
|
|
1563
1810
|
* It calls `updateColumns()` and `_updateRows()`.
|
|
1811
|
+
*
|
|
1812
|
+
* This protected method should not be called directly but via
|
|
1813
|
+
* {@link WunderbaumNode.setModified}`, {@link Wunderbaum.setModified},
|
|
1814
|
+
* or {@link Wunderbaum.updatePendingModifications}.
|
|
1564
1815
|
* @internal
|
|
1565
1816
|
*/
|
|
1566
|
-
protected
|
|
1817
|
+
protected _updateViewportImmediately() {
|
|
1567
1818
|
if (this._disableUpdateCount) {
|
|
1568
1819
|
this.log(
|
|
1569
|
-
`IGNORED
|
|
1820
|
+
`IGNORED _updateViewportImmediately() disable level: ${this._disableUpdateCount}`
|
|
1570
1821
|
);
|
|
1571
1822
|
return;
|
|
1572
1823
|
}
|
|
1573
1824
|
const newNodesOnly = !this.changeRedrawRequestPending;
|
|
1574
1825
|
this.changeRedrawRequestPending = false;
|
|
1826
|
+
this.changeScrollRequestPending = false;
|
|
1575
1827
|
|
|
1576
|
-
let height = this.
|
|
1828
|
+
let height = this.scrollContainerElement.clientHeight;
|
|
1577
1829
|
// We cannot get the height for absolute positioned parent, so look at first col
|
|
1578
1830
|
// let headerHeight = this.headerElement.clientHeight
|
|
1579
1831
|
// let headerHeight = this.headerElement.children[0].children[0].clientHeight;
|
|
1580
|
-
const headerHeight = this.options.headerHeightPx;
|
|
1832
|
+
// const headerHeight = this.options.headerHeightPx;
|
|
1833
|
+
const headerHeight = this.headerElement.clientHeight; // May be 0
|
|
1581
1834
|
const wantHeight = this.element.clientHeight - headerHeight;
|
|
1582
1835
|
|
|
1583
1836
|
if (Math.abs(height - wantHeight) > 1.0) {
|
|
1584
1837
|
// this.log("resize", height, wantHeight);
|
|
1585
|
-
this.
|
|
1838
|
+
this.scrollContainerElement.style.height = wantHeight + "px";
|
|
1586
1839
|
height = wantHeight;
|
|
1587
1840
|
}
|
|
1841
|
+
// console.profile(`_updateViewportImmediately()`)
|
|
1588
1842
|
|
|
1589
1843
|
this.updateColumns({ updateRows: false });
|
|
1590
1844
|
|
|
1591
1845
|
this._updateRows({ newNodesOnly: newNodesOnly });
|
|
1592
1846
|
|
|
1847
|
+
// console.profileEnd(`_updateViewportImmediately()`)
|
|
1848
|
+
|
|
1849
|
+
if (this.options.connectTopBreadcrumb) {
|
|
1850
|
+
let path = this.getTopmostVpNode(true)?.getPath(false, "title", " > ");
|
|
1851
|
+
path = path ? path + " >" : "";
|
|
1852
|
+
this.options.connectTopBreadcrumb.textContent = path;
|
|
1853
|
+
}
|
|
1593
1854
|
this._callEvent("update");
|
|
1594
1855
|
}
|
|
1595
1856
|
|
|
1596
|
-
/**
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
protected _validateRows(): boolean {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
}
|
|
1857
|
+
// /**
|
|
1858
|
+
// * Assert that TR order matches the natural node order
|
|
1859
|
+
// * @internal
|
|
1860
|
+
// */
|
|
1861
|
+
// protected _validateRows(): boolean {
|
|
1862
|
+
// let trs = this.nodeListElement.childNodes;
|
|
1863
|
+
// let i = 0;
|
|
1864
|
+
// let prev = -1;
|
|
1865
|
+
// let ok = true;
|
|
1866
|
+
// trs.forEach((element) => {
|
|
1867
|
+
// const tr = element as HTMLTableRowElement;
|
|
1868
|
+
// const top = Number.parseInt(tr.style.top);
|
|
1869
|
+
// const n = (<any>tr)._wb_node;
|
|
1870
|
+
// // if (i < 4) {
|
|
1871
|
+
// // console.info(
|
|
1872
|
+
// // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
|
|
1873
|
+
// // );
|
|
1874
|
+
// // }
|
|
1875
|
+
// if (prev >= 0 && top !== prev + ROW_HEIGHT) {
|
|
1876
|
+
// n.logWarn(
|
|
1877
|
+
// `TR order mismatch at index ${i}: top=${top}px != ${
|
|
1878
|
+
// prev + ROW_HEIGHT
|
|
1879
|
+
// }`
|
|
1880
|
+
// );
|
|
1881
|
+
// // throw new Error("fault");
|
|
1882
|
+
// ok = false;
|
|
1883
|
+
// }
|
|
1884
|
+
// prev = top;
|
|
1885
|
+
// i++;
|
|
1886
|
+
// });
|
|
1887
|
+
// return ok;
|
|
1888
|
+
// }
|
|
1626
1889
|
|
|
1627
1890
|
/*
|
|
1628
|
-
* - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
|
|
1891
|
+
* - Traverse all *visible* nodes of the whole tree, i.e. skip collapsed nodes.
|
|
1629
1892
|
* - Store count of rows to `tree.treeRowCount`.
|
|
1630
1893
|
* - Renumber `node._rowIdx` for all visible nodes.
|
|
1631
1894
|
* - Calculate the index range that must be rendered to fill the viewport
|
|
@@ -1633,15 +1896,15 @@ export class Wunderbaum {
|
|
|
1633
1896
|
* -
|
|
1634
1897
|
*/
|
|
1635
1898
|
protected _updateRows(opts?: any): boolean {
|
|
1636
|
-
const label = this.logTime("_updateRows");
|
|
1637
|
-
|
|
1899
|
+
// const label = this.logTime("_updateRows");
|
|
1900
|
+
// this.log("_updateRows", opts)
|
|
1638
1901
|
opts = Object.assign({ newNodesOnly: false }, opts);
|
|
1639
1902
|
const newNodesOnly = !!opts.newNodesOnly;
|
|
1640
1903
|
|
|
1641
1904
|
const row_height = ROW_HEIGHT;
|
|
1642
|
-
const vp_height = this.
|
|
1905
|
+
const vp_height = this.element.clientHeight;
|
|
1643
1906
|
const prefetch = RENDER_MAX_PREFETCH;
|
|
1644
|
-
const ofs = this.
|
|
1907
|
+
const ofs = this.element.scrollTop;
|
|
1645
1908
|
|
|
1646
1909
|
let startIdx = Math.max(0, ofs / row_height - prefetch);
|
|
1647
1910
|
startIdx = Math.floor(startIdx);
|
|
@@ -1669,7 +1932,7 @@ export class Wunderbaum {
|
|
|
1669
1932
|
let prevElem: HTMLDivElement | "first" | "last" = "first";
|
|
1670
1933
|
|
|
1671
1934
|
this.visitRows(function (node) {
|
|
1672
|
-
//
|
|
1935
|
+
// node.log("visit")
|
|
1673
1936
|
const rowDiv = node._rowElem;
|
|
1674
1937
|
|
|
1675
1938
|
// Renumber all expanded nodes
|
|
@@ -1691,8 +1954,11 @@ export class Wunderbaum {
|
|
|
1691
1954
|
} else {
|
|
1692
1955
|
obsoleteNodes.delete(node);
|
|
1693
1956
|
// Create new markup
|
|
1957
|
+
if (rowDiv) {
|
|
1958
|
+
rowDiv.style.top = idx * ROW_HEIGHT + "px";
|
|
1959
|
+
}
|
|
1694
1960
|
node.render({ top: top, after: prevElem });
|
|
1695
|
-
//
|
|
1961
|
+
// node.log("render", top, prevElem, "=>", node._rowElem);
|
|
1696
1962
|
prevElem = node._rowElem!;
|
|
1697
1963
|
}
|
|
1698
1964
|
idx++;
|
|
@@ -1709,8 +1975,8 @@ export class Wunderbaum {
|
|
|
1709
1975
|
// `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
|
|
1710
1976
|
// this.nodeListElement.style.height
|
|
1711
1977
|
// );
|
|
1712
|
-
this.logTimeEnd(label);
|
|
1713
|
-
this._validateRows();
|
|
1978
|
+
// this.logTimeEnd(label);
|
|
1979
|
+
// this._validateRows();
|
|
1714
1980
|
return modified;
|
|
1715
1981
|
}
|
|
1716
1982
|
|
|
@@ -1891,17 +2157,11 @@ export class Wunderbaum {
|
|
|
1891
2157
|
/**
|
|
1892
2158
|
* Reload the tree with a new source.
|
|
1893
2159
|
*
|
|
1894
|
-
* Previous data is cleared.
|
|
1895
|
-
*
|
|
2160
|
+
* Previous data is cleared. Note that also column- and type defintions may
|
|
2161
|
+
* be passed with the `source` object.
|
|
1896
2162
|
*/
|
|
1897
|
-
load(source: any
|
|
2163
|
+
load(source: any) {
|
|
1898
2164
|
this.clear();
|
|
1899
|
-
const columns = options.columns || source.columns;
|
|
1900
|
-
if (columns) {
|
|
1901
|
-
this.columns = options.columns;
|
|
1902
|
-
// this._renderHeaderMarkup();
|
|
1903
|
-
this.updateColumns({ calculateCols: false });
|
|
1904
|
-
}
|
|
1905
2165
|
return this.root.load(source);
|
|
1906
2166
|
}
|
|
1907
2167
|
|
|
@@ -1913,7 +2173,7 @@ export class Wunderbaum {
|
|
|
1913
2173
|
* tree.enableUpdate(false);
|
|
1914
2174
|
* // ... (long running operation that would trigger many updates)
|
|
1915
2175
|
* foo();
|
|
1916
|
-
* // ... NOTE: make sure that async operations have finished
|
|
2176
|
+
* // ... NOTE: make sure that async operations have finished, e.g.
|
|
1917
2177
|
* await foo();
|
|
1918
2178
|
* } finally {
|
|
1919
2179
|
* tree.enableUpdate(true);
|
|
@@ -1937,7 +2197,9 @@ export class Wunderbaum {
|
|
|
1937
2197
|
// `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
|
|
1938
2198
|
// );
|
|
1939
2199
|
if (this._disableUpdateCount === 0) {
|
|
1940
|
-
this.
|
|
2200
|
+
// this.changeRedrawRequestPending = true; // make sure, we re-render all markup
|
|
2201
|
+
// this.updateViewport();
|
|
2202
|
+
this.setModified(ChangeType.any, { immediate: true });
|
|
1941
2203
|
}
|
|
1942
2204
|
} else {
|
|
1943
2205
|
this._disableUpdateCount++;
|