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
package/src/util.ts
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Wunderbaum - util
|
|
3
|
+
* Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
|
|
4
|
+
* @VERSION, @DATE (https://github.com/mar10/wunderbaum)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Readable names for `MouseEvent.button` */
|
|
8
|
+
export const MOUSE_BUTTONS: { [key: number]: string } = {
|
|
9
|
+
0: "",
|
|
10
|
+
1: "left",
|
|
11
|
+
2: "middle",
|
|
12
|
+
3: "right",
|
|
13
|
+
4: "back",
|
|
14
|
+
5: "forward",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const MAX_INT = 9007199254740991;
|
|
18
|
+
const userInfo = _getUserInfo();
|
|
19
|
+
/**True if the user is using a macOS platform. */
|
|
20
|
+
export const isMac = userInfo.isMac;
|
|
21
|
+
|
|
22
|
+
const REX_HTML = /[&<>"'/]/g; // Escape those characters
|
|
23
|
+
const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips
|
|
24
|
+
const ENTITY_MAP: { [key: string]: string } = {
|
|
25
|
+
"&": "&",
|
|
26
|
+
"<": "<",
|
|
27
|
+
">": ">",
|
|
28
|
+
'"': """,
|
|
29
|
+
"'": "'",
|
|
30
|
+
"/": "/",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type FunctionType = (...args: any[]) => any;
|
|
34
|
+
export type EventCallbackType = (e: Event) => boolean | void;
|
|
35
|
+
type PromiseCallbackType = (val: any) => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A ES6 Promise, that exposes the resolve()/reject() methods.
|
|
39
|
+
*/
|
|
40
|
+
export class Deferred {
|
|
41
|
+
private thens: PromiseCallbackType[] = [];
|
|
42
|
+
private catches: PromiseCallbackType[] = [];
|
|
43
|
+
|
|
44
|
+
private status = "";
|
|
45
|
+
private resolvedValue: any;
|
|
46
|
+
private rejectedError: any;
|
|
47
|
+
|
|
48
|
+
constructor() {}
|
|
49
|
+
|
|
50
|
+
resolve(value?: any) {
|
|
51
|
+
if (this.status) {
|
|
52
|
+
throw new Error("already settled");
|
|
53
|
+
}
|
|
54
|
+
this.status = "resolved";
|
|
55
|
+
this.resolvedValue = value;
|
|
56
|
+
this.thens.forEach((t) => t(value));
|
|
57
|
+
this.thens = []; // Avoid memleaks.
|
|
58
|
+
}
|
|
59
|
+
reject(error?: any) {
|
|
60
|
+
if (this.status) {
|
|
61
|
+
throw new Error("already settled");
|
|
62
|
+
}
|
|
63
|
+
this.status = "rejected";
|
|
64
|
+
this.rejectedError = error;
|
|
65
|
+
this.catches.forEach((c) => c(error));
|
|
66
|
+
this.catches = []; // Avoid memleaks.
|
|
67
|
+
}
|
|
68
|
+
then(cb: any) {
|
|
69
|
+
if (status === "resolved") {
|
|
70
|
+
cb(this.resolvedValue);
|
|
71
|
+
} else {
|
|
72
|
+
this.thens.unshift(cb);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch(cb: any) {
|
|
76
|
+
if (this.status === "rejected") {
|
|
77
|
+
cb(this.rejectedError);
|
|
78
|
+
} else {
|
|
79
|
+
this.catches.unshift(cb);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
promise() {
|
|
83
|
+
return {
|
|
84
|
+
then: this.then,
|
|
85
|
+
catch: this.catch,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**Throw an `Error` if `cond` is falsey. */
|
|
91
|
+
export function assert(cond: any, msg?: string) {
|
|
92
|
+
if (!cond) {
|
|
93
|
+
msg = msg || "Assertion failed.";
|
|
94
|
+
throw new Error(msg);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _getUserInfo() {
|
|
99
|
+
const nav = navigator;
|
|
100
|
+
// const ua = nav.userAgentData;
|
|
101
|
+
const res = {
|
|
102
|
+
isMac: /Mac/.test(nav.platform),
|
|
103
|
+
};
|
|
104
|
+
return res;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Run `callback` when document was loaded. */
|
|
108
|
+
export function documentReady(callback: () => void): void {
|
|
109
|
+
if (document.readyState === "loading") {
|
|
110
|
+
document.addEventListener("DOMContentLoaded", callback);
|
|
111
|
+
} else {
|
|
112
|
+
callback();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Resolve when document was loaded. */
|
|
117
|
+
export function documentReadyPromise(): Promise<void> {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
documentReady(resolve);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Iterate over Object properties or array elements.
|
|
125
|
+
*
|
|
126
|
+
* @param obj `Object`, `Array` or null
|
|
127
|
+
* @param callback(index, item) called for every item.
|
|
128
|
+
* `this` also contains the item.
|
|
129
|
+
* Return `false` to stop the iteration.
|
|
130
|
+
*/
|
|
131
|
+
export function each(
|
|
132
|
+
obj: any,
|
|
133
|
+
callback: (index: number | string, item: any) => void | boolean
|
|
134
|
+
): any {
|
|
135
|
+
if (obj == null) {
|
|
136
|
+
// accept `null` or `undefined`
|
|
137
|
+
return obj;
|
|
138
|
+
}
|
|
139
|
+
let length = obj.length,
|
|
140
|
+
i = 0;
|
|
141
|
+
|
|
142
|
+
if (typeof length === "number") {
|
|
143
|
+
for (; i < length; i++) {
|
|
144
|
+
if (callback.call(obj[i], i, obj[i]) === false) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
for (let k in obj) {
|
|
150
|
+
if (callback.call(obj[i], k, obj[k]) === false) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return obj;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Shortcut for `throw new Error(msg)`.*/
|
|
159
|
+
export function error(msg: string) {
|
|
160
|
+
throw new Error(msg);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. */
|
|
164
|
+
export function escapeHtml(s: string): string {
|
|
165
|
+
return ("" + s).replace(REX_HTML, function (s) {
|
|
166
|
+
return ENTITY_MAP[s];
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// export function escapeRegExp(s: string) {
|
|
171
|
+
// return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
|
172
|
+
// }
|
|
173
|
+
|
|
174
|
+
/**Convert a regular expression string by escaping special characters (e.g. `"$"` -> `"\$"`) */
|
|
175
|
+
export function escapeRegex(s: string) {
|
|
176
|
+
return ("" + s).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Convert `<`, `>`, `"`, `'`, and `/` (but not `&`) to the equivalent entities. */
|
|
180
|
+
export function escapeTooltip(s: string): string {
|
|
181
|
+
return ("" + s).replace(REX_TOOLTIP, function (s) {
|
|
182
|
+
return ENTITY_MAP[s];
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** TODO */
|
|
187
|
+
export function extractHtmlText(s: string) {
|
|
188
|
+
if (s.indexOf(">") >= 0) {
|
|
189
|
+
error("not implemented");
|
|
190
|
+
// return $("<div/>").html(s).text();
|
|
191
|
+
}
|
|
192
|
+
return s;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read the value from an HTML input element.
|
|
197
|
+
*
|
|
198
|
+
* If a `<span class="wb-col">` is passed, the first child input is used.
|
|
199
|
+
* Depending on the target element type, `value` is interpreted accordingly.
|
|
200
|
+
* For example for a checkbox, a value of true, false, or null is returned if the
|
|
201
|
+
* the element is checked, unchecked, or indeterminate.
|
|
202
|
+
* For datetime input control a numerical value is assumed, etc.
|
|
203
|
+
*
|
|
204
|
+
* Common use case: store the new user input in the `change` event:
|
|
205
|
+
*
|
|
206
|
+
* ```ts
|
|
207
|
+
* change: (e) => {
|
|
208
|
+
* // Read the value from the input control that triggered the change event:
|
|
209
|
+
* let value = e.tree.getValueFromElem(e.element);
|
|
210
|
+
* //
|
|
211
|
+
* e.node.data[]
|
|
212
|
+
* },
|
|
213
|
+
* ```
|
|
214
|
+
* @param elem `<input>` or `<select>` element Also a parent `span.wb-col` is accepted.
|
|
215
|
+
* @param coerce pass true to convert date/time inputs to `Date`.
|
|
216
|
+
* @returns the value
|
|
217
|
+
*/
|
|
218
|
+
export function getValueFromElem(elem: HTMLElement, coerce = false): any {
|
|
219
|
+
const tag = elem.tagName;
|
|
220
|
+
let value = null;
|
|
221
|
+
|
|
222
|
+
if (tag === "SPAN" && elem.classList.contains("wb-col")) {
|
|
223
|
+
const span = <HTMLSpanElement>elem;
|
|
224
|
+
const embeddedInput = span.querySelector("input,select");
|
|
225
|
+
|
|
226
|
+
if (embeddedInput) {
|
|
227
|
+
return getValueFromElem(<HTMLElement>embeddedInput, coerce);
|
|
228
|
+
}
|
|
229
|
+
span.innerText = "" + value;
|
|
230
|
+
} else if (tag === "INPUT") {
|
|
231
|
+
const input = <HTMLInputElement>elem;
|
|
232
|
+
const type = input.type;
|
|
233
|
+
|
|
234
|
+
switch (type) {
|
|
235
|
+
case "button":
|
|
236
|
+
case "reset":
|
|
237
|
+
case "submit":
|
|
238
|
+
case "image":
|
|
239
|
+
break;
|
|
240
|
+
case "checkbox":
|
|
241
|
+
value = input.indeterminate ? null : input.checked;
|
|
242
|
+
break;
|
|
243
|
+
case "date":
|
|
244
|
+
case "datetime":
|
|
245
|
+
case "datetime-local":
|
|
246
|
+
case "month":
|
|
247
|
+
case "time":
|
|
248
|
+
case "week":
|
|
249
|
+
value = coerce ? input.valueAsDate : input.value;
|
|
250
|
+
break;
|
|
251
|
+
case "number":
|
|
252
|
+
case "range":
|
|
253
|
+
value = input.valueAsNumber;
|
|
254
|
+
break;
|
|
255
|
+
case "radio":
|
|
256
|
+
const name = input.name;
|
|
257
|
+
const checked = input.parentElement!.querySelector(
|
|
258
|
+
`input[name="${name}"]:checked`
|
|
259
|
+
);
|
|
260
|
+
value = checked ? (<HTMLInputElement>checked).value : undefined;
|
|
261
|
+
break;
|
|
262
|
+
case "text":
|
|
263
|
+
default:
|
|
264
|
+
value = input.value;
|
|
265
|
+
}
|
|
266
|
+
} else if (tag === "SELECT") {
|
|
267
|
+
const select = <HTMLSelectElement>elem;
|
|
268
|
+
value = select.value;
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Set the value of an HTML input element.
|
|
275
|
+
*
|
|
276
|
+
* If a `<span class="wb-col">` is passed, the first child input is used.
|
|
277
|
+
* Depending on the target element type, `value` is interpreted accordingly.
|
|
278
|
+
* For example a checkbox is set to checked, unchecked, or indeterminate if the
|
|
279
|
+
* value is truethy, falsy, or `null`.
|
|
280
|
+
* For datetime input control a numerical value is assumed, etc.
|
|
281
|
+
*
|
|
282
|
+
* @param elem `<input>` or `<select>` element Also a parent `span.wb-col` is accepted.
|
|
283
|
+
* @param value a value that matches the target element.
|
|
284
|
+
*/
|
|
285
|
+
export function setValueToElem(elem: HTMLElement, value: any): void {
|
|
286
|
+
const tag = elem.tagName;
|
|
287
|
+
|
|
288
|
+
if (tag === "SPAN" && elem.classList.contains("wb-col")) {
|
|
289
|
+
const span = <HTMLSpanElement>elem;
|
|
290
|
+
const embeddedInput = span.querySelector("input,select");
|
|
291
|
+
|
|
292
|
+
if (embeddedInput) {
|
|
293
|
+
return setValueToElem(<HTMLElement>embeddedInput, value);
|
|
294
|
+
}
|
|
295
|
+
span.innerText = "" + value;
|
|
296
|
+
} else if (tag === "INPUT") {
|
|
297
|
+
const input = <HTMLInputElement>elem;
|
|
298
|
+
const type = input.type;
|
|
299
|
+
|
|
300
|
+
switch (type) {
|
|
301
|
+
case "checkbox":
|
|
302
|
+
input.indeterminate = value == null;
|
|
303
|
+
input.checked = !!value;
|
|
304
|
+
break;
|
|
305
|
+
case "date":
|
|
306
|
+
case "month":
|
|
307
|
+
case "time":
|
|
308
|
+
case "week":
|
|
309
|
+
case "datetime":
|
|
310
|
+
case "datetime-local":
|
|
311
|
+
input.valueAsDate = value;
|
|
312
|
+
break;
|
|
313
|
+
case "number":
|
|
314
|
+
case "range":
|
|
315
|
+
input.valueAsNumber = value;
|
|
316
|
+
break;
|
|
317
|
+
case "radio":
|
|
318
|
+
assert(false, "not implemented");
|
|
319
|
+
// const name = input.name;
|
|
320
|
+
// const checked = input.parentElement!.querySelector(
|
|
321
|
+
// `input[name="${name}"]:checked`
|
|
322
|
+
// );
|
|
323
|
+
// value = checked ? (<HTMLInputElement>checked).value : undefined;
|
|
324
|
+
break;
|
|
325
|
+
case "button":
|
|
326
|
+
case "reset":
|
|
327
|
+
case "submit":
|
|
328
|
+
case "image":
|
|
329
|
+
break;
|
|
330
|
+
case "text":
|
|
331
|
+
default:
|
|
332
|
+
input.innerText = value;
|
|
333
|
+
}
|
|
334
|
+
} else if (tag === "SELECT") {
|
|
335
|
+
const select = <HTMLSelectElement>elem;
|
|
336
|
+
select.value = value;
|
|
337
|
+
}
|
|
338
|
+
// return value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Return an unconnected `HTMLElement` from a HTML string. */
|
|
342
|
+
export function elemFromHtml(html: string): HTMLElement {
|
|
343
|
+
const t = document.createElement("template");
|
|
344
|
+
t.innerHTML = html.trim();
|
|
345
|
+
return t.content.firstElementChild as HTMLElement;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const _IGNORE_KEYS = new Set(["Alt", "Control", "Meta", "Shift"]);
|
|
349
|
+
|
|
350
|
+
/** Return a HtmlElement from selector or cast an existing element. */
|
|
351
|
+
export function elemFromSelector(obj: string | Element): HTMLElement | null {
|
|
352
|
+
if (!obj) {
|
|
353
|
+
return null; //(null as unknown) as HTMLElement;
|
|
354
|
+
}
|
|
355
|
+
if (typeof obj === "string") {
|
|
356
|
+
return document.querySelector(obj) as HTMLElement;
|
|
357
|
+
}
|
|
358
|
+
return obj as HTMLElement;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Return a descriptive string for a keyboard or mouse event.
|
|
363
|
+
*
|
|
364
|
+
* The result also contains a prefix for modifiers if any, for example
|
|
365
|
+
* `"x"`, `"F2"`, `"Control+Home"`, or `"Shift+clickright"`.
|
|
366
|
+
*/
|
|
367
|
+
export function eventToString(event: Event): string {
|
|
368
|
+
let key = (<KeyboardEvent>event).key,
|
|
369
|
+
et = event.type,
|
|
370
|
+
s = [];
|
|
371
|
+
|
|
372
|
+
if ((<KeyboardEvent>event).altKey) {
|
|
373
|
+
s.push("Alt");
|
|
374
|
+
}
|
|
375
|
+
if ((<KeyboardEvent>event).ctrlKey) {
|
|
376
|
+
s.push("Control");
|
|
377
|
+
}
|
|
378
|
+
if ((<KeyboardEvent>event).metaKey) {
|
|
379
|
+
s.push("Meta");
|
|
380
|
+
}
|
|
381
|
+
if ((<KeyboardEvent>event).shiftKey) {
|
|
382
|
+
s.push("Shift");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (et === "click" || et === "dblclick") {
|
|
386
|
+
s.push(MOUSE_BUTTONS[(<MouseEvent>event).button] + et);
|
|
387
|
+
} else if (et === "wheel") {
|
|
388
|
+
s.push(et);
|
|
389
|
+
// } else if (!IGNORE_KEYCODES[key]) {
|
|
390
|
+
// s.push(
|
|
391
|
+
// SPECIAL_KEYCODES[key] ||
|
|
392
|
+
// String.fromCharCode(key).toLowerCase()
|
|
393
|
+
// );
|
|
394
|
+
} else if (!_IGNORE_KEYS.has(key)) {
|
|
395
|
+
s.push(key);
|
|
396
|
+
}
|
|
397
|
+
return s.join("+");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function extend(...args: any[]) {
|
|
401
|
+
for (let i = 1; i < args.length; i++) {
|
|
402
|
+
let arg = args[i];
|
|
403
|
+
if (arg == null) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
for (let key in arg) {
|
|
407
|
+
if (Object.prototype.hasOwnProperty.call(arg, key)) {
|
|
408
|
+
args[0][key] = arg[key];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return args[0];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function isArray(obj: any) {
|
|
416
|
+
return Array.isArray(obj);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function isEmptyObject(obj: any) {
|
|
420
|
+
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function isFunction(obj: any) {
|
|
424
|
+
return typeof obj === "function";
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function isPlainObject(obj: any) {
|
|
428
|
+
return Object.prototype.toString.call(obj) === "[object Object]";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A dummy function that does nothing ('no operation'). */
|
|
432
|
+
export function noop(...args: any[]): any {}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Bind one or more event handlers directly to an [[HtmlElement]].
|
|
436
|
+
*
|
|
437
|
+
* @param element HTMLElement or selector
|
|
438
|
+
* @param eventNames
|
|
439
|
+
* @param handler
|
|
440
|
+
*/
|
|
441
|
+
export function onEvent(
|
|
442
|
+
rootElem: HTMLElement | string,
|
|
443
|
+
eventNames: string,
|
|
444
|
+
handler: EventCallbackType
|
|
445
|
+
): void;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Bind one or more event handlers using event delegation.
|
|
449
|
+
*
|
|
450
|
+
* E.g. handle all 'input' events for input and textarea elements of a given
|
|
451
|
+
* form:
|
|
452
|
+
* ```ts
|
|
453
|
+
* onEvent("#form_1", "input", "input,textarea", function (e: Event) {
|
|
454
|
+
* console.log(e.type, e.target);
|
|
455
|
+
* });
|
|
456
|
+
* ```
|
|
457
|
+
*
|
|
458
|
+
* @param element HTMLElement or selector
|
|
459
|
+
* @param eventNames
|
|
460
|
+
* @param selector
|
|
461
|
+
* @param handler
|
|
462
|
+
*/
|
|
463
|
+
export function onEvent(
|
|
464
|
+
rootElem: HTMLElement | string,
|
|
465
|
+
eventNames: string,
|
|
466
|
+
selector: string,
|
|
467
|
+
handler: EventCallbackType
|
|
468
|
+
): void;
|
|
469
|
+
|
|
470
|
+
export function onEvent(
|
|
471
|
+
rootElem: HTMLElement | string,
|
|
472
|
+
eventNames: string,
|
|
473
|
+
selectorOrHandler: string | EventCallbackType,
|
|
474
|
+
handlerOrNone?: EventCallbackType
|
|
475
|
+
): void {
|
|
476
|
+
let selector: string | null, handler: EventCallbackType;
|
|
477
|
+
rootElem = elemFromSelector(rootElem)!;
|
|
478
|
+
|
|
479
|
+
if (handlerOrNone) {
|
|
480
|
+
selector = selectorOrHandler as string;
|
|
481
|
+
handler = handlerOrNone!;
|
|
482
|
+
} else {
|
|
483
|
+
selector = "";
|
|
484
|
+
handler = selectorOrHandler as EventCallbackType;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
eventNames.split(" ").forEach((evn) => {
|
|
488
|
+
(<HTMLElement>rootElem).addEventListener(evn, function (e) {
|
|
489
|
+
if (!selector) {
|
|
490
|
+
return handler!(e); // no event delegation
|
|
491
|
+
} else if (e.target) {
|
|
492
|
+
let elem = e.target as HTMLElement;
|
|
493
|
+
if (elem.matches(selector as string)) {
|
|
494
|
+
return handler!(e);
|
|
495
|
+
}
|
|
496
|
+
elem = elem.closest(selector) as HTMLElement;
|
|
497
|
+
if (elem) {
|
|
498
|
+
return handler(e);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Return a wrapped handler method, that provides `this._super`.
|
|
506
|
+
*
|
|
507
|
+
* ```ts
|
|
508
|
+
// Implement `opts.createNode` event to add the 'draggable' attribute
|
|
509
|
+
overrideMethod(ctx.options, "createNode", (event, data) => {
|
|
510
|
+
// Default processing if any
|
|
511
|
+
this._super.apply(this, event, data);
|
|
512
|
+
// Add 'draggable' attribute
|
|
513
|
+
data.node.span.draggable = true;
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
*/
|
|
517
|
+
export function overrideMethod(
|
|
518
|
+
instance: any,
|
|
519
|
+
methodName: string,
|
|
520
|
+
handler: FunctionType,
|
|
521
|
+
ctx?: any
|
|
522
|
+
) {
|
|
523
|
+
let prevSuper: FunctionType,
|
|
524
|
+
prevSuperApply: FunctionType,
|
|
525
|
+
self = ctx || instance,
|
|
526
|
+
prevFunc = instance[methodName],
|
|
527
|
+
_super = (...args: any[]) => {
|
|
528
|
+
return prevFunc.apply(self, args);
|
|
529
|
+
},
|
|
530
|
+
_superApply = (argsArray: any[]) => {
|
|
531
|
+
return prevFunc.apply(self, argsArray);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
let wrapper = (...args: any[]) => {
|
|
535
|
+
try {
|
|
536
|
+
prevSuper = self._super;
|
|
537
|
+
prevSuperApply = self._superApply;
|
|
538
|
+
self._super = _super;
|
|
539
|
+
self._superApply = _superApply;
|
|
540
|
+
return handler.apply(self, args);
|
|
541
|
+
} finally {
|
|
542
|
+
self._super = prevSuper;
|
|
543
|
+
self._superApply = prevSuperApply;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
instance[methodName] = wrapper;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Run function after ms milliseconds and return a promise that resolves when done. */
|
|
550
|
+
export function setTimeoutPromise(
|
|
551
|
+
callback: (...args: any[]) => void,
|
|
552
|
+
ms: number
|
|
553
|
+
) {
|
|
554
|
+
return new Promise((resolve, reject) => {
|
|
555
|
+
setTimeout(() => {
|
|
556
|
+
try {
|
|
557
|
+
resolve(callback.apply(self));
|
|
558
|
+
} catch (err) {
|
|
559
|
+
reject(err);
|
|
560
|
+
}
|
|
561
|
+
}, ms);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Wait `ms` microseconds.
|
|
567
|
+
*
|
|
568
|
+
* Example:
|
|
569
|
+
* ```js
|
|
570
|
+
* await sleep(1000);
|
|
571
|
+
* ```
|
|
572
|
+
* @param ms duration
|
|
573
|
+
* @returns
|
|
574
|
+
*/
|
|
575
|
+
export async function sleep(ms: number) {
|
|
576
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Set or rotate checkbox status with support for tri-state.
|
|
581
|
+
*/
|
|
582
|
+
export function toggleCheckbox(
|
|
583
|
+
element: HTMLElement | string,
|
|
584
|
+
value?: boolean | null,
|
|
585
|
+
tristate?: boolean
|
|
586
|
+
): void {
|
|
587
|
+
const input = elemFromSelector(element) as HTMLInputElement;
|
|
588
|
+
assert(input.type === "checkbox");
|
|
589
|
+
tristate ??= input.classList.contains("wb-tristate") || input.indeterminate;
|
|
590
|
+
|
|
591
|
+
if (value === undefined) {
|
|
592
|
+
const curValue = input.indeterminate ? null : input.checked;
|
|
593
|
+
switch (curValue) {
|
|
594
|
+
case true:
|
|
595
|
+
value = false;
|
|
596
|
+
break;
|
|
597
|
+
case false:
|
|
598
|
+
value = tristate ? null : true;
|
|
599
|
+
break;
|
|
600
|
+
case null:
|
|
601
|
+
value = true;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
input.indeterminate = value == null;
|
|
606
|
+
input.checked = !!value;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Return `opts.NAME` if opts is valid and
|
|
611
|
+
*
|
|
612
|
+
* @param opts dict, object, or null
|
|
613
|
+
* @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
|
|
614
|
+
* @param defaultValue returned when `opts` is not an object, or does not have a NAME property
|
|
615
|
+
*/
|
|
616
|
+
export function getOption(
|
|
617
|
+
opts: any,
|
|
618
|
+
name: string,
|
|
619
|
+
defaultValue = undefined
|
|
620
|
+
): any {
|
|
621
|
+
let ext;
|
|
622
|
+
|
|
623
|
+
// Lookup `name` in options dict
|
|
624
|
+
if (opts && name.indexOf(".") >= 0) {
|
|
625
|
+
[ext, name] = name.split(".");
|
|
626
|
+
opts = opts[ext];
|
|
627
|
+
}
|
|
628
|
+
let value = opts ? opts[name] : null;
|
|
629
|
+
// Use value from value options dict, fallback do default
|
|
630
|
+
return value ?? defaultValue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Convert an Array or space-separated string to a Set. */
|
|
634
|
+
export function toSet(val: any): Set<string> {
|
|
635
|
+
if (val instanceof Set) {
|
|
636
|
+
return val;
|
|
637
|
+
}
|
|
638
|
+
if (typeof val === "string") {
|
|
639
|
+
let set = new Set<string>();
|
|
640
|
+
for (const c of val.split(" ")) {
|
|
641
|
+
set.add(c.trim());
|
|
642
|
+
}
|
|
643
|
+
return set;
|
|
644
|
+
}
|
|
645
|
+
if (Array.isArray(val)) {
|
|
646
|
+
return new Set<string>(val);
|
|
647
|
+
}
|
|
648
|
+
throw new Error("Cannot convert to Set<string>: " + val);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function type(obj: any): string {
|
|
652
|
+
return Object.prototype.toString
|
|
653
|
+
.call(obj)
|
|
654
|
+
.replace(/^\[object (.+)\]$/, "$1")
|
|
655
|
+
.toLowerCase();
|
|
656
|
+
}
|