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.
@@ -0,0 +1,370 @@
1
+ /*!
2
+ * Wunderbaum - ext-filter
3
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
+ */
6
+
7
+ import {
8
+ elemFromSelector,
9
+ escapeHtml,
10
+ escapeRegex,
11
+ extend,
12
+ extractHtmlText,
13
+ onEvent,
14
+ } from "./util";
15
+ import { NodeFilterCallback, NodeStatusType } from "./common";
16
+ import { Wunderbaum } from "./wunderbaum";
17
+ import { WunderbaumNode } from "./wb_node";
18
+ import { WunderbaumExtension } from "./wb_extension_base";
19
+ import { debounce } from "./debounce";
20
+
21
+ const START_MARKER = "\uFFF7";
22
+ const END_MARKER = "\uFFF8";
23
+ const RE_START_MARKER = new RegExp(escapeRegex(START_MARKER), "g");
24
+ const RE_END_MARTKER = new RegExp(escapeRegex(END_MARKER), "g");
25
+
26
+ export class FilterExtension extends WunderbaumExtension {
27
+ public queryInput?: HTMLInputElement;
28
+ public lastFilterArgs: IArguments | null = null;
29
+
30
+ constructor(tree: Wunderbaum) {
31
+ super(tree, "filter", {
32
+ autoApply: true, // Re-apply last filter if lazy data is loaded
33
+ autoExpand: false, // Expand all branches that contain matches while filtered
34
+ counter: true, // Show a badge with number of matching child nodes near parent icons
35
+ fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
36
+ hideExpandedCounter: true, // Hide counter badge if parent is expanded
37
+ hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
38
+ highlight: true, // Highlight matches by wrapping inside <mark> tags
39
+ leavesOnly: false, // Match end nodes only
40
+ mode: "hide", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
41
+ noData: true, // Display a 'no data' status node if result is empty
42
+ });
43
+ }
44
+
45
+ init() {
46
+ super.init();
47
+ let attachInput = this.getPluginOption("attachInput");
48
+ if (attachInput) {
49
+ this.queryInput = elemFromSelector(attachInput) as HTMLInputElement;
50
+ onEvent(
51
+ this.queryInput,
52
+ "input",
53
+ debounce((e) => {
54
+ // this.tree.log("query", e);
55
+ this.filterNodes(this.queryInput!.value.trim(), {});
56
+ }, 700)
57
+ );
58
+ }
59
+ }
60
+
61
+ _applyFilterNoUpdate(
62
+ filter: string | NodeFilterCallback,
63
+ branchMode: boolean,
64
+ _opts: any
65
+ ) {
66
+ return this.tree.runWithoutUpdate(() => {
67
+ return this._applyFilterImpl(filter, branchMode, _opts);
68
+ });
69
+ }
70
+
71
+ _applyFilterImpl(
72
+ filter: string | NodeFilterCallback,
73
+ branchMode: boolean,
74
+ _opts: any
75
+ ) {
76
+ let match,
77
+ temp,
78
+ start = Date.now(),
79
+ count = 0,
80
+ tree = this.tree,
81
+ treeOpts = tree.options,
82
+ escapeTitles = treeOpts.escapeTitles,
83
+ prevAutoCollapse = treeOpts.autoCollapse,
84
+ opts = extend({}, treeOpts.filter, _opts),
85
+ hideMode = opts.mode === "hide",
86
+ leavesOnly = !!opts.leavesOnly && !branchMode;
87
+
88
+ // Default to 'match title substring (case insensitive)'
89
+ if (typeof filter === "string") {
90
+ if (filter === "") {
91
+ tree.logInfo(
92
+ "Passing an empty string as a filter is handled as clearFilter()."
93
+ );
94
+ this.clearFilter();
95
+ return;
96
+ }
97
+ if (opts.fuzzy) {
98
+ // See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905
99
+ // and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed
100
+ // and http://www.dustindiaz.com/autocomplete-fuzzy-matching
101
+ match = filter
102
+ .split("")
103
+ // Escaping the `filter` will not work because,
104
+ // it gets further split into individual characters. So,
105
+ // escape each character after splitting
106
+ .map(escapeRegex)
107
+ .reduce(function (a, b) {
108
+ // create capture groups for parts that comes before
109
+ // the character
110
+ return a + "([^" + b + "]*)" + b;
111
+ }, "");
112
+ } else {
113
+ match = escapeRegex(filter); // make sure a '.' is treated literally
114
+ }
115
+ let re = new RegExp(match, "i");
116
+ let reHighlight = new RegExp(escapeRegex(filter), "gi");
117
+ filter = (node: WunderbaumNode) => {
118
+ if (!node.title) {
119
+ return false;
120
+ }
121
+ let text = escapeTitles ? node.title : extractHtmlText(node.title);
122
+ // `.match` instead of `.test` to get the capture groups
123
+ let res = text.match(re);
124
+
125
+ if (res && opts.highlight) {
126
+ if (escapeTitles) {
127
+ if (opts.fuzzy) {
128
+ temp = _markFuzzyMatchedChars(text, res, escapeTitles);
129
+ } else {
130
+ // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
131
+ // Use some exotic characters to mark matches:
132
+ temp = text.replace(reHighlight, function (s) {
133
+ return START_MARKER + s + END_MARKER;
134
+ });
135
+ }
136
+ // now we can escape the title...
137
+ node.titleWithHighlight = escapeHtml(temp)
138
+ // ... and finally insert the desired `<mark>` tags
139
+ .replace(RE_START_MARKER, "<mark>")
140
+ .replace(RE_END_MARTKER, "</mark>");
141
+ } else {
142
+ if (opts.fuzzy) {
143
+ node.titleWithHighlight = _markFuzzyMatchedChars(text, res);
144
+ } else {
145
+ node.titleWithHighlight = text.replace(reHighlight, function (s) {
146
+ return "<mark>" + s + "</mark>";
147
+ });
148
+ }
149
+ }
150
+ // node.debug("filter", escapeTitles, text, node.titleWithHighlight);
151
+ }
152
+ return !!res;
153
+ };
154
+ }
155
+
156
+ tree.filterMode = opts.mode;
157
+ this.lastFilterArgs = arguments;
158
+
159
+ tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
160
+ tree.element.classList.toggle("wb-ext-filter-dim", !hideMode);
161
+ tree.element.classList.toggle(
162
+ "wb-ext-filter-hide-expanders",
163
+ !!opts.hideExpanders
164
+ );
165
+ // Reset current filter
166
+ tree.root.subMatchCount = 0;
167
+ tree.visit((node) => {
168
+ delete node.match;
169
+ delete node.titleWithHighlight;
170
+ node.subMatchCount = 0;
171
+ });
172
+ // statusNode = tree.root.findDirectChild(KEY_NODATA);
173
+ // if (statusNode) {
174
+ // statusNode.remove();
175
+ // }
176
+ tree.setStatus(NodeStatusType.ok);
177
+
178
+ // Adjust node.hide, .match, and .subMatchCount properties
179
+ treeOpts.autoCollapse = false; // #528
180
+
181
+ tree.visit((node) => {
182
+ if (leavesOnly && node.children != null) {
183
+ return;
184
+ }
185
+ let res = (<NodeFilterCallback>filter)(node);
186
+
187
+ if (res === "skip") {
188
+ node.visit(function (c) {
189
+ c.match = false;
190
+ }, true);
191
+ return "skip";
192
+ }
193
+
194
+ let matchedByBranch = false;
195
+ if ((branchMode || res === "branch") && node.parent.match) {
196
+ res = true;
197
+ matchedByBranch = true;
198
+ }
199
+
200
+ if (res) {
201
+ count++;
202
+ node.match = true;
203
+ node.visitParents((p) => {
204
+ if (p !== node) {
205
+ p.subMatchCount! += 1;
206
+ }
207
+ // Expand match (unless this is no real match, but only a node in a matched branch)
208
+ if (opts.autoExpand && !matchedByBranch && !p.expanded) {
209
+ p.setExpanded(true, {
210
+ noAnimation: true,
211
+ noEvents: true,
212
+ scrollIntoView: false,
213
+ });
214
+ p._filterAutoExpanded = true;
215
+ }
216
+ }, true);
217
+ }
218
+ });
219
+ treeOpts.autoCollapse = prevAutoCollapse;
220
+
221
+ if (count === 0 && opts.noData && hideMode) {
222
+ tree.root.setStatus(NodeStatusType.noData);
223
+ }
224
+ // Redraw whole tree
225
+ tree.logInfo(
226
+ `Filter '${match}' found ${count} nodes in ${Date.now() - start} ms.`
227
+ );
228
+ return count;
229
+ }
230
+
231
+ /**
232
+ * [ext-filter] Dim or hide nodes.
233
+ *
234
+ * @param {boolean} [opts={autoExpand: false, leavesOnly: false}]
235
+ */
236
+ filterNodes(filter: string | NodeFilterCallback, opts: any) {
237
+ return this._applyFilterNoUpdate(filter, false, opts);
238
+ }
239
+
240
+ /**
241
+ * [ext-filter] Dim or hide whole branches.
242
+ *
243
+ * @param {boolean} [opts={autoExpand: false}]
244
+ */
245
+ filterBranches(filter: string | NodeFilterCallback, opts: any) {
246
+ return this._applyFilterNoUpdate(filter, true, opts);
247
+ }
248
+
249
+ /**
250
+ * [ext-filter] Re-apply current filter.
251
+ *
252
+ * @requires jquery.fancytree.filter.js
253
+ */
254
+ updateFilter() {
255
+ let tree = this.tree;
256
+ if (
257
+ tree.filterMode &&
258
+ this.lastFilterArgs &&
259
+ tree.options.filter.autoApply
260
+ ) {
261
+ this._applyFilterNoUpdate.apply(this, <any>this.lastFilterArgs);
262
+ } else {
263
+ tree.logWarn("updateFilter(): no filter active.");
264
+ }
265
+ }
266
+
267
+ /**
268
+ * [ext-filter] Reset the filter.
269
+ *
270
+ * @alias Fancytree#clearFilter
271
+ * @requires jquery.fancytree.filter.js
272
+ */
273
+ clearFilter() {
274
+ let tree = this.tree,
275
+ // statusNode = tree.root.findDirectChild(KEY_NODATA),
276
+ escapeTitles = tree.options.escapeTitles;
277
+ // enhanceTitle = tree.options.enhanceTitle,
278
+ tree.enableUpdate(false);
279
+
280
+ // if (statusNode) {
281
+ // statusNode.remove();
282
+ // }
283
+ tree.setStatus(NodeStatusType.ok);
284
+ // we also counted root node's subMatchCount
285
+ delete tree.root.match;
286
+ delete tree.root.subMatchCount;
287
+
288
+ tree.visit((node) => {
289
+ if (node.match && node._rowElem) {
290
+ // #491, #601
291
+ let titleElem = node._rowElem.querySelector("span.wb-title")!;
292
+ if (escapeTitles) {
293
+ titleElem.textContent = node.title;
294
+ } else {
295
+ titleElem.innerHTML = node.title;
296
+ }
297
+ node._callEvent("enhanceTitle", { titleElem: titleElem });
298
+ }
299
+ delete node.match;
300
+ delete node.subMatchCount;
301
+ delete node.titleWithHighlight;
302
+ if (node.subMatchBadge) {
303
+ node.subMatchBadge.remove();
304
+ delete node.subMatchBadge;
305
+ }
306
+ if (node._filterAutoExpanded && node.expanded) {
307
+ node.setExpanded(false, {
308
+ noAnimation: true,
309
+ noEvents: true,
310
+ scrollIntoView: false,
311
+ });
312
+ }
313
+ delete node._filterAutoExpanded;
314
+ });
315
+ tree.filterMode = null;
316
+ this.lastFilterArgs = null;
317
+ tree.element.classList.remove(
318
+ // "wb-ext-filter",
319
+ "wb-ext-filter-dim",
320
+ "wb-ext-filter-hide"
321
+ );
322
+ // tree._callHook("treeStructureChanged", this, "clearFilter");
323
+ // tree.render();
324
+ tree.enableUpdate(true);
325
+ }
326
+ }
327
+
328
+ /**
329
+ * @description Marks the matching charecters of `text` either by `mark` or
330
+ * by exotic*Chars (if `escapeTitles` is `true`) based on `matches`
331
+ * which is an array of matching groups.
332
+ * @param {string} text
333
+ * @param {RegExpMatchArray} matches
334
+ */
335
+ function _markFuzzyMatchedChars(
336
+ text: string,
337
+ matches: RegExpMatchArray,
338
+ escapeTitles = false
339
+ ) {
340
+ let matchingIndices = [];
341
+ // get the indices of matched characters (Iterate through `RegExpMatchArray`)
342
+ for (
343
+ let _matchingArrIdx = 1;
344
+ _matchingArrIdx < matches.length;
345
+ _matchingArrIdx++
346
+ ) {
347
+ let _mIdx: number =
348
+ // get matching char index by cumulatively adding
349
+ // the matched group length
350
+ matches[_matchingArrIdx].length +
351
+ (_matchingArrIdx === 1 ? 0 : 1) +
352
+ (matchingIndices[matchingIndices.length - 1] || 0);
353
+ matchingIndices.push(_mIdx);
354
+ }
355
+ // Map each `text` char to its position and store in `textPoses`.
356
+ let textPoses = text.split("");
357
+ if (escapeTitles) {
358
+ // If escaping the title, then wrap the matchng char within exotic chars
359
+ matchingIndices.forEach(function (v) {
360
+ textPoses[v] = START_MARKER + textPoses[v] + END_MARKER;
361
+ });
362
+ } else {
363
+ // Otherwise, Wrap the matching chars within `mark`.
364
+ matchingIndices.forEach(function (v) {
365
+ textPoses[v] = "<mark>" + textPoses[v] + "</mark>";
366
+ });
367
+ }
368
+ // Join back the modified `textPoses` to create final highlight markup.
369
+ return textPoses.join("");
370
+ }
@@ -0,0 +1,210 @@
1
+ /*!
2
+ * Wunderbaum - ext-keynav
3
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
+ */
6
+
7
+ import { NavigationMode, NavigationModeOption } from "./common";
8
+ import { eventToString } from "./util";
9
+ import { Wunderbaum } from "./wunderbaum";
10
+ import { WunderbaumNode } from "./wb_node";
11
+ import { WunderbaumExtension } from "./wb_extension_base";
12
+
13
+ export class KeynavExtension extends WunderbaumExtension {
14
+ constructor(tree: Wunderbaum) {
15
+ super(tree, "keynav", {});
16
+ }
17
+
18
+ onKeyEvent(data: any): boolean | undefined {
19
+ let event = data.event,
20
+ eventName = eventToString(event),
21
+ focusNode,
22
+ node = data.node as WunderbaumNode,
23
+ tree = this.tree,
24
+ opts = data.options,
25
+ handled = true,
26
+ activate = !event.ctrlKey || opts.autoActivate;
27
+ const navModeOption = opts.navigationMode;
28
+
29
+ tree.logDebug(`onKeyEvent: ${eventName}`);
30
+
31
+ // Let callback prevent default processing
32
+ if (tree._callEvent("keydown", data) === false) {
33
+ return false;
34
+ }
35
+
36
+ // Let ext-edit trigger editing
37
+ if (tree._callMethod("edit._preprocessKeyEvent", data) === false) {
38
+ return false;
39
+ }
40
+
41
+ // Set focus to active (or first node) if no other node has the focus yet
42
+ if (!node) {
43
+ const activeNode = tree.getActiveNode();
44
+ const firstNode = tree.getFirstChild();
45
+
46
+ if (!activeNode && firstNode && eventName === "ArrowDown") {
47
+ firstNode.logInfo("Keydown: activate first node.");
48
+ firstNode.setActive();
49
+ return;
50
+ }
51
+
52
+ focusNode = activeNode || firstNode;
53
+ if (focusNode) {
54
+ focusNode.setFocus();
55
+ node = tree.getFocusNode()!;
56
+ node.logInfo("Keydown: force focus on active node.");
57
+ }
58
+ }
59
+
60
+ if (tree.navMode === NavigationMode.row) {
61
+ // --- Quick-Search
62
+ if (
63
+ opts.quicksearch &&
64
+ eventName.length === 1 &&
65
+ /^\w$/.test(eventName)
66
+ // && !$target.is(":input:enabled")
67
+ ) {
68
+ // Allow to search for longer streaks if typed in quickly
69
+ const stamp = Date.now();
70
+ if (stamp - tree.lastQuicksearchTime > 500) {
71
+ tree.lastQuicksearchTerm = "";
72
+ }
73
+ tree.lastQuicksearchTime = stamp;
74
+ tree.lastQuicksearchTerm += eventName;
75
+ let matchNode = tree.findNextNode(
76
+ tree.lastQuicksearchTerm,
77
+ tree.getActiveNode()
78
+ );
79
+ if (matchNode) {
80
+ matchNode.setActive(true, { event: event });
81
+ }
82
+ event.preventDefault();
83
+ return;
84
+ }
85
+
86
+ // Pre-Evaluate expand/collapse action for LEFT/RIGHT
87
+ switch (eventName) {
88
+ case "ArrowLeft":
89
+ if (node.expanded) {
90
+ eventName = "Subtract"; // collapse
91
+ }
92
+ break;
93
+ case "ArrowRight":
94
+ if (!node.expanded && (node.children || node.lazy)) {
95
+ eventName = "Add"; // expand
96
+ } else if (navModeOption === NavigationModeOption.startRow) {
97
+ tree.setCellMode(NavigationMode.cellNav);
98
+ return;
99
+ }
100
+ break;
101
+ }
102
+
103
+ // Standard navigation (row mode)
104
+ switch (eventName) {
105
+ case "+":
106
+ case "Add":
107
+ // case "=": // 187: '+' @ Chrome, Safari
108
+ node.setExpanded(true);
109
+ break;
110
+ case "-":
111
+ case "Subtract":
112
+ node.setExpanded(false);
113
+ break;
114
+ case " ":
115
+ // if (node.isPagingNode()) {
116
+ // tree._triggerNodeEvent("clickPaging", ctx, event);
117
+ // } else
118
+ if (node.getOption("checkbox")) {
119
+ node.setSelected(!node.isSelected());
120
+ } else {
121
+ node.setActive(true, { event: event });
122
+ }
123
+ break;
124
+ case "Enter":
125
+ node.setActive(true, { event: event });
126
+ break;
127
+ case "ArrowDown":
128
+ case "ArrowLeft":
129
+ case "ArrowRight":
130
+ case "ArrowUp":
131
+ case "Backspace":
132
+ case "End":
133
+ case "Home":
134
+ case "Control+End":
135
+ case "Control+Home":
136
+ case "PageDown":
137
+ case "PageUp":
138
+ node.navigate(eventName, { activate: activate, event: event });
139
+ break;
140
+ default:
141
+ handled = false;
142
+ }
143
+ } else {
144
+ // Standard navigation (cell mode)
145
+ switch (eventName) {
146
+ case " ":
147
+ if (tree.activeColIdx === 0 && node.getOption("checkbox")) {
148
+ node.setSelected(!node.isSelected());
149
+ handled = true;
150
+ } else {
151
+ // [Space] key should trigger embedded checkbox
152
+ const elem = tree.getActiveColElem();
153
+ const cb = elem?.querySelector(
154
+ "input[type=checkbox]"
155
+ ) as HTMLInputElement;
156
+ cb?.click();
157
+ }
158
+ break;
159
+ case "Enter":
160
+ if (tree.activeColIdx === 0 && node.isExpandable()) {
161
+ node.setExpanded(!node.isExpanded());
162
+ handled = true;
163
+ }
164
+ break;
165
+ case "Escape":
166
+ if (tree.navMode === NavigationMode.cellEdit) {
167
+ tree.setCellMode(NavigationMode.cellNav);
168
+ handled = true;
169
+ } else if (tree.navMode === NavigationMode.cellNav) {
170
+ tree.setCellMode(NavigationMode.row);
171
+ handled = true;
172
+ }
173
+ break;
174
+ case "ArrowLeft":
175
+ if (tree.activeColIdx > 0) {
176
+ tree.setColumn(tree.activeColIdx - 1);
177
+ handled = true;
178
+ } else if (navModeOption !== NavigationModeOption.cell) {
179
+ tree.setCellMode(NavigationMode.row);
180
+ handled = true;
181
+ }
182
+ break;
183
+ case "ArrowRight":
184
+ if (tree.activeColIdx < tree.columns.length - 1) {
185
+ tree.setColumn(tree.activeColIdx + 1);
186
+ handled = true;
187
+ }
188
+ break;
189
+ case "ArrowDown":
190
+ case "ArrowUp":
191
+ case "Backspace":
192
+ case "End":
193
+ case "Home":
194
+ case "Control+End":
195
+ case "Control+Home":
196
+ case "PageDown":
197
+ case "PageUp":
198
+ node.navigate(eventName, { activate: activate, event: event });
199
+ break;
200
+ default:
201
+ handled = false;
202
+ }
203
+ }
204
+
205
+ if (handled) {
206
+ event.preventDefault();
207
+ }
208
+ return;
209
+ }
210
+ }
@@ -0,0 +1,54 @@
1
+ /*!
2
+ * Wunderbaum - ext-logger
3
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
+ */
6
+
7
+ import { overrideMethod } from "./util";
8
+ import { WunderbaumExtension } from "./wb_extension_base";
9
+ import { Wunderbaum } from "./wunderbaum";
10
+
11
+ export class LoggerExtension extends WunderbaumExtension {
12
+ readonly prefix: string;
13
+ protected ignoreEvents = new Set<string>([
14
+ "enhanceTitle",
15
+ "render",
16
+ "discard",
17
+ ]);
18
+
19
+ constructor(tree: Wunderbaum) {
20
+ super(tree, "logger", {});
21
+ this.prefix = tree + ".ext-logger";
22
+ }
23
+
24
+ init() {
25
+ const tree = this.tree;
26
+
27
+ // this.ignoreEvents.add();
28
+
29
+ if (tree.getOption("debugLevel") >= 4) {
30
+ // const self = this;
31
+ const ignoreEvents = this.ignoreEvents;
32
+ const prefix = this.prefix;
33
+
34
+ overrideMethod(tree, "callEvent", function (name, extra) {
35
+ if (ignoreEvents.has(name)) {
36
+ return (<any>tree)._superApply(arguments);
37
+ }
38
+ let start = Date.now();
39
+ const res = (<any>tree)._superApply(arguments);
40
+ console.debug(
41
+ `${prefix}: callEvent('${name}') took ${Date.now() - start} ms.`,
42
+ arguments[1]
43
+ );
44
+ return res;
45
+ });
46
+ }
47
+ }
48
+
49
+ onKeyEvent(data: any): boolean | undefined {
50
+ // this.tree.logInfo("onKeyEvent", eventToString(data.event), data);
51
+ console.debug(`${this.prefix}: onKeyEvent()`, data);
52
+ return;
53
+ }
54
+ }
@@ -0,0 +1,76 @@
1
+ /*!
2
+ * Wunderbaum - wb_extension_base
3
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
+ */
6
+
7
+ import * as util from "./util";
8
+ import { Wunderbaum } from "./wunderbaum";
9
+
10
+ export type ExtensionsDict = { [key: string]: WunderbaumExtension };
11
+
12
+ export abstract class WunderbaumExtension {
13
+ public enabled = true;
14
+ readonly id: string;
15
+ readonly tree: Wunderbaum;
16
+ readonly treeOpts: any;
17
+ readonly extensionOpts: any;
18
+
19
+ constructor(tree: Wunderbaum, id: string, defaults: any) {
20
+ this.tree = tree;
21
+ this.id = id;
22
+ this.treeOpts = tree.options;
23
+
24
+ const opts = tree.options as any;
25
+
26
+ if (this.treeOpts[id] === undefined) {
27
+ opts[id] = this.extensionOpts = util.extend({}, defaults);
28
+ } else {
29
+ // TODO: do we break existing object instance references here?
30
+ this.extensionOpts = util.extend({}, defaults, opts[id]);
31
+ opts[id] = this.extensionOpts;
32
+ }
33
+ this.enabled = this.getPluginOption("enabled", true);
34
+ }
35
+
36
+ /** Called on tree (re)init after all extensions are added, but before loading.*/
37
+ init() {
38
+ this.tree.element.classList.add("wb-ext-" + this.id);
39
+ }
40
+
41
+ // protected callEvent(name: string, extra?: any): any {
42
+ // let func = this.extensionOpts[name];
43
+ // if (func) {
44
+ // return func.call(
45
+ // this.tree,
46
+ // util.extend(
47
+ // {
48
+ // event: this.id + "." + name,
49
+ // },
50
+ // extra
51
+ // )
52
+ // );
53
+ // }
54
+ // }
55
+
56
+ getPluginOption(name: string, defaultValue?: any): any {
57
+ return this.extensionOpts[name] ?? defaultValue;
58
+ }
59
+
60
+ setPluginOption(name: string, value: any): void {
61
+ this.extensionOpts[name] = value;
62
+ }
63
+
64
+ setEnabled(flag = true) {
65
+ return this.setPluginOption("enabled", !!flag);
66
+ // this.enabled = !!flag;
67
+ }
68
+
69
+ onKeyEvent(data: any): boolean | undefined {
70
+ return;
71
+ }
72
+
73
+ onRender(data: any): boolean | undefined {
74
+ return;
75
+ }
76
+ }