wunderbaum 0.0.1-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 +21 -0
- package/README.md +39 -0
- package/dist/wunderbaum.css +5 -0
- package/dist/wunderbaum.d.ts +1348 -0
- package/dist/wunderbaum.esm.js +5266 -0
- package/dist/wunderbaum.esm.min.js +69 -0
- package/dist/wunderbaum.esm.min.js.map +1 -0
- package/dist/wunderbaum.umd.js +5276 -0
- package/dist/wunderbaum.umd.min.js +71 -0
- package/dist/wunderbaum.umd.min.js.map +1 -0
- package/package.json +123 -0
- package/src/common.ts +198 -0
- package/src/debounce.ts +365 -0
- package/src/deferred.ts +50 -0
- package/src/util.ts +656 -0
- package/src/wb_ext_dnd.ts +336 -0
- package/src/wb_ext_edit.ts +340 -0
- package/src/wb_ext_filter.ts +370 -0
- package/src/wb_ext_keynav.ts +210 -0
- package/src/wb_ext_logger.ts +54 -0
- package/src/wb_extension_base.ts +76 -0
- package/src/wb_node.ts +1780 -0
- package/src/wb_options.ts +117 -0
- package/src/wunderbaum.scss +509 -0
- package/src/wunderbaum.ts +1819 -0
|
@@ -0,0 +1,1819 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* wunderbaum.ts
|
|
3
|
+
*
|
|
4
|
+
* A tree control.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
|
|
7
|
+
* Released under the MIT license.
|
|
8
|
+
*
|
|
9
|
+
* @version @VERSION
|
|
10
|
+
* @date @DATE
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import "./wunderbaum.scss";
|
|
14
|
+
import * as util from "./util";
|
|
15
|
+
import { FilterExtension } from "./wb_ext_filter";
|
|
16
|
+
import { KeynavExtension } from "./wb_ext_keynav";
|
|
17
|
+
import { LoggerExtension } from "./wb_ext_logger";
|
|
18
|
+
import { DndExtension } from "./wb_ext_dnd";
|
|
19
|
+
import { ExtensionsDict, WunderbaumExtension } from "./wb_extension_base";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
NavigationMode,
|
|
23
|
+
ChangeType,
|
|
24
|
+
DEFAULT_DEBUGLEVEL,
|
|
25
|
+
FilterModeType,
|
|
26
|
+
makeNodeTitleStartMatcher,
|
|
27
|
+
MatcherType,
|
|
28
|
+
NavigationModeOption,
|
|
29
|
+
NodeStatusType,
|
|
30
|
+
RENDER_MAX_PREFETCH,
|
|
31
|
+
ROW_HEIGHT,
|
|
32
|
+
TargetType as NodeRegion,
|
|
33
|
+
ApplyCommandType,
|
|
34
|
+
} from "./common";
|
|
35
|
+
import { WunderbaumNode } from "./wb_node";
|
|
36
|
+
import { Deferred } from "./deferred";
|
|
37
|
+
import { DebouncedFunction, throttle } from "./debounce";
|
|
38
|
+
import { EditExtension } from "./wb_ext_edit";
|
|
39
|
+
import { WunderbaumOptions } from "./wb_options";
|
|
40
|
+
|
|
41
|
+
// const class_prefix = "wb-";
|
|
42
|
+
// const node_props: string[] = ["title", "key", "refKey"];
|
|
43
|
+
const MAX_CHANGED_NODES = 10;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A persistent plain object or array.
|
|
47
|
+
*
|
|
48
|
+
* See also [[WunderbaumOptions]].
|
|
49
|
+
*/
|
|
50
|
+
export class Wunderbaum {
|
|
51
|
+
static version: string = "@VERSION"; // Set to semver by 'grunt release'
|
|
52
|
+
static sequence = 0;
|
|
53
|
+
|
|
54
|
+
/** The invisible root node, that holds all visible top level nodes. */
|
|
55
|
+
readonly root: WunderbaumNode;
|
|
56
|
+
readonly id: string;
|
|
57
|
+
|
|
58
|
+
readonly element: HTMLDivElement;
|
|
59
|
+
// readonly treeElement: HTMLDivElement;
|
|
60
|
+
readonly headerElement: HTMLDivElement | null;
|
|
61
|
+
readonly scrollContainer: HTMLDivElement;
|
|
62
|
+
readonly nodeListElement: HTMLDivElement;
|
|
63
|
+
readonly _updateViewportThrottled: DebouncedFunction<(...args: any) => void>;
|
|
64
|
+
|
|
65
|
+
protected extensionList: WunderbaumExtension[] = [];
|
|
66
|
+
protected extensions: ExtensionsDict = {};
|
|
67
|
+
// protected extensionMap = new Map<string, WunderbaumExtension>();
|
|
68
|
+
|
|
69
|
+
/** Merged options from constructor args and tree- and extension defaults. */
|
|
70
|
+
public options: WunderbaumOptions;
|
|
71
|
+
|
|
72
|
+
protected keyMap = new Map<string, WunderbaumNode>();
|
|
73
|
+
protected refKeyMap = new Map<string, Set<WunderbaumNode>>();
|
|
74
|
+
protected viewNodes = new Set<WunderbaumNode>();
|
|
75
|
+
// protected rows: WunderbaumNode[] = [];
|
|
76
|
+
// protected _rowCount = 0;
|
|
77
|
+
|
|
78
|
+
// protected eventHandlers : Array<function> = [];
|
|
79
|
+
|
|
80
|
+
public activeNode: WunderbaumNode | null = null;
|
|
81
|
+
public focusNode: WunderbaumNode | null = null;
|
|
82
|
+
_disableUpdate = 0;
|
|
83
|
+
_disableUpdateCount = 0;
|
|
84
|
+
|
|
85
|
+
/** Shared properties, referenced by `node.type`. */
|
|
86
|
+
public types: { [key: string]: any } = {};
|
|
87
|
+
/** List of column definitions. */
|
|
88
|
+
public columns: any[] = [];
|
|
89
|
+
public _columnsById: { [key: string]: any } = {};
|
|
90
|
+
|
|
91
|
+
protected resizeObserver;
|
|
92
|
+
|
|
93
|
+
// Modification Status
|
|
94
|
+
protected changedSince = 0;
|
|
95
|
+
protected changes = new Set<ChangeType>();
|
|
96
|
+
protected changedNodes = new Set<WunderbaumNode>();
|
|
97
|
+
|
|
98
|
+
// --- FILTER ---
|
|
99
|
+
public filterMode: FilterModeType = null;
|
|
100
|
+
|
|
101
|
+
// --- KEYNAV ---
|
|
102
|
+
public activeColIdx = 0;
|
|
103
|
+
public navMode = NavigationMode.row;
|
|
104
|
+
public lastQuicksearchTime = 0;
|
|
105
|
+
public lastQuicksearchTerm = "";
|
|
106
|
+
|
|
107
|
+
// --- EDIT ---
|
|
108
|
+
protected lastClickTime = 0;
|
|
109
|
+
|
|
110
|
+
readonly ready: Promise<any>;
|
|
111
|
+
|
|
112
|
+
public static util = util;
|
|
113
|
+
// TODO: make accessible in compiled JS like this?
|
|
114
|
+
public _util = util;
|
|
115
|
+
|
|
116
|
+
constructor(options: WunderbaumOptions) {
|
|
117
|
+
let opts = (this.options = util.extend(
|
|
118
|
+
{
|
|
119
|
+
id: null,
|
|
120
|
+
source: null, // URL for GET/PUT, ajax options, or callback
|
|
121
|
+
element: null, // <div class="wunderbaum">
|
|
122
|
+
debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
|
|
123
|
+
header: null, // Show/hide header (pass bool or string)
|
|
124
|
+
headerHeightPx: ROW_HEIGHT,
|
|
125
|
+
rowHeightPx: ROW_HEIGHT,
|
|
126
|
+
columns: null,
|
|
127
|
+
types: null,
|
|
128
|
+
escapeTitles: true,
|
|
129
|
+
showSpinner: false,
|
|
130
|
+
checkbox: true,
|
|
131
|
+
minExpandLevel: 0,
|
|
132
|
+
updateThrottleWait: 200,
|
|
133
|
+
skeleton: false,
|
|
134
|
+
// --- KeyNav ---
|
|
135
|
+
navigationMode: NavigationModeOption.startRow,
|
|
136
|
+
quicksearch: true,
|
|
137
|
+
// --- Events ---
|
|
138
|
+
change: util.noop,
|
|
139
|
+
enhanceTitle: util.noop,
|
|
140
|
+
error: util.noop,
|
|
141
|
+
receive: util.noop,
|
|
142
|
+
// --- Strings ---
|
|
143
|
+
strings: {
|
|
144
|
+
loadError: "Error",
|
|
145
|
+
loading: "Loading...",
|
|
146
|
+
// loading: "Loading…",
|
|
147
|
+
noData: "No data",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
options
|
|
151
|
+
));
|
|
152
|
+
|
|
153
|
+
const readyDeferred = new Deferred();
|
|
154
|
+
this.ready = readyDeferred.promise();
|
|
155
|
+
let readyOk = false;
|
|
156
|
+
this.ready
|
|
157
|
+
.then(() => {
|
|
158
|
+
readyOk = true;
|
|
159
|
+
try {
|
|
160
|
+
this._callEvent("init");
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// We re-raise in the reject handler, but Chrome resets the stack
|
|
163
|
+
// frame then, so we log it here:
|
|
164
|
+
console.error("Exception inside `init(e)` event:", error);
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
.catch((err) => {
|
|
168
|
+
if (readyOk) {
|
|
169
|
+
// Error occurred in `init` handler. We can re-raise, but Chrome
|
|
170
|
+
// resets the stack frame.
|
|
171
|
+
throw err;
|
|
172
|
+
} else {
|
|
173
|
+
// Error in load process
|
|
174
|
+
this._callEvent("init", { error: err });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
|
|
179
|
+
this.root = new WunderbaumNode(this, <WunderbaumNode>(<unknown>null), {
|
|
180
|
+
key: "__root__",
|
|
181
|
+
// title: "__root__",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this._registerExtension(new KeynavExtension(this));
|
|
185
|
+
this._registerExtension(new EditExtension(this));
|
|
186
|
+
this._registerExtension(new FilterExtension(this));
|
|
187
|
+
this._registerExtension(new DndExtension(this));
|
|
188
|
+
this._registerExtension(new LoggerExtension(this));
|
|
189
|
+
|
|
190
|
+
// --- Evaluate options
|
|
191
|
+
this.columns = opts.columns;
|
|
192
|
+
delete opts.columns;
|
|
193
|
+
if (!this.columns) {
|
|
194
|
+
let defaultName = typeof opts.header === "string" ? opts.header : this.id;
|
|
195
|
+
this.columns = [{ id: "*", title: defaultName, width: "*" }];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.types = opts.types || {};
|
|
199
|
+
delete opts.types;
|
|
200
|
+
// Convert `TYPE.classes` to a Set
|
|
201
|
+
for (let t of Object.values(this.types) as any) {
|
|
202
|
+
if (t.classes) {
|
|
203
|
+
t.classes = util.toSet(t.classes);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this.columns.length === 1) {
|
|
208
|
+
opts.navigationMode = NavigationModeOption.row;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
opts.navigationMode === NavigationModeOption.cell ||
|
|
213
|
+
opts.navigationMode === NavigationModeOption.startCell
|
|
214
|
+
) {
|
|
215
|
+
this.navMode = NavigationMode.cellNav;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this._updateViewportThrottled = throttle(
|
|
219
|
+
() => {
|
|
220
|
+
this._updateViewport();
|
|
221
|
+
},
|
|
222
|
+
opts.updateThrottleWait,
|
|
223
|
+
{ leading: true, trailing: true }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// --- Create Markup
|
|
227
|
+
this.element = util.elemFromSelector(opts.element) as HTMLDivElement;
|
|
228
|
+
util.assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
|
|
229
|
+
|
|
230
|
+
this.element.classList.add("wunderbaum");
|
|
231
|
+
if (!this.element.getAttribute("tabindex")) {
|
|
232
|
+
this.element.tabIndex = 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Attach tree instance to <div>
|
|
236
|
+
(<any>this.element)._wb_tree = this;
|
|
237
|
+
|
|
238
|
+
// Create header markup, or take it from the existing html
|
|
239
|
+
|
|
240
|
+
this.headerElement = this.element.querySelector(
|
|
241
|
+
"div.wb-header"
|
|
242
|
+
) as HTMLDivElement;
|
|
243
|
+
|
|
244
|
+
const wantHeader =
|
|
245
|
+
opts.header == null ? this.columns.length > 1 : !!opts.header;
|
|
246
|
+
|
|
247
|
+
if (this.headerElement) {
|
|
248
|
+
// User existing header markup to define `this.columns`
|
|
249
|
+
util.assert(
|
|
250
|
+
!this.columns,
|
|
251
|
+
"`opts.columns` must not be set if markup already contains a header"
|
|
252
|
+
);
|
|
253
|
+
this.columns = [];
|
|
254
|
+
const rowElement = this.headerElement.querySelector(
|
|
255
|
+
"div.wb-row"
|
|
256
|
+
) as HTMLDivElement;
|
|
257
|
+
for (const colDiv of rowElement.querySelectorAll("div")) {
|
|
258
|
+
this.columns.push({
|
|
259
|
+
id: colDiv.dataset.id || null,
|
|
260
|
+
text: "" + colDiv.textContent,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} else if (wantHeader) {
|
|
264
|
+
// We need a row div, the rest will be computed from `this.columns`
|
|
265
|
+
const coldivs = "<span class='wb-col'></span>".repeat(
|
|
266
|
+
this.columns.length
|
|
267
|
+
);
|
|
268
|
+
this.element.innerHTML = `
|
|
269
|
+
<div class='wb-header'>
|
|
270
|
+
<div class='wb-row'>
|
|
271
|
+
${coldivs}
|
|
272
|
+
</div>
|
|
273
|
+
</div>`;
|
|
274
|
+
// this.updateColumns({ render: false });
|
|
275
|
+
} else {
|
|
276
|
+
this.element.innerHTML = "";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
//
|
|
280
|
+
this.element.innerHTML += `
|
|
281
|
+
<div class="wb-scroll-container">
|
|
282
|
+
<div class="wb-node-list"></div>
|
|
283
|
+
</div>`;
|
|
284
|
+
this.scrollContainer = this.element.querySelector(
|
|
285
|
+
"div.wb-scroll-container"
|
|
286
|
+
) as HTMLDivElement;
|
|
287
|
+
this.nodeListElement = this.scrollContainer.querySelector(
|
|
288
|
+
"div.wb-node-list"
|
|
289
|
+
) as HTMLDivElement;
|
|
290
|
+
this.headerElement = this.element.querySelector(
|
|
291
|
+
"div.wb-header"
|
|
292
|
+
) as HTMLDivElement;
|
|
293
|
+
|
|
294
|
+
if (this.columns.length > 1) {
|
|
295
|
+
this.element.classList.add("wb-grid");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this._initExtensions();
|
|
299
|
+
|
|
300
|
+
// --- Load initial data
|
|
301
|
+
if (opts.source) {
|
|
302
|
+
if (opts.showSpinner) {
|
|
303
|
+
this.nodeListElement.innerHTML =
|
|
304
|
+
"<progress class='spinner'>loading...</progress>";
|
|
305
|
+
}
|
|
306
|
+
this.load(opts.source)
|
|
307
|
+
.then(() => {
|
|
308
|
+
readyDeferred.resolve();
|
|
309
|
+
})
|
|
310
|
+
.catch((error) => {
|
|
311
|
+
readyDeferred.reject(error);
|
|
312
|
+
})
|
|
313
|
+
.finally(() => {
|
|
314
|
+
this.element.querySelector("progress.spinner")?.remove();
|
|
315
|
+
this.element.classList.remove("wb-initializing");
|
|
316
|
+
// this.updateViewport();
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
// this.updateViewport();
|
|
320
|
+
readyDeferred.resolve();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// TODO: This is sometimes required, because this.element.clientWidth
|
|
324
|
+
// has a wrong value at start???
|
|
325
|
+
setTimeout(() => {
|
|
326
|
+
this.updateViewport();
|
|
327
|
+
}, 50);
|
|
328
|
+
|
|
329
|
+
// --- Bind listeners
|
|
330
|
+
this.scrollContainer.addEventListener("scroll", (e: Event) => {
|
|
331
|
+
this.updateViewport();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// window.addEventListener("resize", (e: Event) => {
|
|
335
|
+
// this.updateViewport();
|
|
336
|
+
// });
|
|
337
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
338
|
+
this.updateViewport();
|
|
339
|
+
console.log("ResizeObserver: Size changed", entries);
|
|
340
|
+
});
|
|
341
|
+
this.resizeObserver.observe(this.element);
|
|
342
|
+
|
|
343
|
+
util.onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
|
|
344
|
+
const info = Wunderbaum.getEventInfo(e);
|
|
345
|
+
const node = info.node;
|
|
346
|
+
|
|
347
|
+
if (
|
|
348
|
+
this._callEvent("click", { event: e, node: node, info: info }) === false
|
|
349
|
+
) {
|
|
350
|
+
this.lastClickTime = Date.now();
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
if (node) {
|
|
354
|
+
// Edit title if 'clickActive' is triggered:
|
|
355
|
+
const trigger = this.getOption("edit.trigger");
|
|
356
|
+
const slowClickDelay = this.getOption("edit.slowClickDelay");
|
|
357
|
+
if (
|
|
358
|
+
trigger.indexOf("clickActive") >= 0 &&
|
|
359
|
+
info.region === "title" &&
|
|
360
|
+
node.isActive() &&
|
|
361
|
+
(!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)
|
|
362
|
+
) {
|
|
363
|
+
this._callMethod("edit.startEditTitle", node);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (info.colIdx >= 0) {
|
|
367
|
+
node.setActive(true, { colIdx: info.colIdx, event: e });
|
|
368
|
+
} else {
|
|
369
|
+
node.setActive(true, { event: e });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (info.region === NodeRegion.expander) {
|
|
373
|
+
node.setExpanded(!node.isExpanded());
|
|
374
|
+
} else if (info.region === NodeRegion.checkbox) {
|
|
375
|
+
node.setSelected(!node.isSelected());
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// if(e.target.classList.)
|
|
379
|
+
// this.log("click", info);
|
|
380
|
+
this.lastClickTime = Date.now();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
util.onEvent(this.element, "keydown", (e) => {
|
|
384
|
+
const info = Wunderbaum.getEventInfo(e);
|
|
385
|
+
const eventName = util.eventToString(e);
|
|
386
|
+
this._callHook("onKeyEvent", {
|
|
387
|
+
event: e,
|
|
388
|
+
node: info.node,
|
|
389
|
+
info: info,
|
|
390
|
+
eventName: eventName,
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
util.onEvent(this.element, "focusin focusout", (e) => {
|
|
395
|
+
const flag = e.type === "focusin";
|
|
396
|
+
|
|
397
|
+
this._callEvent("focus", { flag: flag, event: e });
|
|
398
|
+
if (!flag) {
|
|
399
|
+
this._callMethod("edit._stopEditTitle", true, {
|
|
400
|
+
event: e,
|
|
401
|
+
forceClose: true,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
// if (flag && !this.activeNode ) {
|
|
405
|
+
// setTimeout(() => {
|
|
406
|
+
// if (!this.activeNode) {
|
|
407
|
+
// const firstNode = this.getFirstChild();
|
|
408
|
+
// if (firstNode && !firstNode?.isStatusNode()) {
|
|
409
|
+
// firstNode.logInfo("Activate on focus", e);
|
|
410
|
+
// firstNode.setActive(true, { event: e });
|
|
411
|
+
// }
|
|
412
|
+
// }
|
|
413
|
+
// }, 10);
|
|
414
|
+
// }
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** */
|
|
419
|
+
// _renderHeader(){
|
|
420
|
+
// const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
|
|
421
|
+
// this.element.innerHTML = `
|
|
422
|
+
// <div class='wb-header'>
|
|
423
|
+
// <div class='wb-row'>
|
|
424
|
+
// ${coldivs}
|
|
425
|
+
// </div>
|
|
426
|
+
// </div>`;
|
|
427
|
+
|
|
428
|
+
// }
|
|
429
|
+
|
|
430
|
+
/** Return a Wunderbaum instance, from element, index, or event.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* getTree(); // Get first Wunderbaum instance on page
|
|
434
|
+
* getTree(1); // Get second Wunderbaum instance on page
|
|
435
|
+
* getTree(event); // Get tree for this mouse- or keyboard event
|
|
436
|
+
* getTree("foo"); // Get tree for this `tree.options.id`
|
|
437
|
+
* getTree("#tree"); // Get tree for this matching element
|
|
438
|
+
*/
|
|
439
|
+
public static getTree(
|
|
440
|
+
el?: Element | Event | number | string | WunderbaumNode
|
|
441
|
+
): Wunderbaum | null {
|
|
442
|
+
if (el instanceof Wunderbaum) {
|
|
443
|
+
return el;
|
|
444
|
+
} else if (el instanceof WunderbaumNode) {
|
|
445
|
+
return el.tree;
|
|
446
|
+
}
|
|
447
|
+
if (el === undefined) {
|
|
448
|
+
el = 0; // get first tree
|
|
449
|
+
}
|
|
450
|
+
if (typeof el === "number") {
|
|
451
|
+
el = document.querySelectorAll(".wunderbaum")[el]; // el was an integer: return nth element
|
|
452
|
+
} else if (typeof el === "string") {
|
|
453
|
+
// Search all trees for matching ID
|
|
454
|
+
for (let treeElem of document.querySelectorAll(".wunderbaum")) {
|
|
455
|
+
const tree = (<any>treeElem)._wb_tree;
|
|
456
|
+
if (tree && tree.id === el) {
|
|
457
|
+
return tree;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Search by selector
|
|
461
|
+
el = document.querySelector(el)!;
|
|
462
|
+
if (!el) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
} else if ((<Event>el).target) {
|
|
466
|
+
el = (<Event>el).target as Element;
|
|
467
|
+
}
|
|
468
|
+
util.assert(el instanceof Element);
|
|
469
|
+
if (!(<HTMLElement>el).matches(".wunderbaum")) {
|
|
470
|
+
el = (<HTMLElement>el).closest(".wunderbaum")!;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (el && (<any>el)._wb_tree) {
|
|
474
|
+
return (<any>el)._wb_tree;
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Return a WunderbaumNode instance from element, event.
|
|
480
|
+
*
|
|
481
|
+
* @param el
|
|
482
|
+
*/
|
|
483
|
+
public static getNode(el: Element | Event): WunderbaumNode | null {
|
|
484
|
+
if (!el) {
|
|
485
|
+
return null;
|
|
486
|
+
} else if (el instanceof WunderbaumNode) {
|
|
487
|
+
return el;
|
|
488
|
+
} else if ((<Event>el).target !== undefined) {
|
|
489
|
+
el = (<Event>el).target! as Element; // el was an Event
|
|
490
|
+
}
|
|
491
|
+
// `el` is a DOM element
|
|
492
|
+
// let nodeElem = obj.closest("div.wb-row");
|
|
493
|
+
while (el) {
|
|
494
|
+
if ((<any>el)._wb_node) {
|
|
495
|
+
return (<any>el)._wb_node as WunderbaumNode;
|
|
496
|
+
}
|
|
497
|
+
el = (<Element>el).parentElement!; //.parentNode;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** */
|
|
503
|
+
protected _registerExtension(extension: WunderbaumExtension): void {
|
|
504
|
+
this.extensionList.push(extension);
|
|
505
|
+
this.extensions[extension.id] = extension;
|
|
506
|
+
// this.extensionMap.set(extension.id, extension);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Called on tree (re)init after markup is created, before loading. */
|
|
510
|
+
protected _initExtensions(): void {
|
|
511
|
+
for (let ext of this.extensionList) {
|
|
512
|
+
ext.init();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Add node to tree's bookkeeping data structures. */
|
|
517
|
+
_registerNode(node: WunderbaumNode): void {
|
|
518
|
+
let key = node.key;
|
|
519
|
+
util.assert(key != null && !this.keyMap.has(key));
|
|
520
|
+
this.keyMap.set(key, node);
|
|
521
|
+
let rk = node.refKey;
|
|
522
|
+
if (rk) {
|
|
523
|
+
let rks = this.refKeyMap.get(rk); // Set of nodes with this refKey
|
|
524
|
+
if (rks) {
|
|
525
|
+
rks.add(node);
|
|
526
|
+
} else {
|
|
527
|
+
this.refKeyMap.set(rk, new Set());
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Remove node from tree's bookkeeping data structures. */
|
|
533
|
+
_unregisterNode(node: WunderbaumNode): void {
|
|
534
|
+
const rk = node.refKey;
|
|
535
|
+
if (rk) {
|
|
536
|
+
const rks = this.refKeyMap.get(rk);
|
|
537
|
+
if (rks && rks.delete(node) && !rks.size) {
|
|
538
|
+
// We just removed the last element
|
|
539
|
+
this.refKeyMap.delete(rk);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// mark as disposed
|
|
543
|
+
(node.tree as any) = null;
|
|
544
|
+
(node.parent as any) = null;
|
|
545
|
+
// node.title = "DISPOSED: " + node.title
|
|
546
|
+
this.viewNodes.delete(node);
|
|
547
|
+
node.removeMarkup();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Call all hook methods of all registered extensions.*/
|
|
551
|
+
protected _callHook(hook: keyof WunderbaumExtension, data: any = {}): any {
|
|
552
|
+
let res;
|
|
553
|
+
let d = util.extend(
|
|
554
|
+
{},
|
|
555
|
+
{ tree: this, options: this.options, result: undefined },
|
|
556
|
+
data
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
for (let ext of this.extensionList) {
|
|
560
|
+
res = (<any>ext[hook]).call(ext, d);
|
|
561
|
+
if (res === false) {
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
if (d.result !== undefined) {
|
|
565
|
+
res = d.result;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return res;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Call tree method or extension method if defined.
|
|
572
|
+
* Example:
|
|
573
|
+
* ```js
|
|
574
|
+
* tree._callMethod("edit.startEdit", "arg1", "arg2")
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
_callMethod(name: string, ...args: any[]): any {
|
|
578
|
+
const [p, n] = name.split(".");
|
|
579
|
+
const obj = n ? this.extensions[p] : this;
|
|
580
|
+
const func = (<any>obj)[n];
|
|
581
|
+
if (func) {
|
|
582
|
+
return func.apply(obj, args);
|
|
583
|
+
} else {
|
|
584
|
+
this.logError(`Calling undefined method '${name}()'.`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Call event handler if defined in tree.options.
|
|
589
|
+
* Example:
|
|
590
|
+
* ```js
|
|
591
|
+
* tree._callEvent("edit.beforeEdit", {foo: 42})
|
|
592
|
+
* ```
|
|
593
|
+
*/
|
|
594
|
+
_callEvent(name: string, extra?: any): any {
|
|
595
|
+
const [p, n] = name.split(".");
|
|
596
|
+
const opts = this.options as any;
|
|
597
|
+
const func = n ? opts[p][n] : opts[p];
|
|
598
|
+
if (func) {
|
|
599
|
+
return func.call(
|
|
600
|
+
this,
|
|
601
|
+
util.extend({ name: name, tree: this, util: this._util }, extra)
|
|
602
|
+
);
|
|
603
|
+
// } else {
|
|
604
|
+
// this.logError(`Triggering undefined event '${name}'.`)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Return the topmost visible node in the viewport */
|
|
609
|
+
protected _firstNodeInView(complete = true) {
|
|
610
|
+
let topIdx: number, node: WunderbaumNode;
|
|
611
|
+
if (complete) {
|
|
612
|
+
topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
|
|
613
|
+
} else {
|
|
614
|
+
topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
|
|
615
|
+
}
|
|
616
|
+
// TODO: start searching from active node (reverse)
|
|
617
|
+
this.visitRows((n) => {
|
|
618
|
+
if (n._rowIdx === topIdx) {
|
|
619
|
+
node = n;
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
return <WunderbaumNode>node!;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Return the lowest visible node in the viewport */
|
|
627
|
+
protected _lastNodeInView(complete = true) {
|
|
628
|
+
let bottomIdx: number, node: WunderbaumNode;
|
|
629
|
+
if (complete) {
|
|
630
|
+
bottomIdx =
|
|
631
|
+
Math.floor(
|
|
632
|
+
(this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
|
|
633
|
+
ROW_HEIGHT
|
|
634
|
+
) - 1;
|
|
635
|
+
} else {
|
|
636
|
+
bottomIdx =
|
|
637
|
+
Math.ceil(
|
|
638
|
+
(this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
|
|
639
|
+
ROW_HEIGHT
|
|
640
|
+
) - 1;
|
|
641
|
+
}
|
|
642
|
+
// TODO: start searching from active node
|
|
643
|
+
this.visitRows((n) => {
|
|
644
|
+
if (n._rowIdx === bottomIdx) {
|
|
645
|
+
node = n;
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
return <WunderbaumNode>node!;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Return preceeding visible node in the viewport */
|
|
653
|
+
protected _getPrevNodeInView(node?: WunderbaumNode, ofs = 1) {
|
|
654
|
+
this.visitRows(
|
|
655
|
+
(n) => {
|
|
656
|
+
node = n;
|
|
657
|
+
if (ofs-- <= 0) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
{ reverse: true, start: node || this.getActiveNode() }
|
|
662
|
+
);
|
|
663
|
+
return node;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Return following visible node in the viewport */
|
|
667
|
+
protected _getNextNodeInView(node?: WunderbaumNode, ofs = 1) {
|
|
668
|
+
this.visitRows(
|
|
669
|
+
(n) => {
|
|
670
|
+
node = n;
|
|
671
|
+
if (ofs-- <= 0) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
{ reverse: false, start: node || this.getActiveNode() }
|
|
676
|
+
);
|
|
677
|
+
return node;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
addChildren(nodeData: any, options?: any): WunderbaumNode {
|
|
681
|
+
return this.root.addChildren(nodeData, options);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Apply a modification (or navigation) operation on the tree or active node.
|
|
686
|
+
* @returns
|
|
687
|
+
*/
|
|
688
|
+
applyCommand(cmd: ApplyCommandType, opts?: any): any;
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Apply a modification (or navigation) operation on a node.
|
|
692
|
+
* @returns
|
|
693
|
+
*/
|
|
694
|
+
applyCommand(cmd: ApplyCommandType, node: WunderbaumNode, opts?: any): any;
|
|
695
|
+
|
|
696
|
+
/*
|
|
697
|
+
* Apply a modification or navigation operation.
|
|
698
|
+
*
|
|
699
|
+
* Most of these commands simply map to a node or tree method.
|
|
700
|
+
* This method is especially useful when implementing keyboard mapping,
|
|
701
|
+
* context menus, or external buttons.
|
|
702
|
+
*
|
|
703
|
+
* Valid commands:
|
|
704
|
+
* - 'moveUp', 'moveDown'
|
|
705
|
+
* - 'indent', 'outdent'
|
|
706
|
+
* - 'remove'
|
|
707
|
+
* - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension)
|
|
708
|
+
* - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard')
|
|
709
|
+
* - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate
|
|
710
|
+
*
|
|
711
|
+
*/
|
|
712
|
+
applyCommand(
|
|
713
|
+
cmd: ApplyCommandType,
|
|
714
|
+
nodeOrOpts?: WunderbaumNode | any,
|
|
715
|
+
opts?: any
|
|
716
|
+
): any {
|
|
717
|
+
let // clipboard,
|
|
718
|
+
node,
|
|
719
|
+
refNode;
|
|
720
|
+
// opts = $.extend(
|
|
721
|
+
// { setActive: true, clipboard: CLIPBOARD },
|
|
722
|
+
// opts_
|
|
723
|
+
// );
|
|
724
|
+
if (nodeOrOpts instanceof WunderbaumNode) {
|
|
725
|
+
node = nodeOrOpts;
|
|
726
|
+
} else {
|
|
727
|
+
node = this.getActiveNode()!;
|
|
728
|
+
util.assert(opts === undefined);
|
|
729
|
+
opts = nodeOrOpts;
|
|
730
|
+
}
|
|
731
|
+
// clipboard = opts.clipboard;
|
|
732
|
+
|
|
733
|
+
switch (cmd) {
|
|
734
|
+
// Sorting and indentation:
|
|
735
|
+
case "moveUp":
|
|
736
|
+
refNode = node.getPrevSibling();
|
|
737
|
+
if (refNode) {
|
|
738
|
+
node.moveTo(refNode, "before");
|
|
739
|
+
node.setActive();
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
case "moveDown":
|
|
743
|
+
refNode = node.getNextSibling();
|
|
744
|
+
if (refNode) {
|
|
745
|
+
node.moveTo(refNode, "after");
|
|
746
|
+
node.setActive();
|
|
747
|
+
}
|
|
748
|
+
break;
|
|
749
|
+
case "indent":
|
|
750
|
+
refNode = node.getPrevSibling();
|
|
751
|
+
if (refNode) {
|
|
752
|
+
node.moveTo(refNode, "appendChild");
|
|
753
|
+
refNode.setExpanded();
|
|
754
|
+
node.setActive();
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
case "outdent":
|
|
758
|
+
if (!node.isTopLevel()) {
|
|
759
|
+
node.moveTo(node.getParent()!, "after");
|
|
760
|
+
node.setActive();
|
|
761
|
+
}
|
|
762
|
+
break;
|
|
763
|
+
// Remove:
|
|
764
|
+
case "remove":
|
|
765
|
+
refNode = node.getPrevSibling() || node.getParent();
|
|
766
|
+
node.remove();
|
|
767
|
+
if (refNode) {
|
|
768
|
+
refNode.setActive();
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
// Add, edit (requires ext-edit):
|
|
772
|
+
case "addChild":
|
|
773
|
+
this._callMethod("edit.createNode", "prependChild");
|
|
774
|
+
break;
|
|
775
|
+
case "addSibling":
|
|
776
|
+
this._callMethod("edit.createNode", "after");
|
|
777
|
+
break;
|
|
778
|
+
case "rename":
|
|
779
|
+
this._callMethod("edit.startEditTitle");
|
|
780
|
+
break;
|
|
781
|
+
// Simple clipboard simulation:
|
|
782
|
+
// case "cut":
|
|
783
|
+
// clipboard = { mode: cmd, data: node };
|
|
784
|
+
// break;
|
|
785
|
+
// case "copy":
|
|
786
|
+
// clipboard = {
|
|
787
|
+
// mode: cmd,
|
|
788
|
+
// data: node.toDict(function(d, n) {
|
|
789
|
+
// delete d.key;
|
|
790
|
+
// }),
|
|
791
|
+
// };
|
|
792
|
+
// break;
|
|
793
|
+
// case "clear":
|
|
794
|
+
// clipboard = null;
|
|
795
|
+
// break;
|
|
796
|
+
// case "paste":
|
|
797
|
+
// if (clipboard.mode === "cut") {
|
|
798
|
+
// // refNode = node.getPrevSibling();
|
|
799
|
+
// clipboard.data.moveTo(node, "child");
|
|
800
|
+
// clipboard.data.setActive();
|
|
801
|
+
// } else if (clipboard.mode === "copy") {
|
|
802
|
+
// node.addChildren(clipboard.data).setActive();
|
|
803
|
+
// }
|
|
804
|
+
// break;
|
|
805
|
+
// Navigation commands:
|
|
806
|
+
case "down":
|
|
807
|
+
case "first":
|
|
808
|
+
case "last":
|
|
809
|
+
case "left":
|
|
810
|
+
case "pageDown":
|
|
811
|
+
case "pageUp":
|
|
812
|
+
case "parent":
|
|
813
|
+
case "right":
|
|
814
|
+
case "up":
|
|
815
|
+
return node.navigate(cmd);
|
|
816
|
+
default:
|
|
817
|
+
util.error(`Unhandled command: '${cmd}'`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Delete all nodes. */
|
|
822
|
+
clear() {
|
|
823
|
+
this.root.removeChildren();
|
|
824
|
+
this.root.children = null;
|
|
825
|
+
this.keyMap.clear();
|
|
826
|
+
this.refKeyMap.clear();
|
|
827
|
+
this.viewNodes.clear();
|
|
828
|
+
this.activeNode = null;
|
|
829
|
+
this.focusNode = null;
|
|
830
|
+
|
|
831
|
+
// this.types = {};
|
|
832
|
+
// this. columns =[];
|
|
833
|
+
// this._columnsById = {};
|
|
834
|
+
|
|
835
|
+
// Modification Status
|
|
836
|
+
this.changedSince = 0;
|
|
837
|
+
this.changes.clear();
|
|
838
|
+
this.changedNodes.clear();
|
|
839
|
+
|
|
840
|
+
// // --- FILTER ---
|
|
841
|
+
// public filterMode: FilterModeType = null;
|
|
842
|
+
|
|
843
|
+
// // --- KEYNAV ---
|
|
844
|
+
// public activeColIdx = 0;
|
|
845
|
+
// public cellNavMode = false;
|
|
846
|
+
// public lastQuicksearchTime = 0;
|
|
847
|
+
// public lastQuicksearchTerm = "";
|
|
848
|
+
this.updateViewport();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Clear nodes and markup and detach events and observers.
|
|
853
|
+
*
|
|
854
|
+
* This method may be useful to free up resources before re-creating a tree
|
|
855
|
+
* on an existing div, for example in unittest suites.
|
|
856
|
+
* Note that this Wunderbaum instance becomes unusable afterwards.
|
|
857
|
+
*/
|
|
858
|
+
public destroy() {
|
|
859
|
+
this.logInfo("destroy()...");
|
|
860
|
+
this.clear();
|
|
861
|
+
this.resizeObserver.disconnect();
|
|
862
|
+
this.element.innerHTML = "";
|
|
863
|
+
// Remove all event handlers
|
|
864
|
+
this.element.outerHTML = this.element.outerHTML;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Return `tree.option.NAME` (also resolving if this is a callback).
|
|
869
|
+
*
|
|
870
|
+
* See also [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
|
|
871
|
+
* `tree.types[node.type].NAME`.
|
|
872
|
+
*
|
|
873
|
+
* @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
|
|
874
|
+
*/
|
|
875
|
+
getOption(name: string, defaultValue?: any): any {
|
|
876
|
+
let ext;
|
|
877
|
+
let opts = this.options as any;
|
|
878
|
+
|
|
879
|
+
// Lookup `name` in options dict
|
|
880
|
+
if (name.indexOf(".") >= 0) {
|
|
881
|
+
[ext, name] = name.split(".");
|
|
882
|
+
opts = opts[ext];
|
|
883
|
+
}
|
|
884
|
+
let value = opts[name];
|
|
885
|
+
|
|
886
|
+
// A callback resolver always takes precedence
|
|
887
|
+
if (typeof value === "function") {
|
|
888
|
+
value = value({ type: "resolve", tree: this });
|
|
889
|
+
}
|
|
890
|
+
// Use value from value options dict, fallback do default
|
|
891
|
+
return value ?? defaultValue;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
*
|
|
896
|
+
* @param name
|
|
897
|
+
* @param value
|
|
898
|
+
*/
|
|
899
|
+
setOption(name: string, value: any): void {
|
|
900
|
+
if (name.indexOf(".") === -1) {
|
|
901
|
+
(this.options as any)[name] = value;
|
|
902
|
+
// switch (name) {
|
|
903
|
+
// case value:
|
|
904
|
+
// break;
|
|
905
|
+
// default:
|
|
906
|
+
// break;
|
|
907
|
+
// }
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const parts = name.split(".");
|
|
911
|
+
const ext = this.extensions[parts[0]];
|
|
912
|
+
ext!.setPluginOption(parts[1], value);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**Return true if the tree (or one of its nodes) has the input focus. */
|
|
916
|
+
hasFocus() {
|
|
917
|
+
return this.element.contains(document.activeElement);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Run code, but defer `updateViewport()` until done. */
|
|
921
|
+
runWithoutUpdate(func: () => any, hint = null): void {
|
|
922
|
+
// const prev = this._disableUpdate;
|
|
923
|
+
// const start = Date.now();
|
|
924
|
+
// this._disableUpdate = Date.now();
|
|
925
|
+
try {
|
|
926
|
+
this.enableUpdate(false);
|
|
927
|
+
return func();
|
|
928
|
+
} finally {
|
|
929
|
+
this.enableUpdate(true);
|
|
930
|
+
// if (!prev && this._disableUpdate === start) {
|
|
931
|
+
// this._disableUpdate = 0;
|
|
932
|
+
// }
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Recursively expand all expandable nodes (triggers lazy load id needed). */
|
|
937
|
+
async expandAll(flag: boolean = true) {
|
|
938
|
+
const tag = this.logTime("expandAll(" + flag + ")");
|
|
939
|
+
try {
|
|
940
|
+
this.enableUpdate(false);
|
|
941
|
+
await this.root.expandAll(flag);
|
|
942
|
+
} finally {
|
|
943
|
+
this.enableUpdate(true);
|
|
944
|
+
this.logTimeEnd(tag);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Return the number of nodes in the data model.*/
|
|
949
|
+
count(visible = false) {
|
|
950
|
+
if (visible) {
|
|
951
|
+
return this.viewNodes.size;
|
|
952
|
+
}
|
|
953
|
+
return this.keyMap.size;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/* Internal sanity check. */
|
|
957
|
+
_check() {
|
|
958
|
+
let i = 0;
|
|
959
|
+
this.visit((n) => {
|
|
960
|
+
i++;
|
|
961
|
+
});
|
|
962
|
+
if (this.keyMap.size !== i) {
|
|
963
|
+
this.logWarn(`_check failed: ${this.keyMap.size} !== ${i}`);
|
|
964
|
+
}
|
|
965
|
+
// util.assert(this.keyMap.size === i);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**Find all nodes that matches condition.
|
|
969
|
+
*
|
|
970
|
+
* @param match title string to search for, or a
|
|
971
|
+
* callback function that returns `true` if a node is matched.
|
|
972
|
+
* @see [[WunderbaumNode.findAll]]
|
|
973
|
+
*/
|
|
974
|
+
findAll(match: string | MatcherType) {
|
|
975
|
+
return this.root.findAll(match);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**Find first node that matches condition.
|
|
979
|
+
*
|
|
980
|
+
* @param match title string to search for, or a
|
|
981
|
+
* callback function that returns `true` if a node is matched.
|
|
982
|
+
* @see [[WunderbaumNode.findFirst]]
|
|
983
|
+
*/
|
|
984
|
+
findFirst(match: string | MatcherType) {
|
|
985
|
+
return this.root.findFirst(match);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** Find the next visible node that starts with `match`, starting at `startNode`
|
|
989
|
+
* and wrap-around at the end.
|
|
990
|
+
*/
|
|
991
|
+
findNextNode(
|
|
992
|
+
match: string | MatcherType,
|
|
993
|
+
startNode?: WunderbaumNode | null
|
|
994
|
+
): WunderbaumNode | null {
|
|
995
|
+
//, visibleOnly) {
|
|
996
|
+
let res: WunderbaumNode | null = null,
|
|
997
|
+
firstNode = this.getFirstChild()!;
|
|
998
|
+
|
|
999
|
+
let matcher =
|
|
1000
|
+
typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
|
|
1001
|
+
startNode = startNode || firstNode;
|
|
1002
|
+
|
|
1003
|
+
function _checkNode(n: WunderbaumNode) {
|
|
1004
|
+
// console.log("_check " + n)
|
|
1005
|
+
if (matcher(n)) {
|
|
1006
|
+
res = n;
|
|
1007
|
+
}
|
|
1008
|
+
if (res || n === startNode) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
this.visitRows(_checkNode, {
|
|
1013
|
+
start: startNode,
|
|
1014
|
+
includeSelf: false,
|
|
1015
|
+
});
|
|
1016
|
+
// Wrap around search
|
|
1017
|
+
if (!res && startNode !== firstNode) {
|
|
1018
|
+
this.visitRows(_checkNode, {
|
|
1019
|
+
start: firstNode,
|
|
1020
|
+
includeSelf: true,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return res;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Find a node relative to another node.
|
|
1027
|
+
*
|
|
1028
|
+
* @param node
|
|
1029
|
+
* @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
|
|
1030
|
+
* (Alternatively the keyCode that would normally trigger this move,
|
|
1031
|
+
* e.g. `$.ui.keyCode.LEFT` = 'left'.
|
|
1032
|
+
* @param includeHidden Not yet implemented
|
|
1033
|
+
*/
|
|
1034
|
+
findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) {
|
|
1035
|
+
let res = null;
|
|
1036
|
+
let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
|
|
1037
|
+
|
|
1038
|
+
switch (where) {
|
|
1039
|
+
case "parent":
|
|
1040
|
+
if (node.parent && node.parent.parent) {
|
|
1041
|
+
res = node.parent;
|
|
1042
|
+
}
|
|
1043
|
+
break;
|
|
1044
|
+
case "first":
|
|
1045
|
+
// First visible node
|
|
1046
|
+
this.visit(function (n) {
|
|
1047
|
+
if (n.isVisible()) {
|
|
1048
|
+
res = n;
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
break;
|
|
1053
|
+
case "last":
|
|
1054
|
+
this.visit(function (n) {
|
|
1055
|
+
// last visible node
|
|
1056
|
+
if (n.isVisible()) {
|
|
1057
|
+
res = n;
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
break;
|
|
1061
|
+
case "left":
|
|
1062
|
+
if (node.parent && node.parent.parent) {
|
|
1063
|
+
res = node.parent;
|
|
1064
|
+
}
|
|
1065
|
+
// if (node.expanded) {
|
|
1066
|
+
// node.setExpanded(false);
|
|
1067
|
+
// } else if (node.parent && node.parent.parent) {
|
|
1068
|
+
// res = node.parent;
|
|
1069
|
+
// }
|
|
1070
|
+
break;
|
|
1071
|
+
case "right":
|
|
1072
|
+
if (node.children && node.children.length) {
|
|
1073
|
+
res = node.children[0];
|
|
1074
|
+
}
|
|
1075
|
+
// if (this.cellNavMode) {
|
|
1076
|
+
// throw new Error("Not implemented");
|
|
1077
|
+
// } else {
|
|
1078
|
+
// if (!node.expanded && (node.children || node.lazy)) {
|
|
1079
|
+
// node.setExpanded();
|
|
1080
|
+
// res = node;
|
|
1081
|
+
// } else if (node.children && node.children.length) {
|
|
1082
|
+
// res = node.children[0];
|
|
1083
|
+
// }
|
|
1084
|
+
// }
|
|
1085
|
+
break;
|
|
1086
|
+
case "up":
|
|
1087
|
+
res = this._getPrevNodeInView(node);
|
|
1088
|
+
break;
|
|
1089
|
+
case "down":
|
|
1090
|
+
res = this._getNextNodeInView(node);
|
|
1091
|
+
break;
|
|
1092
|
+
case "pageDown":
|
|
1093
|
+
let bottomNode = this._lastNodeInView();
|
|
1094
|
+
// this.logDebug(where, this.focusNode, bottomNode);
|
|
1095
|
+
|
|
1096
|
+
if (this.focusNode !== bottomNode) {
|
|
1097
|
+
res = bottomNode;
|
|
1098
|
+
} else {
|
|
1099
|
+
res = this._getNextNodeInView(node, pageSize);
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
case "pageUp":
|
|
1103
|
+
if (this.focusNode && this.focusNode._rowIdx === 0) {
|
|
1104
|
+
res = this.focusNode;
|
|
1105
|
+
} else {
|
|
1106
|
+
let topNode = this._firstNodeInView();
|
|
1107
|
+
if (this.focusNode !== topNode) {
|
|
1108
|
+
res = topNode;
|
|
1109
|
+
} else {
|
|
1110
|
+
res = this._getPrevNodeInView(node, pageSize);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
break;
|
|
1114
|
+
default:
|
|
1115
|
+
this.logWarn("Unknown relation '" + where + "'.");
|
|
1116
|
+
}
|
|
1117
|
+
return res;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Return the active cell of the currently active node or null.
|
|
1122
|
+
*/
|
|
1123
|
+
getActiveColElem() {
|
|
1124
|
+
if (this.activeNode && this.activeColIdx >= 0) {
|
|
1125
|
+
return this.activeNode.getColElem(this.activeColIdx);
|
|
1126
|
+
}
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Return the currently active node or null.
|
|
1132
|
+
*/
|
|
1133
|
+
getActiveNode() {
|
|
1134
|
+
return this.activeNode;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** Return the first top level node if any (not the invisible root node).
|
|
1138
|
+
* @returns {FancytreeNode | null}
|
|
1139
|
+
*/
|
|
1140
|
+
getFirstChild() {
|
|
1141
|
+
return this.root.getFirstChild();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Return the currently active node or null.
|
|
1146
|
+
*/
|
|
1147
|
+
getFocusNode() {
|
|
1148
|
+
return this.focusNode;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/** Return a {node: FancytreeNode, region: TYPE} object for a mouse event.
|
|
1152
|
+
*
|
|
1153
|
+
* @param {Event} event Mouse event, e.g. click, ...
|
|
1154
|
+
* @returns {object} Return a {node: FancytreeNode, region: TYPE} object
|
|
1155
|
+
* TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
|
|
1156
|
+
*/
|
|
1157
|
+
static getEventInfo(event: Event) {
|
|
1158
|
+
let target = <Element>event.target,
|
|
1159
|
+
cl = target.classList,
|
|
1160
|
+
parentCol = target.closest(".wb-col"),
|
|
1161
|
+
node = Wunderbaum.getNode(target),
|
|
1162
|
+
res = {
|
|
1163
|
+
node: node,
|
|
1164
|
+
region: NodeRegion.unknown,
|
|
1165
|
+
colDef: undefined,
|
|
1166
|
+
colIdx: -1,
|
|
1167
|
+
colId: undefined,
|
|
1168
|
+
colElem: parentCol,
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
if (cl.contains("wb-title")) {
|
|
1172
|
+
res.region = NodeRegion.title;
|
|
1173
|
+
} else if (cl.contains("wb-expander")) {
|
|
1174
|
+
res.region =
|
|
1175
|
+
node!.hasChildren() === false ? NodeRegion.prefix : NodeRegion.expander;
|
|
1176
|
+
} else if (cl.contains("wb-checkbox")) {
|
|
1177
|
+
res.region = NodeRegion.checkbox;
|
|
1178
|
+
} else if (cl.contains("wb-icon")) {
|
|
1179
|
+
//|| cl.contains("wb-custom-icon")) {
|
|
1180
|
+
res.region = NodeRegion.icon;
|
|
1181
|
+
} else if (cl.contains("wb-node")) {
|
|
1182
|
+
res.region = NodeRegion.title;
|
|
1183
|
+
} else if (parentCol) {
|
|
1184
|
+
res.region = NodeRegion.column;
|
|
1185
|
+
const idx = Array.prototype.indexOf.call(
|
|
1186
|
+
parentCol.parentNode!.children,
|
|
1187
|
+
parentCol
|
|
1188
|
+
);
|
|
1189
|
+
res.colIdx = idx;
|
|
1190
|
+
} else {
|
|
1191
|
+
// Somewhere near the title
|
|
1192
|
+
console.warn("getEventInfo(): not found", event, res);
|
|
1193
|
+
return res;
|
|
1194
|
+
}
|
|
1195
|
+
if (res.colIdx === -1) {
|
|
1196
|
+
res.colIdx = 0;
|
|
1197
|
+
}
|
|
1198
|
+
res.colDef = node!.tree.columns[res.colIdx];
|
|
1199
|
+
res.colDef != null ? (res.colId = (<any>res.colDef).id) : 0;
|
|
1200
|
+
// this.log("Event", event, res);
|
|
1201
|
+
return res;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// /** Return a string describing the affected node region for a mouse event.
|
|
1205
|
+
// *
|
|
1206
|
+
// * @param {Event} event Mouse event, e.g. click, mousemove, ...
|
|
1207
|
+
// * @returns {string} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
|
|
1208
|
+
// */
|
|
1209
|
+
// getEventNodeRegion(event: Event) {
|
|
1210
|
+
// return this.getEventInfo(event).region;
|
|
1211
|
+
// }
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Return readable string representation for this instance.
|
|
1215
|
+
* @internal
|
|
1216
|
+
*/
|
|
1217
|
+
toString() {
|
|
1218
|
+
return "Wunderbaum<'" + this.id + "'>";
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/** Return true if any node is currently in edit-title mode. */
|
|
1222
|
+
isEditing(): boolean {
|
|
1223
|
+
return this._callMethod("edit.isEditingTitle");
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
|
|
1227
|
+
*/
|
|
1228
|
+
isLoading(): boolean {
|
|
1229
|
+
var res = false;
|
|
1230
|
+
|
|
1231
|
+
this.root.visit((n) => {
|
|
1232
|
+
// also visit rootNode
|
|
1233
|
+
if (n._isLoading || n._requestId) {
|
|
1234
|
+
res = true;
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
}, true);
|
|
1238
|
+
return res;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** Alias for `logDebug` */
|
|
1242
|
+
log = this.logDebug; // Alias
|
|
1243
|
+
|
|
1244
|
+
/** Log to console if opts.debugLevel >= 4 */
|
|
1245
|
+
logDebug(...args: any[]) {
|
|
1246
|
+
if (this.options.debugLevel >= 4) {
|
|
1247
|
+
Array.prototype.unshift.call(args, this.toString());
|
|
1248
|
+
console.log.apply(console, args);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/** Log error to console. */
|
|
1253
|
+
logError(...args: any[]) {
|
|
1254
|
+
if (this.options.debugLevel >= 1) {
|
|
1255
|
+
Array.prototype.unshift.call(args, this.toString());
|
|
1256
|
+
console.error.apply(console, args);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/* Log to console if opts.debugLevel >= 3 */
|
|
1261
|
+
logInfo(...args: any[]) {
|
|
1262
|
+
if (this.options.debugLevel >= 3) {
|
|
1263
|
+
Array.prototype.unshift.call(args, this.toString());
|
|
1264
|
+
console.info.apply(console, args);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/** @internal */
|
|
1269
|
+
logTime(label: string): string {
|
|
1270
|
+
if (this.options.debugLevel >= 4) {
|
|
1271
|
+
console.time(this + ": " + label);
|
|
1272
|
+
}
|
|
1273
|
+
return label;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/** @internal */
|
|
1277
|
+
logTimeEnd(label: string): void {
|
|
1278
|
+
if (this.options.debugLevel >= 4) {
|
|
1279
|
+
console.timeEnd(this + ": " + label);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** Log to console if opts.debugLevel >= 2 */
|
|
1284
|
+
logWarn(...args: any[]) {
|
|
1285
|
+
if (this.options.debugLevel >= 2) {
|
|
1286
|
+
Array.prototype.unshift.call(args, this.toString());
|
|
1287
|
+
console.warn.apply(console, args);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** */
|
|
1292
|
+
protected render(opts?: any): boolean {
|
|
1293
|
+
const label = this.logTime("render");
|
|
1294
|
+
let idx = 0;
|
|
1295
|
+
let top = 0;
|
|
1296
|
+
const height = ROW_HEIGHT;
|
|
1297
|
+
let modified = false;
|
|
1298
|
+
let start = opts?.startIdx;
|
|
1299
|
+
let end = opts?.endIdx;
|
|
1300
|
+
const obsoleteViewNodes = this.viewNodes;
|
|
1301
|
+
|
|
1302
|
+
this.viewNodes = new Set();
|
|
1303
|
+
let viewNodes = this.viewNodes;
|
|
1304
|
+
// this.debug("render", opts);
|
|
1305
|
+
util.assert(start != null && end != null);
|
|
1306
|
+
|
|
1307
|
+
// Make sure start is always even, so the alternating row colors don't
|
|
1308
|
+
// change when scrolling:
|
|
1309
|
+
if (start % 2) {
|
|
1310
|
+
start--;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
this.visitRows(function (node) {
|
|
1314
|
+
const prevIdx = node._rowIdx;
|
|
1315
|
+
|
|
1316
|
+
viewNodes.add(node);
|
|
1317
|
+
obsoleteViewNodes.delete(node);
|
|
1318
|
+
if (prevIdx !== idx) {
|
|
1319
|
+
node._rowIdx = idx;
|
|
1320
|
+
modified = true;
|
|
1321
|
+
}
|
|
1322
|
+
if (idx < start || idx > end) {
|
|
1323
|
+
node._callEvent("discard");
|
|
1324
|
+
node.removeMarkup();
|
|
1325
|
+
} else {
|
|
1326
|
+
// if (!node._rowElem || prevIdx != idx) {
|
|
1327
|
+
node.render({ top: top });
|
|
1328
|
+
}
|
|
1329
|
+
idx++;
|
|
1330
|
+
top += height;
|
|
1331
|
+
});
|
|
1332
|
+
for (const prevNode of obsoleteViewNodes) {
|
|
1333
|
+
prevNode._callEvent("discard");
|
|
1334
|
+
prevNode.removeMarkup();
|
|
1335
|
+
}
|
|
1336
|
+
// Resize tree container
|
|
1337
|
+
this.nodeListElement.style.height = "" + top + "px";
|
|
1338
|
+
// this.log("render()", this.nodeListElement.style.height);
|
|
1339
|
+
this.logTimeEnd(label);
|
|
1340
|
+
return modified;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**Recalc and apply header columns from `this.columns`. */
|
|
1344
|
+
renderHeader() {
|
|
1345
|
+
if (!this.headerElement) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const headerRow = this.headerElement.querySelector(".wb-row")!;
|
|
1349
|
+
util.assert(headerRow);
|
|
1350
|
+
headerRow.innerHTML = "<span class='wb-col'></span>".repeat(
|
|
1351
|
+
this.columns.length
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
for (let i = 0; i < this.columns.length; i++) {
|
|
1355
|
+
let col = this.columns[i];
|
|
1356
|
+
let colElem = <HTMLElement>headerRow.children[i];
|
|
1357
|
+
colElem.style.left = col._ofsPx + "px";
|
|
1358
|
+
colElem.style.width = col._widthPx + "px";
|
|
1359
|
+
colElem.textContent = col.title || col.id;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
*
|
|
1365
|
+
* @param {boolean | PlainObject} [effects=false] animation options.
|
|
1366
|
+
* @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
|
|
1367
|
+
* this node will remain visible in
|
|
1368
|
+
* any case, even if `this` is outside the scroll pane.
|
|
1369
|
+
* Make sure that a node is scrolled into the viewport.
|
|
1370
|
+
*/
|
|
1371
|
+
scrollTo(opts: any) {
|
|
1372
|
+
const MARGIN = 1;
|
|
1373
|
+
const node = opts.node || this.getActiveNode();
|
|
1374
|
+
util.assert(node._rowIdx != null);
|
|
1375
|
+
const curTop = this.scrollContainer.scrollTop;
|
|
1376
|
+
const height = this.scrollContainer.clientHeight;
|
|
1377
|
+
const nodeOfs = node._rowIdx * ROW_HEIGHT;
|
|
1378
|
+
let newTop;
|
|
1379
|
+
|
|
1380
|
+
if (nodeOfs > curTop) {
|
|
1381
|
+
if (nodeOfs + ROW_HEIGHT < curTop + height) {
|
|
1382
|
+
// Already in view
|
|
1383
|
+
} else {
|
|
1384
|
+
// Node is below viewport
|
|
1385
|
+
newTop = nodeOfs - height + ROW_HEIGHT - MARGIN;
|
|
1386
|
+
}
|
|
1387
|
+
} else if (nodeOfs < curTop) {
|
|
1388
|
+
// Node is above viewport
|
|
1389
|
+
newTop = nodeOfs + MARGIN;
|
|
1390
|
+
}
|
|
1391
|
+
this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
|
|
1392
|
+
if (newTop != null) {
|
|
1393
|
+
this.scrollContainer.scrollTop = newTop;
|
|
1394
|
+
this.updateViewport();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/** */
|
|
1399
|
+
setCellMode(mode: NavigationMode) {
|
|
1400
|
+
// util.assert(this.cellNavMode);
|
|
1401
|
+
// util.assert(0 <= colIdx && colIdx < this.columns.length);
|
|
1402
|
+
if (mode === this.navMode) {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const prevMode = this.navMode;
|
|
1406
|
+
const cellMode = mode !== NavigationMode.row;
|
|
1407
|
+
|
|
1408
|
+
this.navMode = mode;
|
|
1409
|
+
if (cellMode && prevMode === NavigationMode.row) {
|
|
1410
|
+
this.setColumn(0);
|
|
1411
|
+
}
|
|
1412
|
+
this.element.classList.toggle("wb-cell-mode", cellMode);
|
|
1413
|
+
this.element.classList.toggle(
|
|
1414
|
+
"wb-cell-edit-mode",
|
|
1415
|
+
mode === NavigationMode.cellEdit
|
|
1416
|
+
);
|
|
1417
|
+
this.setModified(ChangeType.row, this.activeNode);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/** */
|
|
1421
|
+
setColumn(colIdx: number) {
|
|
1422
|
+
util.assert(this.navMode !== NavigationMode.row);
|
|
1423
|
+
util.assert(0 <= colIdx && colIdx < this.columns.length);
|
|
1424
|
+
this.activeColIdx = colIdx;
|
|
1425
|
+
// node.setActive(true, { column: tree.activeColIdx + 1 });
|
|
1426
|
+
this.setModified(ChangeType.row, this.activeNode);
|
|
1427
|
+
// Update `wb-active` class for all headers
|
|
1428
|
+
if (this.headerElement) {
|
|
1429
|
+
for (let rowDiv of this.headerElement.children) {
|
|
1430
|
+
// for (let rowDiv of document.querySelector("div.wb-header").children) {
|
|
1431
|
+
let i = 0;
|
|
1432
|
+
for (let colDiv of rowDiv.children) {
|
|
1433
|
+
(colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Update `wb-active` class for all cell divs
|
|
1438
|
+
for (let rowDiv of this.nodeListElement.children) {
|
|
1439
|
+
let i = 0;
|
|
1440
|
+
for (let colDiv of rowDiv.children) {
|
|
1441
|
+
(colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** */
|
|
1447
|
+
setFocus(flag = true) {
|
|
1448
|
+
if (flag) {
|
|
1449
|
+
this.element.focus();
|
|
1450
|
+
} else {
|
|
1451
|
+
this.element.blur();
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/** */
|
|
1456
|
+
setModified(change: ChangeType, options?: any): void;
|
|
1457
|
+
|
|
1458
|
+
/** */
|
|
1459
|
+
setModified(
|
|
1460
|
+
change: ChangeType,
|
|
1461
|
+
node?: WunderbaumNode | any,
|
|
1462
|
+
options?: any
|
|
1463
|
+
): void {
|
|
1464
|
+
if (!(node instanceof WunderbaumNode)) {
|
|
1465
|
+
options = node;
|
|
1466
|
+
}
|
|
1467
|
+
if (!this.changedSince) {
|
|
1468
|
+
this.changedSince = Date.now();
|
|
1469
|
+
}
|
|
1470
|
+
this.changes.add(change);
|
|
1471
|
+
if (change === ChangeType.structure) {
|
|
1472
|
+
this.changedNodes.clear();
|
|
1473
|
+
} else if (node && !this.changes.has(ChangeType.structure)) {
|
|
1474
|
+
if (this.changedNodes.size < MAX_CHANGED_NODES) {
|
|
1475
|
+
this.changedNodes.add(node);
|
|
1476
|
+
} else {
|
|
1477
|
+
this.changes.add(ChangeType.structure);
|
|
1478
|
+
this.changedNodes.clear();
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
// this.log("setModified(" + change + ")", node);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
setStatus(
|
|
1485
|
+
status: NodeStatusType,
|
|
1486
|
+
message?: string,
|
|
1487
|
+
details?: string
|
|
1488
|
+
): WunderbaumNode | null {
|
|
1489
|
+
return this.root.setStatus(status, message, details);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/** Update column headers and width. */
|
|
1493
|
+
updateColumns(opts: any) {
|
|
1494
|
+
let modified = false;
|
|
1495
|
+
let minWidth = 4;
|
|
1496
|
+
let vpWidth = this.element.clientWidth;
|
|
1497
|
+
let totalWeight = 0;
|
|
1498
|
+
let fixedWidth = 0;
|
|
1499
|
+
|
|
1500
|
+
// Gather width requests
|
|
1501
|
+
this._columnsById = {};
|
|
1502
|
+
for (let col of this.columns) {
|
|
1503
|
+
this._columnsById[<string>col.id] = col;
|
|
1504
|
+
let cw = col.width;
|
|
1505
|
+
|
|
1506
|
+
if (!cw || cw === "*") {
|
|
1507
|
+
col._weight = 1.0;
|
|
1508
|
+
totalWeight += 1.0;
|
|
1509
|
+
} else if (typeof cw === "number") {
|
|
1510
|
+
col._weight = cw;
|
|
1511
|
+
totalWeight += cw;
|
|
1512
|
+
} else if (typeof cw === "string" && cw.endsWith("px")) {
|
|
1513
|
+
col._weight = 0;
|
|
1514
|
+
let px = parseFloat(cw.slice(0, -2));
|
|
1515
|
+
if (col._widthPx != px) {
|
|
1516
|
+
modified = true;
|
|
1517
|
+
col._widthPx = px;
|
|
1518
|
+
}
|
|
1519
|
+
fixedWidth += px;
|
|
1520
|
+
} else {
|
|
1521
|
+
util.error("Invalid column width: " + cw);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
// Share remaining space between non-fixed columns
|
|
1525
|
+
let restPx = Math.max(0, vpWidth - fixedWidth);
|
|
1526
|
+
let ofsPx = 0;
|
|
1527
|
+
|
|
1528
|
+
for (let col of this.columns) {
|
|
1529
|
+
if (col._weight) {
|
|
1530
|
+
let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
|
|
1531
|
+
if (col._widthPx != px) {
|
|
1532
|
+
modified = true;
|
|
1533
|
+
col._widthPx = px;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
col._ofsPx = ofsPx;
|
|
1537
|
+
ofsPx += col._widthPx;
|
|
1538
|
+
}
|
|
1539
|
+
// Every column has now a calculated `_ofsPx` and `_widthPx`
|
|
1540
|
+
// this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
|
|
1541
|
+
// console.trace();
|
|
1542
|
+
// util.error("BREAK");
|
|
1543
|
+
if (modified) {
|
|
1544
|
+
this.renderHeader();
|
|
1545
|
+
if (opts.render !== false) {
|
|
1546
|
+
this.render();
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/** Render all rows that are visible in the viewport. */
|
|
1552
|
+
updateViewport(immediate = false) {
|
|
1553
|
+
// Call the `throttle` wrapper for `this._updateViewport()` which will
|
|
1554
|
+
// execute immediately on the leading edge of a sequence:
|
|
1555
|
+
this._updateViewportThrottled();
|
|
1556
|
+
if (immediate) {
|
|
1557
|
+
this._updateViewportThrottled.flush();
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
protected _updateViewport() {
|
|
1562
|
+
if (this._disableUpdate) {
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
let height = this.scrollContainer.clientHeight;
|
|
1566
|
+
// We cannot get the height for abolut positioned parent, so look at first col
|
|
1567
|
+
// let headerHeight = this.headerElement.clientHeight
|
|
1568
|
+
// let headerHeight = this.headerElement.children[0].children[0].clientHeight;
|
|
1569
|
+
const headerHeight = this.options.headerHeightPx;
|
|
1570
|
+
let wantHeight = this.element.clientHeight - headerHeight;
|
|
1571
|
+
let ofs = this.scrollContainer.scrollTop;
|
|
1572
|
+
|
|
1573
|
+
if (Math.abs(height - wantHeight) > 1.0) {
|
|
1574
|
+
// this.log("resize", height, wantHeight);
|
|
1575
|
+
this.scrollContainer.style.height = wantHeight + "px";
|
|
1576
|
+
height = wantHeight;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
this.updateColumns({ render: false });
|
|
1580
|
+
this.render({
|
|
1581
|
+
startIdx: Math.max(0, ofs / ROW_HEIGHT - RENDER_MAX_PREFETCH),
|
|
1582
|
+
endIdx: Math.max(0, (ofs + height) / ROW_HEIGHT + RENDER_MAX_PREFETCH),
|
|
1583
|
+
});
|
|
1584
|
+
this._callEvent("update");
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/** Call callback(node) for all nodes in hierarchical order (depth-first).
|
|
1588
|
+
*
|
|
1589
|
+
* @param {function} callback the callback function.
|
|
1590
|
+
* Return false to stop iteration, return "skip" to skip this node and children only.
|
|
1591
|
+
* @returns {boolean} false, if the iterator was stopped.
|
|
1592
|
+
*/
|
|
1593
|
+
visit(callback: (node: WunderbaumNode) => any) {
|
|
1594
|
+
return this.root.visit(callback, false);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
|
|
1598
|
+
* Stop iteration, if fn() returns false.<br>
|
|
1599
|
+
* Return false if iteration was stopped.
|
|
1600
|
+
*
|
|
1601
|
+
* @param callback the callback function.
|
|
1602
|
+
* Return false to stop iteration, return "skip" to skip this node and children only.
|
|
1603
|
+
* @param [options]
|
|
1604
|
+
* Defaults:
|
|
1605
|
+
* {start: First tree node, reverse: false, includeSelf: true, includeHidden: false, wrap: false}
|
|
1606
|
+
* @returns {boolean} false if iteration was canceled
|
|
1607
|
+
*/
|
|
1608
|
+
visitRows(callback: (node: WunderbaumNode) => any, opts?: any): boolean {
|
|
1609
|
+
if (!this.root.hasChildren()) {
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
if (opts && opts.reverse) {
|
|
1613
|
+
delete opts.reverse;
|
|
1614
|
+
return this._visitRowsUp(callback, opts);
|
|
1615
|
+
}
|
|
1616
|
+
opts = opts || {};
|
|
1617
|
+
let i,
|
|
1618
|
+
nextIdx,
|
|
1619
|
+
parent,
|
|
1620
|
+
res,
|
|
1621
|
+
siblings,
|
|
1622
|
+
stopNode: WunderbaumNode,
|
|
1623
|
+
siblingOfs = 0,
|
|
1624
|
+
skipFirstNode = opts.includeSelf === false,
|
|
1625
|
+
includeHidden = !!opts.includeHidden,
|
|
1626
|
+
checkFilter = !includeHidden && this.filterMode === "hide",
|
|
1627
|
+
node: WunderbaumNode = opts.start || this.root.children![0];
|
|
1628
|
+
|
|
1629
|
+
parent = node.parent;
|
|
1630
|
+
while (parent) {
|
|
1631
|
+
// visit siblings
|
|
1632
|
+
siblings = parent.children!;
|
|
1633
|
+
nextIdx = siblings.indexOf(node) + siblingOfs;
|
|
1634
|
+
util.assert(
|
|
1635
|
+
nextIdx >= 0,
|
|
1636
|
+
"Could not find " + node + " in parent's children: " + parent
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1639
|
+
for (i = nextIdx; i < siblings.length; i++) {
|
|
1640
|
+
node = siblings[i];
|
|
1641
|
+
if (node === stopNode!) {
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
if (
|
|
1645
|
+
checkFilter &&
|
|
1646
|
+
!node.statusNodeType &&
|
|
1647
|
+
!node.match &&
|
|
1648
|
+
!node.subMatchCount
|
|
1649
|
+
) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
if (!skipFirstNode && callback(node) === false) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
skipFirstNode = false;
|
|
1656
|
+
// Dive into node's child nodes
|
|
1657
|
+
if (
|
|
1658
|
+
node.children &&
|
|
1659
|
+
node.children.length &&
|
|
1660
|
+
(includeHidden || node.expanded)
|
|
1661
|
+
) {
|
|
1662
|
+
res = node.visit(function (n: WunderbaumNode) {
|
|
1663
|
+
if (n === stopNode) {
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
if (checkFilter && !n.match && !n.subMatchCount) {
|
|
1667
|
+
return "skip";
|
|
1668
|
+
}
|
|
1669
|
+
if (callback(n) === false) {
|
|
1670
|
+
return false;
|
|
1671
|
+
}
|
|
1672
|
+
if (!includeHidden && n.children && !n.expanded) {
|
|
1673
|
+
return "skip";
|
|
1674
|
+
}
|
|
1675
|
+
}, false);
|
|
1676
|
+
if (res === false) {
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
// Visit parent nodes (bottom up)
|
|
1682
|
+
node = parent;
|
|
1683
|
+
parent = parent.parent;
|
|
1684
|
+
siblingOfs = 1; //
|
|
1685
|
+
|
|
1686
|
+
if (!parent && opts.wrap) {
|
|
1687
|
+
this.logDebug("visitRows(): wrap around");
|
|
1688
|
+
util.assert(opts.start, "`wrap` option requires `start`");
|
|
1689
|
+
stopNode = opts.start;
|
|
1690
|
+
opts.wrap = false;
|
|
1691
|
+
parent = this.root;
|
|
1692
|
+
siblingOfs = 0;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/** Call fn(node) for all nodes in vertical order, bottom up.
|
|
1699
|
+
* @internal
|
|
1700
|
+
*/
|
|
1701
|
+
protected _visitRowsUp(
|
|
1702
|
+
callback: (node: WunderbaumNode) => any,
|
|
1703
|
+
opts: any
|
|
1704
|
+
): boolean {
|
|
1705
|
+
let children,
|
|
1706
|
+
idx,
|
|
1707
|
+
parent,
|
|
1708
|
+
includeHidden = !!opts.includeHidden,
|
|
1709
|
+
node = opts.start || this.root.children![0];
|
|
1710
|
+
|
|
1711
|
+
if (opts.includeSelf !== false) {
|
|
1712
|
+
if (callback(node) === false) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
while (true) {
|
|
1717
|
+
parent = node.parent;
|
|
1718
|
+
children = parent.children;
|
|
1719
|
+
|
|
1720
|
+
if (children[0] === node) {
|
|
1721
|
+
// If this is already the first sibling, goto parent
|
|
1722
|
+
node = parent;
|
|
1723
|
+
if (!node.parent) {
|
|
1724
|
+
break; // first node of the tree
|
|
1725
|
+
}
|
|
1726
|
+
children = parent.children;
|
|
1727
|
+
} else {
|
|
1728
|
+
// Otherwise, goto prev. sibling
|
|
1729
|
+
idx = children.indexOf(node);
|
|
1730
|
+
node = children[idx - 1];
|
|
1731
|
+
// If the prev. sibling has children, follow down to last descendant
|
|
1732
|
+
while (
|
|
1733
|
+
(includeHidden || node.expanded) &&
|
|
1734
|
+
node.children &&
|
|
1735
|
+
node.children.length
|
|
1736
|
+
) {
|
|
1737
|
+
children = node.children;
|
|
1738
|
+
parent = node;
|
|
1739
|
+
node = children[children.length - 1];
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
// Skip invisible
|
|
1743
|
+
if (!includeHidden && !node.isVisible()) {
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
if (callback(node) === false) {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/** . */
|
|
1754
|
+
load(source: any, options: any = {}) {
|
|
1755
|
+
this.clear();
|
|
1756
|
+
const columns = options.columns || source.columns;
|
|
1757
|
+
if (columns) {
|
|
1758
|
+
this.columns = options.columns;
|
|
1759
|
+
this.renderHeader();
|
|
1760
|
+
// this.updateColumns({ render: false });
|
|
1761
|
+
}
|
|
1762
|
+
return this.root.load(source);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
*
|
|
1767
|
+
*/
|
|
1768
|
+
public enableUpdate(flag: boolean): void {
|
|
1769
|
+
/*
|
|
1770
|
+
5 7 9 20 25 30
|
|
1771
|
+
1 >-------------------------------------<
|
|
1772
|
+
2 >--------------------<
|
|
1773
|
+
3 >--------------------------<
|
|
1774
|
+
|
|
1775
|
+
5
|
|
1776
|
+
|
|
1777
|
+
*/
|
|
1778
|
+
// this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
|
|
1779
|
+
if (flag) {
|
|
1780
|
+
util.assert(this._disableUpdateCount > 0);
|
|
1781
|
+
this._disableUpdateCount--;
|
|
1782
|
+
if (this._disableUpdateCount === 0) {
|
|
1783
|
+
this.updateViewport();
|
|
1784
|
+
}
|
|
1785
|
+
} else {
|
|
1786
|
+
this._disableUpdateCount++;
|
|
1787
|
+
// this._disableUpdate = Date.now();
|
|
1788
|
+
}
|
|
1789
|
+
// return !flag; // return previous value
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/* ---------------------------------------------------------------------------
|
|
1793
|
+
* FILTER
|
|
1794
|
+
* -------------------------------------------------------------------------*/
|
|
1795
|
+
/**
|
|
1796
|
+
* [ext-filter] Reset the filter.
|
|
1797
|
+
*
|
|
1798
|
+
* @requires [[FilterExtension]]
|
|
1799
|
+
*/
|
|
1800
|
+
clearFilter() {
|
|
1801
|
+
return (this.extensions.filter as FilterExtension).clearFilter();
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* [ext-filter] Return true if a filter is currently applied.
|
|
1805
|
+
*
|
|
1806
|
+
* @requires [[FilterExtension]]
|
|
1807
|
+
*/
|
|
1808
|
+
isFilterActive() {
|
|
1809
|
+
return !!this.filterMode;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* [ext-filter] Re-apply current filter.
|
|
1813
|
+
*
|
|
1814
|
+
* @requires [[FilterExtension]]
|
|
1815
|
+
*/
|
|
1816
|
+
updateFilter() {
|
|
1817
|
+
return (this.extensions.filter as FilterExtension).updateFilter();
|
|
1818
|
+
}
|
|
1819
|
+
}
|