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/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
+ "&": "&amp;",
26
+ "<": "&lt;",
27
+ ">": "&gt;",
28
+ '"': "&quot;",
29
+ "'": "&#39;",
30
+ "/": "&#x2F;",
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
+ }