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,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. `"`
|
|
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
|
+
}
|