raiutils 8.7.11 → 9.0.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/utils.ts ADDED
@@ -0,0 +1,996 @@
1
+ //https://github.com/Pecacheu/Utils.js; GNU GPL v3
2
+
3
+ //Node.js compat
4
+ type P = [typeof window, typeof history, typeof DOMRect, typeof HTMLCollection,
5
+ typeof Element, typeof NodeList, typeof addEventListener]
6
+ //@ts-expect-error
7
+ const IsNode=typeof window==='undefined', P:P=IsNode?
8
+ [{}, {back:()=>{},forward:()=>{}}, class{}, class{}, class{}, class{}, ()=>{}]:
9
+ [window, history, DOMRect, HTMLCollection, Element, NodeList, addEventListener];
10
+
11
+ //-------------------------------------------- Types --------------------------------------------
12
+
13
+ declare global {
14
+ export interface Array<T> {
15
+ /** Remove 'empty' elements like 0, false, ' ', undefined, and NaN from array.
16
+ Often useful in combination with Array.split
17
+ @param keepZero Keep `0`s */
18
+ clean(keepZero?: boolean): this;
19
+ /** Remove first instance of item from array. Use a while loop to remove all instances
20
+ @returns true if found */
21
+ remove(itm: T): boolean;
22
+ /** Calls `fn` on each index of array
23
+
24
+ If fn returns `!`, it will remove the element from the array.
25
+ Otherwise, if fn returns any non-null value, the loop is
26
+ broken and the value is returned by each
27
+ @param fn: Callback function(itm, idx, len)
28
+ @param st: Start index. If negative, relative to end
29
+ @param en: End index. If negative, relative to end */
30
+ each: <R>(fn: (itm: T, idx: number, len: number) => R | "!",
31
+ st?: number, en?: number) => (R | undefined);
32
+ /** Adds async support to `Array.each`
33
+ @param pe: Enable parallel async execution */
34
+ eachAsync: <R>(fn: (itm: T, idx: number, len: number) => R | "!",
35
+ st?: number, en?: number, pe?: boolean) => Promise<R | undefined>;
36
+ /** Find first `undefined` or `null` slot in array */
37
+ firstEmpty(): number;
38
+ //Polyfill
39
+ at(idx: number): T | undefined;
40
+ }
41
+
42
+ export interface HTMLCollection {
43
+ each: <R>(fn: (itm: Element, idx: number, len: number) => R | "!",
44
+ st?: number, en?: number) => (R | undefined);
45
+ eachAsync: <R>(fn: (itm: Element, idx: number, len: number) => R | "!",
46
+ st?: number, en?: number, pe?: boolean) => Promise<R | undefined>;
47
+ }
48
+ export interface NodeList {
49
+ each: <R>(fn: (itm: Node, idx: number, len: number) => R | "!",
50
+ st?: number, en?: number) => (R | undefined);
51
+ eachAsync: <R>(fn: (itm: Node, idx: number, len: number) => R | "!",
52
+ st?: number, en?: number, pe?: boolean) => Promise<R | undefined>;
53
+ }
54
+
55
+ export interface Function {
56
+ /** Wrap a function so that it always has a preset argument list when called.
57
+ In the called function, `this` is set to the caller's arguments, granting access to both */
58
+ wrap<A extends any[], R>(this: (this: any, ...args: A) => R, ...args: A): (this: any, ...args: any[]) => R;
59
+ }
60
+
61
+ export interface Math {
62
+ /** Cotangent */
63
+ cot(x: number): number;
64
+ }
65
+
66
+ export interface RegExpConstructor {
67
+ /** Escapes regex syntax characters */
68
+ escape(s: string): string;
69
+ }
70
+
71
+ export interface TouchList {
72
+ /** Get touch by id, if it exists */
73
+ get(id: number): Touch | undefined;
74
+ }
75
+
76
+ export interface Element {
77
+ /** Get an element's index in its parent. Returns -1 if the element has no parent */
78
+ index: number;
79
+ /** Insert child at index */
80
+ insertChildAt(el: Element, i: number): void;
81
+ /** Get element bounding rect as UtilRect object */
82
+ boundingRect: utils.UtilRect;
83
+ /** Get element inner rect (excluding border and padding) as UtilRect object */
84
+ innerRect: utils.UtilRect;
85
+ }
86
+ }
87
+
88
+ export interface AnyMap {[k: string]: any};
89
+ export interface StringMap {[k: string]: string};
90
+ export interface QueryMap {[k: string]: true | string | string[]};
91
+
92
+ //-------------------------------------------- Extensions --------------------------------------------
93
+
94
+ export namespace utils {
95
+
96
+ const [window, history, DOMRect, HTMLCollection,
97
+ Element, NodeList, addEventListener] = P;
98
+
99
+ /** Current library version */
100
+ export const VER = "v9.0";
101
+
102
+ //==== Objects ====
103
+
104
+ /** Add getter and/or setter for `name` to `obj` */
105
+ export function define(obj: Object, name: string | string[],
106
+ get?: () => any | null, set?: (v: any) => void | null) {
107
+ const t = {get: get||undefined, set: set||undefined};
108
+ if(Array.isArray(name)) for(const n of name) Object.defineProperty(obj,n,t);
109
+ else Object.defineProperty(obj,name,t);
110
+ }
111
+
112
+ /** Define immutable, non-enumerable property or method in object prototype
113
+ @param isStat Define static property directly on object
114
+ @param isWrite Make property writable */
115
+ export function proto(obj: Object, name: string, val: any, isStat?: boolean, isWrite?: boolean) {
116
+ const t = {value: val, writable: !!isWrite};
117
+ if(!isStat) obj = (obj as any).prototype;
118
+ if(Array.isArray(name)) for(const n of name) Object.defineProperty(obj,n,t);
119
+ else Object.defineProperty(obj,name,t);
120
+ }
121
+
122
+ /** Deep (recursive) Object.create
123
+ @param sub Copy down to given sub-levels, defaults to all */
124
+ export function copy<T>(obj: T, sub?: number) {
125
+ if(sub === 0 || typeof obj !== 'object') return obj;
126
+ sub = sub!>0?sub!-1:undefined;
127
+ let o2: AnyMap;
128
+ if(Array.isArray(obj)) {
129
+ o2 = new Array(obj.length);
130
+ obj.forEach((v,i) => o2[i] = copy(v,sub));
131
+ } else {
132
+ o2 = {};
133
+ for(let k in obj) o2[k] = copy(obj[k],sub);
134
+ }
135
+ return o2 as T;
136
+ }
137
+
138
+ /** Recursively merges two (or more) objects, giving the last precedence
139
+
140
+ If both objects contain a property at the same index, and both are Arrays/Objects, they are merged */
141
+ export function merge(dest: AnyMap, ...src: AnyMap[]) {
142
+ let oP: any, nP: any;
143
+ for(const s of src) for(const key in s) {
144
+ oP = dest[key], nP = s[key];
145
+ if(oP && nP) { //Conflict
146
+ if(oP.length >= 0 && nP.length >= 0) { //Both Array-like
147
+ Array.prototype.push.apply(oP, nP); continue;
148
+ } else if(typeof oP === 'object' && typeof nP === 'object') { //Both Objects
149
+ merge(oP, nP); continue;
150
+ }
151
+ }
152
+ dest[key] = nP;
153
+ }
154
+ return dest;
155
+ }
156
+
157
+ /** Safely set nested property in object, even if the higher levels don't exist.
158
+ Useful for defining settings in a complex config object
159
+ @param path Dot-separated string or array defining the path to the property
160
+ @param val Value to set
161
+ @param onlyNull Don't overwrite existing values
162
+ @returns True if successful */
163
+ export function setProp(obj: AnyMap, path: string | string[], val: any, onlyNull=false) {
164
+ if(typeof path === 'string') path = path.split('.');
165
+ let i=0, l=path.length-1, o=obj;
166
+ for(; i<l; ++i) {
167
+ o = o[path[i]!];
168
+ if(!o || typeof o !== 'object') {
169
+ if(onlyNull) return false;
170
+ o = obj[path[i]!] = {};
171
+ }
172
+ }
173
+ const p = path.at(-1)!;
174
+ if(onlyNull && o[p] != null) return false;
175
+ o[p] = val; return true;
176
+ }
177
+
178
+ /** Safely get nested property in object. Useful for reading config settings
179
+ @param path Dot-separated string or array defining the path to the property
180
+ @returns Value or `undefined` if it or any level doesn't exist */
181
+ export function getProp(obj: AnyMap, path: string | string[]): any {
182
+ if(typeof path === 'string') path = path.split('.');
183
+ try {
184
+ for(const p of path) obj=obj[p];
185
+ return obj;
186
+ } catch(_) {}
187
+ }
188
+
189
+ //==== Arrays ====
190
+
191
+ //By Pecacheu & https://stackoverflow.com/users/5445/cms
192
+ proto(Array, 'clean', function(this: any[], kz: boolean) {
193
+ for(let i=0, l=this.length, e: any; i<l; ++i) {
194
+ e=this[i];
195
+ if(isBlank(e) || e===false || !kz && e===0) this.splice(i--,1),--l;
196
+ }
197
+ return this;
198
+ });
199
+
200
+ proto(Array, 'remove', function(this: any[], itm: any) {
201
+ const i=this.indexOf(itm); if(i===-1) return false;
202
+ this.splice(i,1); return true;
203
+ });
204
+
205
+ proto(Array, 'firstEmpty', function(this: any[]) {
206
+ const l = this.length;
207
+ for(let i=0; i<l; ++i) if(this[i] == null) return i;
208
+ return l;
209
+ });
210
+
211
+ function each(this: any[], fn: (itm: any, idx: number, len: number) => any, st?: number, en?: number) {
212
+ let l=this.length, i=Math.max(st!<0?l+st!:(st||0),0), r: any;
213
+ if(en!=null) l=Math.min(en<0?l+en:en,l);
214
+ for(; i<l; ++i) if((r=fn(this[i],i,l))==='!') {
215
+ this instanceof HTMLCollection?this[i].remove():this.splice(i,1);
216
+ --i,--l;
217
+ } else if(r!=null) return r;
218
+ }
219
+ async function eachAsync(this: any[], fn: (itm: any, idx: number, len: number) => any, st?: number, en?: number, pe=true) {
220
+ let l=this.length,i=st=Math.max(st!<0?l+st!:(st||0),0), n: any, r=[];
221
+ if(en!=null) l=Math.min(en<0?l+en:en,l);
222
+ for(; i<l; ++i) {
223
+ n=fn(this[i],i,l);
224
+ if(!pe) { n=await n; if(n!=='!' && n!=null) return n; }
225
+ r.push(n);
226
+ }
227
+ if(pe) r=await Promise.all(r);
228
+ for(i=st,n=0; i<l; ++i,++n) if(r[n]==='!') {
229
+ this instanceof HTMLCollection?this[i].remove():this.splice(i,1); --i,--l;
230
+ } else if(r[n]!=null) return r[n];
231
+ }
232
+ [Array, HTMLCollection, NodeList].forEach(p => {proto(p,'each',each), proto(p,'eachAsync',eachAsync)});
233
+
234
+ //==== Numbers ====
235
+
236
+ //JS Math w/ BigInt support
237
+ type Num = number | bigint;
238
+ const BI=typeof BigInt==='undefined'?(n: Num)=>n:BigInt, B0=BI(0);
239
+ export const abs = (x: Num) => typeof x==='bigint'?(x<B0?-x:x):Math.abs(x);
240
+ export const min = (...args: Num[]) => {let v:Num,m:Num|undefined; for(v of args) v>m!||(m=v); return m!}
241
+ export const max = (...args: Num[]) => {let v:Num,m:Num|undefined; for(v of args) v<m!||(m=v); return m!}
242
+
243
+ /** Degrees <-> Radians */
244
+ export const deg = (rad: number) => rad*180/Math.PI;
245
+ export const rad = (deg: number) => deg*Math.PI/180;
246
+ Math.cot = x => 1/Math.tan(x);
247
+
248
+ /** Convert Number to fixed-length
249
+ @param radix Set to `16` for Hex or `2` for Binary */
250
+ export function fixedNum(n: Num, len: Num, radix=10) {
251
+ if(typeof len==='bigint') len=Number(len);
252
+ let s=abs(n).toString(radix).toUpperCase();
253
+ return (n<0?'-':'')+(radix==16?'0x':radix==2?'0b':'')+'0'.repeat(Math.max(len-s.length,0))+s;
254
+ }
255
+
256
+ /** Truncate n to range `[min,max]`. Also handles NaN or null */
257
+ export const bounds = <T extends Num>(n: T, min: T=0 as T, max: T=1 as T) => n>=min?n<=max?n:max:min;
258
+
259
+ /** Normalize n to the range `[min,max)`, keeping offset.
260
+ Behaves similar to modulus operator, but min doesn't have to be 0 */
261
+ export function norm<T extends Num>(n: T, min: T=0 as T, max: T=1 as T): T {
262
+ let r = max-min;
263
+ //@ts-expect-error
264
+ return ((n + abs(min))%r+r)%r+min;
265
+ }
266
+
267
+ /** Pecacheu's ultimate unit translation formula! */
268
+ export function map(input: number, minIn: number,
269
+ maxIn: number, minOut: number, maxOut: number, ease?: Ease) {
270
+ let i = (input-minIn)/(maxIn-minIn);
271
+ return ((ease?ease(i):i)*(maxOut-minOut))+minOut;
272
+ }
273
+
274
+ /** Convert HEX color to 24-bit RGB */
275
+ export function hexToRgb(hex: string) {
276
+ const c = parseInt(hex.slice(1), 16);
277
+ return [(c >> 16) & 255, (c >> 8) & 255, c & 255];
278
+ }
279
+
280
+ //By mjackson @ GitHub
281
+ /** Convert R,G,B to H,S,L values */
282
+ export function rgbToHsl(r: number, g: number, b: number) {
283
+ r /= 255, g /= 255, b /= 255;
284
+ let max=Math.max(r,g,b), min=Math.min(r,g,b), h,s,l=(max+min)/2;
285
+ if(max===min) h=s=0; //Achromatic
286
+ else {
287
+ let d=max-min;
288
+ s=l>.5 ? d/(2-max-min) : d/(max+min);
289
+ switch(max) {
290
+ case r: h=(g-b)/d + (g<b?6:0); break;
291
+ case g: h=(b-r)/d + 2; break;
292
+ default: h=(r-g)/d + 4;
293
+ }
294
+ h /= 6;
295
+ }
296
+ return [h*360, s*100, l*100];
297
+ }
298
+
299
+ /** Generate random number from min to max
300
+ @param res Minimum step between min and max (1 by default for ints)
301
+ @param bias Bias the results using an Ease function */
302
+ export function rand(min: number, max: number, res=1, bias?: Ease) {
303
+ max*=res,min*=res; let r=Math.random();
304
+ return Math.round((bias?bias(r):r)*(max-min)+min)/res;
305
+ }
306
+
307
+ /** Format Number as currency. Uses `$` by default */
308
+ export function formatCost(num: number, sym='$') {
309
+ if(!num) return sym+'0.00';
310
+ const p=num.toFixed(2).split('.');
311
+ return sym+(p[0]!).split('').reverse().reduce((a,n,i) =>
312
+ n=='-'?n+a:n+(i&&!(i%3)?',':'')+a,'')+'.'+p[1];
313
+ }
314
+
315
+ //JavaScript Easing Library by: https://github.com/gre & https://gizma.com/easing
316
+ //t should be between 0 and 1
317
+ export type Ease = (t: number) => number;
318
+ export const Easing: {[k: string]: Ease} = {
319
+ //no easing, no acceleration
320
+ linear:t => t,
321
+ //accelerating from zero velocity
322
+ easeInQuad:t => t*t,
323
+ //decelerating to zero velocity
324
+ easeOutQuad:t => t*(2-t),
325
+ //acceleration until halfway, then deceleration
326
+ easeInOutQuad:t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
327
+ //accelerating from zero velocity
328
+ easeInCubic:t => t*t*t,
329
+ //decelerating to zero velocity
330
+ easeOutCubic:t => (--t)*t*t+1,
331
+ //acceleration until halfway, then deceleration
332
+ easeInOutCubic:t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
333
+ //accelerating from zero velocity
334
+ easeInQuart:t => t*t*t*t,
335
+ //decelerating to zero velocity
336
+ easeOutQuart:t => 1-(--t)*t*t*t,
337
+ //acceleration until halfway, then deceleration
338
+ easeInOutQuart:t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
339
+ //accelerating from zero velocity
340
+ easeInQuint:t => t*t*t*t*t,
341
+ //decelerating to zero velocity
342
+ easeOutQuint:t => 1+(--t)*t*t*t*t,
343
+ //acceleration until halfway, then deceleration
344
+ easeInOutQuint:t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t
345
+ }
346
+
347
+ //==== Polyfills ====
348
+
349
+ //Polyfill for RegExp.escape by https://github.com/sindresorhus
350
+ const R_ESC1=/[|\\{}()[\]^$+*?.]/g, R_ESC2=/-/g;
351
+ if(!('escape' in RegExp)) proto(RegExp, 'escape', (s: string) => {
352
+ return s.replace(R_ESC1,'\\$&').replace(R_ESC2,'\\x2d');
353
+ }, true);
354
+
355
+ const B64='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', B64URL=B64.replace('+/','-_'),
356
+ B64F={43:62,47:63,48:52,49:53,50:54,51:55,52:56,53:57,54:58,55:59,56:60,57:61,65:0,66:1,67:2,68:3,69:4,70:5,71:6,72:7,73:8,
357
+ 74:9,75:10,76:11,77:12,78:13,79:14,80:15,81:16,82:17,83:18,84:19,85:20,86:21,87:22,88:23,89:24,90:25,97:26,98:27,99:28,
358
+ 100:29,101:30,102:31,103:32,104:33,105:34,106:35,107:36,108:37,109:38,110:39,111:40,112:41,113:42,114:43,115:44,116:45,
359
+ 117:46,118:47,119:48,120:49,121:50,122:51,45:62,95:63};
360
+
361
+ //Polyfill for Uint8Array.toBase64
362
+ if(!('toBase64' in Uint8Array.prototype)) proto(Uint8Array, 'toBase64', function(this: Uint8Array, opt: any) {
363
+ let l=this.byteLength, br=l%3, b=opt&&opt.alphabet==='base64url'?B64URL:B64,
364
+ i=0,str='',chk: number; l-=br;
365
+ for(; i<l; i+=3) {
366
+ chk = (this[i]!<<16) | (this[i+1]!<<8) | this[i+2]!;
367
+ str += b[(chk&16515072)>>18]! + b[(chk&258048)>>12] + b[(chk&4032)>>6] + b[chk&63];
368
+ }
369
+ if(br===1) {
370
+ chk = this[l]!
371
+ str += b[(chk&252)>>2]! + b[(chk&3)<<4];
372
+ if(!opt || !opt.omitPadding) str += '=';
373
+ } else if(br===2) {
374
+ chk = (this[l]!<<8) | this[l+1]!
375
+ str += b[(chk&64512)>>10]! + b[(chk&1008)>>4] + b[(chk&15)<<2];
376
+ if(!opt || !opt.omitPadding) str += '==';
377
+ }
378
+ return str;
379
+ });
380
+
381
+ function b64Char(s: string, i: number) {
382
+ const n = B64F[s.charCodeAt(i) as keyof typeof B64F];
383
+ if(n==null) throw "Bad char at "+i;
384
+ return n;
385
+ }
386
+
387
+ //Polyfill for Uint8Array.fromBase64
388
+ if(!('fromBase64' in Uint8Array)) proto(Uint8Array, 'fromBase64', (str: string) => {
389
+ let l=str.length, i=l-1;
390
+ for(; i>=0; --i) if(str.charCodeAt(i)!==61) break;
391
+ l=i+1,i=0; let br=l%4; l-=br; if(br==1) throw "Bad b64 len";
392
+ let arr=new Uint8Array(l*3/4+(br?br-1:0)), b=-1,chk;
393
+ for(; i<l; i+=4) {
394
+ chk = (b64Char(str,i)<<18)|(b64Char(str,i+1)<<12)|(b64Char(str,i+2)<<6)|b64Char(str,i+3);
395
+ arr[++b]=chk>>16, arr[++b]=chk>>8, arr[++b]=chk;
396
+ }
397
+ if(br==2) {
398
+ arr[++b] = (b64Char(str,i)<<2)|(b64Char(str,i+1)>>4);
399
+ } else if(br==3) {
400
+ chk = (b64Char(str,i)<<10)|(b64Char(str,i+1)<<4)|(b64Char(str,i+2)>>2);
401
+ arr[++b]=chk>>8, arr[++b]=chk;
402
+ }
403
+ return arr;
404
+ }, true);
405
+
406
+ if(!('at' in Array.prototype)) proto(Array, 'at', function(this: any[], idx: number) {
407
+ const l=this.length, i=idx<0?l+idx:idx;
408
+ if(i>=0 && i<l) return this[i];
409
+ });
410
+
411
+ //==== Other Extensions ====
412
+
413
+ proto(Function, 'wrap', function(this: any, ...args: any[]) {
414
+ const f=this; return function() {return f.apply(arguments, args)}
415
+ }, false, true);
416
+
417
+ if(window.TouchList) proto(TouchList, 'get', function(this: any, id: number) {
418
+ for(const t of this) if(t.identifier === id) return t;
419
+ });
420
+
421
+ define(Element.prototype, 'index', function(this: any) {
422
+ const p=this.parentElement; if(!p) return -1;
423
+ return Array.prototype.indexOf.call(p.children, this);
424
+ });
425
+
426
+
427
+ proto(Element, 'insertChildAt', function(this: any, el: Element, i: number) {
428
+ if(i<0) i=0; if(i >= this.children.length) this.appendChild(el);
429
+ else this.insertBefore(el, this.children[i]);
430
+ });
431
+
432
+ /** Get element bounding rect as UtilRect object */
433
+ export const boundingRect = (e: Element) => new UtilRect(e.getBoundingClientRect());
434
+
435
+ /** Get element inner rect (excluding border and padding) as UtilRect object */
436
+ export function innerRect(e: Element) {
437
+ let r=e.getBoundingClientRect(), s=getComputedStyle(e);
438
+ return new UtilRect(r.top+parseFloat(s.paddingTop)+parseFloat(s.borderTopWidth),
439
+ r.bottom-parseFloat(s.paddingBottom)-parseFloat(s.borderBottomWidth),
440
+ r.left+parseFloat(s.paddingLeft)+parseFloat(s.borderLeftWidth),
441
+ r.right-parseFloat(s.paddingRight)-parseFloat(s.borderRightWidth));
442
+ };
443
+
444
+ define(Element.prototype, 'boundingRect', function(this: any) {return boundingRect(this)});
445
+ define(Element.prototype, 'innerRect', function(this: any) {return innerRect(this)});
446
+
447
+ //-------------------------------------------- DOM Model --------------------------------------------
448
+
449
+ //==== General ====
450
+
451
+ /** Better class for bounding boxes */
452
+ export class UtilRect {
453
+ x!: number; left!: number;
454
+ y!: number; top!: number;
455
+ x2!: number; right!: number;
456
+ y2!: number; bottom!: number;
457
+ w!: number; width!: number;
458
+ h!: number; height!: number;
459
+ centerX!: number; centerY!: number;
460
+
461
+ constructor(t: number | DOMRect | UtilRect, b?: number, l?: number, r?: number) {
462
+ const f=Number.isFinite; let tt=0,bb=0,ll=0,rr=0;
463
+ define(this,'x', ()=>ll, v=>{f(v)?(rr+=v-ll,ll=v):0});
464
+ define(this,'y', ()=>tt, v=>{f(v)?(bb+=v-tt,tt=v):0});
465
+ define(this,'top', ()=>tt, v=>{tt=f(v)?v:0});
466
+ define(this,['bottom','y2'],()=>bb, v=>{bb=f(v)?v:0});
467
+ define(this,'left', ()=>ll, v=>{ll=f(v)?v:0});
468
+ define(this,['right','x2'], ()=>rr, v=>{rr=f(v)?v:0});
469
+ define(this,['width','w'], ()=>rr-ll, v=>{rr=v>=0?ll+v:0});
470
+ define(this,['height','h'], ()=>bb-tt, v=>{bb=v>=0?tt+v:0});
471
+ define(this,'centerX', ()=>ll/2+rr/2);
472
+ define(this,'centerY', ()=>tt/2+bb/2);
473
+ if(t instanceof DOMRect || t instanceof UtilRect)
474
+ tt=t.top, bb=t.bottom, ll=t.left, rr=t.right;
475
+ else tt=t, bb=b!, ll=l!, rr=r!;
476
+ }
477
+
478
+ /** Check if rect contains point, other rect, or Element */
479
+ contains(x: number | UtilRect | Element, y?: number): boolean {
480
+ if(x instanceof Element) return this.contains(x.boundingRect);
481
+ if(x instanceof UtilRect) return x.x >= this.x && x.x2 <= this.x2 && x.y >= this.y && x.y2 <= this.y2;
482
+ return x >= this.x && x <= this.x2 && y! >= this.y && y! <= this.y2;
483
+ }
484
+
485
+ /** Check if rect overlaps rect or Element */
486
+ overlaps(r: UtilRect | Element): boolean {
487
+ if(r instanceof Element) return this.overlaps(r.boundingRect);
488
+ if(!(r instanceof UtilRect)) return false;
489
+ let x: any, y: any;
490
+ if(r.x2-r.x >= this.x2-this.x) x = this.x >= r.x && this.x <= r.x2 || this.x2 >= r.x && this.x2 <= r.x2;
491
+ else x = r.x >= this.x && r.x <= this.x2 || r.x2 >= this.x && r.x2 <= this.x2;
492
+ if(r.y2-r.y >= this.y2-this.y) y = this.y >= r.y && this.y <= r.y2 || this.y2 >= r.y && this.y2 <= r.y2;
493
+ else y = r.y >= this.y && r.y <= this.y2 || r.y2 >= this.y && r.y2 <= this.y2;
494
+ return x&&y;
495
+ }
496
+
497
+ /** Get distance from this rect to point, other rect, or Element */
498
+ dist(x: number | UtilRect | Element, y?: number): number {
499
+ if(x instanceof Element) return this.dist(x.boundingRect);
500
+ const n = x instanceof UtilRect;
501
+ y = Math.abs((n?(x as UtilRect).centerY:y as number)-this.centerY),
502
+ x = Math.abs((n?(x as UtilRect).centerX:x as number)-this.centerX);
503
+ return Math.sqrt(x*x+y*y);
504
+ }
505
+
506
+ /** Expand (or contract if negative) a UtilRect by num of pixels. Useful for using UtilRect objects as element hitboxes
507
+ @returns self for chaining */
508
+ expand(by: number) {
509
+ this.top -= by, this.left -= by, this.bottom += by, this.right += by;
510
+ return this;
511
+ }
512
+ }
513
+
514
+ export interface UserAgentInfo {
515
+ os?: string;
516
+ rawOS?: string;
517
+ type?: string;
518
+ version?: string;
519
+ browser?: string;
520
+ engine?: string;
521
+ mobile?: boolean;
522
+ }
523
+
524
+ /** UserAgent-based Mobile device detection
525
+ @param ua User Agent string; defaults to navigator.userAgent */
526
+ export function deviceInfo(ua?: string) {
527
+ if(!ua) ua = navigator.userAgent;
528
+ const d: UserAgentInfo = {};
529
+ if(!ua.startsWith("Mozilla/5.0 ")) return d;
530
+ let o = ua.indexOf(')'), os: any = d.rawOS=ua.slice(13,o), o2: any, o3: any;
531
+ if(os.startsWith("Windows")) {
532
+ o2=os.split('; '), d.os = "Windows";
533
+ d.type = o2.indexOf('WOW64')!==-1?'x64 PC; x86 Browser':o2.indexOf('x64')!==-1?'x64 PC':'x86 PC';
534
+ o2=os.indexOf("Windows NT "), d.version = os.slice(o2+11,os.indexOf(';',o2+12));
535
+ } else if(os.startsWith("iP")) {
536
+ o2=os.indexOf("OS"), d.os = "iOS", d.type = os.slice(0,os.indexOf(';'));
537
+ d.version = os.slice(o2+3, os.indexOf(' ',o2+4)).replace(/_/g,'.');
538
+ } else if(os.startsWith("Macintosh;")) {
539
+ o2=os.indexOf(" Mac OS X"), d.os = "MacOS", d.type = os.slice(11,o2)+" Mac";
540
+ d.version = os.slice(o2+10).replace(/_/g,'.');
541
+ } else if((o2=os.indexOf("Android"))!==-1) {
542
+ d.os = "Android", d.version = os.slice(o2+8, os.indexOf(';',o2+9));
543
+ o2=os.lastIndexOf(';'), o3=os.indexOf(" Build",o2+2);
544
+ d.type = os.slice(o2+2, o3===-1?undefined:o3);
545
+ } else if(os.startsWith("X11;")) {
546
+ os=os.slice(5).split(/[;\s]+/), o2=os.length;
547
+ d.os = (os[0]==="Linux"?'':"Linux ")+os[0];
548
+ d.type = os[o2-2], d.version = os[o2-1];
549
+ }
550
+ if(o2=Number(d.version)) d.version=o2;
551
+ o2=ua.indexOf(' ',o+2), o3=ua.indexOf(')',o2+1), o3=o3===-1?o2+1:o3+2;
552
+ d.engine = ua.slice(o+2,o2), d.browser = ua.slice(o3);
553
+ d.mobile = !!ua.match(/Mobi/i);
554
+ return d;
555
+ }
556
+
557
+ export const device = IsNode ? null : deviceInfo();
558
+ export const mobile = device?.mobile;
559
+
560
+ const R_CTR = /translate(?:x|y)?\(.+?\)/gi;
561
+
562
+ /** Center element with JS
563
+
564
+ Modes:
565
+ - `trans`: Uses transform: translate. Responsive, no container
566
+ - Default: New flexbox method
567
+ @param only `x` for only x axis centering, `y` for only y axis, null for both */
568
+ export function center(el: HTMLElement, only?: "x" | "y", mode?: "trans") {
569
+ const os = el.style;
570
+ if(mode == 'trans') {
571
+ if(!os.position) os.position='absolute';
572
+ let tr = os.transform.replace(R_CTR,'').trim();
573
+ if(!only || only === 'x') os.left='50%', tr+=' translateX(-50%)';
574
+ if(!only || only === 'y') os.top='50%', tr+=' translateY(-50%)';
575
+ os.transform=tr;
576
+ } else {
577
+ const cont = mkDiv(el.parentNode, null, {display:'flex', top:0, left:0}), cs = cont.style;
578
+ cont.appendChild(el);
579
+ if(!only || only === 'x') cs.justifyContent='center', cs.width='100%';
580
+ if(!only || only === 'y') cs.alignItems='center',
581
+ cs.height='100%', cs.position='absolute';
582
+ }
583
+ }
584
+
585
+ //==== Navigation ====
586
+
587
+ /** Called when a virtual navigation event occurs, including on page load */
588
+ export let onNav: (state: any) => void;
589
+
590
+ /** Generate a virtual navigation event, updating the URL bar
591
+ @param state Optional data given to `onNav` whenever the user returns to this history entry */
592
+ export function go(url: string | URL, state?: any) {
593
+ history.pushState(state, '', url), doNav(state);
594
+ }
595
+
596
+ addEventListener('popstate', e => doNav(e.state));
597
+ addEventListener('load', () => setTimeout(() => doNav(history.state),1));
598
+ function doNav(s: any) {if(onNav) onNav.call(null,s)}
599
+
600
+ //==== DOM Creation ====
601
+
602
+ /** Create elements with ease! Just remember **PCSI** - Parent, class, style, innerHTML */
603
+ export function mkEl<K extends keyof HTMLElementTagNameMap>(tag: K, parent?: Node | null, cls?: string | null,
604
+ style?: CSSStyleDeclaration | AnyMap | null, inner?: string | null): HTMLElementTagNameMap[K] {
605
+ const e = document.createElement(tag);
606
+ if(cls != null) e.className = cls;
607
+ if(inner != null) e.innerHTML = inner;
608
+ if(style && typeof style === 'object') for(const k in style) {
609
+ if(k in e.style) (e.style as any)[k] = (style as any)[k];
610
+ else e.style.setProperty(k, (style as any)[k]);
611
+ }
612
+ if(parent != null) parent.appendChild(e);
613
+ return e;
614
+ }
615
+
616
+ /** Shorthand for `mkEl` with div tag */
617
+ export const mkDiv = (parent?: Node | null, cls?: string | null, style?: CSSStyleDeclaration | AnyMap | null,
618
+ inner?: string | null) => mkEl('div', parent, cls, style, inner);
619
+
620
+ /** Add text node to the DOM */
621
+ export const addText = (parent: Node, text: string) => parent.appendChild(document.createTextNode(text));
622
+
623
+ //==== CSS ====
624
+
625
+ let TWCanvas: HTMLCanvasElement;
626
+
627
+ /** Get predicted width of text w/ given CSS font style */
628
+ export function textWidth(txt: string, font: string) {
629
+ const c = TWCanvas||(TWCanvas=mkEl('canvas')), ctx = c.getContext('2d')!;
630
+ ctx.font = font; return ctx.measureText(txt).width;
631
+ }
632
+
633
+ const R_SK = /[A-Z]/g, R_SR=(s: string) => '-'+s.toLowerCase();
634
+ function defSty() {
635
+ for(const s of document.styleSheets as any) try {s.cssRules; return s} catch(e) {}
636
+ //let ns=mkEl('style',document.head); addText(ns,''); return ns.sheet!;
637
+ return mkEl('style', document.head).sheet;
638
+ }
639
+
640
+ /** Create a CSS rule and append it to the current document
641
+ @param sel CSS selector, eg. `.class` or `#id` */
642
+ export function addCSS(sel: string, style: CSSStyleDeclaration | AnyMap, sheet?: CSSStyleSheet) {
643
+ if(!sheet) sheet=defSty(); let k,s=[];
644
+ for(k in style) s.push(`${k.replace(R_SK, R_SR)}:${(style as AnyMap)[k]}`);
645
+ sheet!.insertRule(`${sel}{${s.join(';')}}`);
646
+ }
647
+
648
+ /** Remove a CSS selector from **all** stylesheets */
649
+ export function remCSS(sel: string) {
650
+ let s,rl;
651
+ for(s of document.styleSheets as any) {
652
+ try {rl=s.cssRules} catch(e) {continue}
653
+ for(let i=0,l=rl.length; i<l; ++i) if(rl[i] instanceof CSSStyleRule
654
+ && rl[i].selectorText===sel) s.deleteRule(i);
655
+ }
656
+ }
657
+
658
+ //==== Cookies! (Yum) ====
659
+
660
+ /** Set a cookie
661
+ @param val Leave blank to unset
662
+ @param exp Optional expiry; Set to `-1` for max
663
+ @param secure Only allow in HTTPS context */
664
+ export function setCookie(key: string, val?: string, exp?: Date | number, secure?: boolean) {
665
+ let c=`${encodeURIComponent(key)}=${val==null?'':encodeURIComponent(val)};path=/`;
666
+ if(exp != null) {
667
+ if(exp === -1) exp=new Date(9e14);
668
+ if(exp instanceof Date) c+=';expires='+exp.toUTCString();
669
+ else c+=';max-age='+exp;
670
+ }
671
+ if(secure) c+=';secure';
672
+ if(!IsNode) document.cookie = c;
673
+ return c;
674
+ }
675
+
676
+ /** Get a cookie
677
+ @param ckStr String to parse; defaults to document.cookie */
678
+ export function getCookie(key: string, ckStr?: string) {
679
+ if(ckStr == null) ckStr=document.cookie;
680
+ key=encodeURIComponent(key)+'=';
681
+ let l=ckStr.split('; '), c: string;
682
+ for(c of l) if(c.startsWith(key))
683
+ return decodeURIComponent(c.slice(key.length));
684
+ }
685
+
686
+ /** Delete a cookie */
687
+ export function remCookie(key: string) {
688
+ let c=encodeURIComponent(key)+'=;max-age=0';
689
+ if(!IsNode) document.cookie = c;
690
+ return c;
691
+ }
692
+
693
+ /** Get a list of all cookies
694
+ @param ckStr String to parse; defaults to document.cookie */
695
+ export function getCookies(ckStr?: string) {
696
+ if(ckStr == null) ckStr=document.cookie;
697
+ if(!ckStr) return {};
698
+ let c,e;
699
+ const d: StringMap = {};
700
+ for(c of ckStr.split('; ')) {
701
+ e=c.indexOf('=');
702
+ d[decodeURIComponent(c.slice(0,e))] = decodeURIComponent(c.slice(e+1));
703
+ }
704
+ return d;
705
+ }
706
+
707
+ //==== Query ====
708
+
709
+ /** Parse a URL query string into an Object
710
+
711
+ If a key has no value, it is set to `true`.
712
+ If multiple keys with the same name are found, they are combined into an array
713
+ @param sep Key separator, defaults to `&` */
714
+ export function fromQuery(query: string, sep='&') {
715
+ if(query.startsWith('?')) query=query.slice(1);
716
+ let data: QueryMap = {}, q: string, p: any, k: string, v: any;
717
+ for(q of query.split(sep)) {
718
+ p=q.indexOf('=');
719
+ if(p===-1) k=q, v=true;
720
+ else k=decodeURIComponent(q.slice(0,p)), v=decodeURIComponent(q.slice(p+1));
721
+ if(k in data) {
722
+ p = data[k];
723
+ if(Array.isArray(p)) p.push(v);
724
+ else data[k] = [p,v];
725
+ } else data[k] = v;
726
+ }
727
+ return data;
728
+ }
729
+
730
+ function valToQs(k: string, v: any) {
731
+ if(v === true) return k;
732
+ if(typeof v !== 'string') v=JSON.stringify(v);
733
+ return `${k}=${encodeURIComponent(v)}`;
734
+ }
735
+
736
+ /** Convert Object into a URL query string
737
+ @param sep Key separator, defaults to `&` */
738
+ export function toQuery(data: QueryMap, sep='&') {
739
+ let q=[], k: string, v: any;
740
+ for(k in data) {
741
+ v=data[k], k=encodeURIComponent(k);
742
+ if(Array.isArray(v)) for(const n of v) q.push(valToQs(k,n));
743
+ else q.push(valToQs(k,v));
744
+ }
745
+ return q.join(sep);
746
+ }
747
+
748
+ //==== Inputs ====
749
+
750
+ const R_NFZ=/\.0*$/;
751
+
752
+ export interface NumField extends HTMLInputElement {
753
+ num: number;
754
+ ns: string | null;
755
+ set: (num: number | string) => void;
756
+ setRange: (min?: number, max?: number, decMax?: number) => void;
757
+ onnuminput?: (this: GlobalEventHandlers, ev?: Event) => any;
758
+ }
759
+
760
+ /** Turns your boring <input> into a mobile-friendly number entry field with max/min & negative support!
761
+
762
+ Tips:
763
+ - Use `field.onnuminput` in place of oninput, get number value with field.num
764
+ - On mobile, use star key for decimal point and pound key for negative
765
+ - You can set `field.nStep` in order to change the up/down arrow step size
766
+ - Use `field.setRange` to change min, max, and decMax
767
+
768
+ @param min Min value, default min safe int
769
+ @param max Max value, default max safe int
770
+ @param decMax Max decimal precision (eg. 3 is 0.001), default 0
771
+ @param sym If a symbol (eg. '$') is given, uses currency mode */
772
+ export function numField(field: HTMLInputElement, min?: number, max?: number, decMax?: number, sym?: string) {
773
+ const f = field as NumField, RM = RegExp(`[,${sym?RegExp.escape(sym):''}]`, 'g');
774
+ f.type = (mobile||decMax||sym)?'tel':'number';
775
+ f.setAttribute('pattern', "\\d*");
776
+ //@ts-expect-error
777
+ if(!f.step) f.step = 1;
778
+ f.addEventListener('keydown', e => {
779
+ if(e.ctrlKey) return;
780
+ let k=e.key, kn=k.length===1&&Number.isFinite(Number(k)),
781
+ ns=f.ns, len=ns!.length, dec=ns!.indexOf('.');
782
+
783
+ if(k==='Tab' || k==='Enter') return;
784
+ else if(kn) {if(dec===-1 || len-dec < decMax!+1) ns+=k} //Number
785
+ else if(k==='.' || k==='*') {if(decMax && dec==-1
786
+ && f.num!=max && (min!>=0 || f.num!=min)) { //Decimal
787
+ if(!len && min!>0) ns=Math.floor(min!)+'.';
788
+ else ns+='.';
789
+ }} else if(k==='Backspace' || k==='Delete') { //Backspace
790
+ if(min!>0 && f.num===min && ns!.endsWith('.')) ns='';
791
+ else ns=ns!.slice(0,-1);
792
+ } else if(k==='-' || k==='#') {if(min!<0 && !len) ns='-'} //Negative
793
+ else if(k==='ArrowUp') ns=null, f.set(f.num+Number(f.step)); //Up
794
+ else if(k==='ArrowDown') ns=null, f.set(f.num-Number(f.step)); //Down
795
+
796
+ if(ns !== null && ns !== f.ns) {
797
+ len=ns.length, dec=ns.indexOf('.');
798
+ let neg=ns==='-'||ns==='-.', s=neg?'0':ns+(ns.endsWith('.')?'0':''),
799
+ nr=Number(s), n=bounds(nr, min, max);
800
+ if(!kn || !ns || f.num !== n || (dec!==-1 && len-dec < decMax!+1)) {
801
+ f.ns=ns, f.num=n;
802
+ f.value = sym ? neg?sym+'-0.00':formatCost(n,sym):
803
+ (ns[0]==='-'?'-':'')+Math.floor(Math.abs(n))
804
+ +(dec!==-1?ns.slice(dec)+(R_NFZ.test(ns)?'0':''):'');
805
+ if(f.onnuminput) f.onnuminput.call(f,e);
806
+ }
807
+ }
808
+ e.preventDefault();
809
+ });
810
+ function numRng(n: any) {
811
+ if(typeof n==='string') n=n.replace(RM,'');
812
+ n=bounds(Number(n)||0, min, max);
813
+ return decMax?Number(n.toFixed(decMax)):Math.round(n);
814
+ }
815
+ f.set=n => {
816
+ f.num = numRng(n);
817
+ f.ns = f.num.toString();
818
+ f.value = sym?formatCost(f.num,sym):f.ns;
819
+ f.ns=f.ns.replace(/^(-?)0+/,'$1');
820
+ if(f.onnuminput) f.onnuminput.call(f);
821
+ }
822
+ f.setRange=(nMin, nMax, nDecMax) => {
823
+ min=nMin==null ? Number.MIN_SAFE_INTEGER : nMin;
824
+ max=nMax==null ? Number.MAX_SAFE_INTEGER : nMax;
825
+ decMax=nDecMax==null ? sym?2:0 : nDecMax;
826
+ if(numRng(f.num) !== f.num) f.set(f.num);
827
+ }
828
+ f.addEventListener('input', () => f.set(f.value));
829
+ f.addEventListener('paste', e => {f.set(e.clipboardData!.getData('text')); e.preventDefault()});
830
+ f.setRange(min, max, decMax);
831
+ return f;
832
+ }
833
+
834
+ export interface TextArea extends HTMLTextAreaElement {
835
+ set: (val: string) => void;
836
+ }
837
+
838
+ //By Rick Kukiela @ StackOverflow
839
+ /** Auto-resizing textarea, dynamically scales lineHeight based on input.
840
+ Use `el.set(value)` to set value & update size */
841
+ export function autosize(el: HTMLTextAreaElement, maxRows=5, minRows=1) {
842
+ const e = el as TextArea;
843
+ e.set = v => {e.value=v,cb()};
844
+ let s=e.style;
845
+ s.maxHeight=s.resize='none', s.minHeight='0', s.height='auto';
846
+ e.setAttribute('rows', minRows as any);
847
+ function cb() {
848
+ if(e.scrollHeight===0) return setTimeout(cb,1); //Still loading
849
+ e.setAttribute('rows', 1 as any);
850
+ //Override style
851
+ let cs=getComputedStyle(e);
852
+ s.setProperty('overflow', 'hidden', 'important');
853
+ s.width=e.innerRect.w+'px', s.boxSizing='content-box', s.borderWidth=s.paddingInline='0';
854
+ //Calc scroll height
855
+ let pad=parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom),
856
+ lh=cs.lineHeight==='normal' ? parseFloat(cs.height) : parseFloat(cs.lineHeight),
857
+ rows=Math.round((Math.round(e.scrollHeight) - pad)/lh);
858
+ //Undo overrides & apply
859
+ s.overflow=s.width=s.boxSizing=s.borderWidth=s.paddingInline='';
860
+ e.setAttribute('rows', bounds(rows, minRows, maxRows) as any);
861
+ }
862
+ e.addEventListener('input', cb);
863
+ }
864
+
865
+ //==== Dates ====
866
+
867
+ export const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
868
+ function fixed2(n: number) {return n<=9?'0'+n:n}
869
+
870
+ export interface DateFormatOpts {
871
+ /** Include seconds */
872
+ sec?: boolean;
873
+ /** True or `3` to include milliseconds (requires sec), `2` or `1` to limit precision */
874
+ ms?: boolean | number;
875
+ /** Use 24-hour time */
876
+ h24?: boolean;
877
+ /** Show time only, false to show date only, null to show both */
878
+ time?: boolean;
879
+ /** Add date suffix (1st, 2nd, etc.), default true */
880
+ suf?: boolean;
881
+ /** Show year (default true), or a number to show year only if it differs from given year */
882
+ year?: boolean | number;
883
+ /** Put date first instead of time */
884
+ df?: boolean;
885
+ }
886
+
887
+ /** Format Date object into human-readable string */
888
+ export function formatDate(d?: Date, opt: DateFormatOpts={}) {
889
+ let t='', yy: number, dd: any;
890
+ if(d==null || !d.getDate || !((yy=d.getFullYear())>1969)) return "[Invalid Date]";
891
+ if(opt.time==null || opt.time) {
892
+ let h=d.getHours(),pm=''; if(!opt.h24) {pm=' AM'; if(h>=12) pm=' PM',h-=12; if(!h) h=12}
893
+ t=h+':'+fixed2(d.getMinutes())+(opt.sec?':'+fixed2(d.getSeconds())+(opt.ms?(d.getMilliseconds()
894
+ /1000).toFixed(Number.isFinite(opt.ms)?opt.ms as number:3).slice(1):''):'');
895
+ t+=pm; if(opt.time) return t;
896
+ }
897
+ dd=d.getDate();
898
+ dd=months[d.getMonth()]+' '+((opt.suf==null||opt.suf)?suffix(dd):dd);
899
+ if((opt.year==null||opt.year) && opt.year!==yy) dd=dd+', '+yy;
900
+ return opt.df?dd+(t&&' '+t):(t&&t+' ')+dd;
901
+ }
902
+
903
+ /** Add suffix to number (eg. 31st, 12th, 22nd) */
904
+ export function suffix(n: number) {
905
+ let j=n%10, k=n%100;
906
+ if(j==1 && k!=11) return n+"st";
907
+ if(j==2 && k!=12) return n+"nd";
908
+ if(j==3 && k!=13) return n+"rd";
909
+ return n+"th";
910
+ }
911
+
912
+ /** Set `datetime-local` or `date` input from JS Date object or string, adjusting for local timezone */
913
+ export function setDateTime(el: HTMLInputElement, date: Date | string | number) {
914
+ if(!(date instanceof Date)) date=new Date(date);
915
+ el.value = new Date(date.getTime() - date.getTimezoneOffset()*60000).
916
+ toISOString().slice(0, el.type==='date'?10:19);
917
+ }
918
+
919
+ /** Get value of `datetime-local` or `date` input as JS Date */
920
+ export const getDateTime = (el: HTMLInputElement) => new Date(el.value+(el.type==='date'?'T00:00':''));
921
+
922
+ //==== Utility ====
923
+
924
+ const R_ES = /\S/;
925
+
926
+ /** Check if string, array, or object is empty.
927
+ Returns false for all other types */
928
+ export function isBlank(s: any) {
929
+ if(s == null) return true;
930
+ if(typeof s === 'string') return !R_ES.test(s);
931
+ if(typeof s === 'object') {
932
+ if(typeof s.length === 'number') return s.length === 0;
933
+ return Object.keys(s).length === 0;
934
+ }
935
+ return false;
936
+ }
937
+
938
+ /** Trigger browser download of file. If `data` is a string or URL,
939
+ it is treated as a URL. Otherwise, it is downloaded as Blob data */
940
+ export async function download(data: string | URL | Blob | ArrayBuffer, name?: string) {
941
+ const a = mkEl('a');
942
+ if(typeof data === 'string' || data instanceof URL) {
943
+ a.href = data.toString();
944
+ a.download = name || a.href.split('/').at(-1)!;
945
+ a.click();
946
+ } else {
947
+ if(!(data instanceof Blob)) data = new Blob([data]);
948
+ const u = URL.createObjectURL(data);
949
+ a.href=u, a.download=name||'file', a.click();
950
+ URL.revokeObjectURL(u);
951
+ }
952
+ }
953
+
954
+ /** setTimeout but async */
955
+ export const delay = (ms: number): Promise<void> => new Promise(r => setTimeout(r,ms));
956
+
957
+ //-------------------------------------------- NodeJS --------------------------------------------
958
+
959
+ let os: typeof import('os');
960
+ async function importNode() {
961
+ if(os) return;
962
+ os = await import('os');
963
+ }
964
+
965
+ /** Get list of system IPs */
966
+ export async function getIPs() {
967
+ await importNode();
968
+ const ip: string[]=[], fl=os.networkInterfaces();
969
+ for(let k in fl) fl[k]!.forEach(f => {
970
+ if(!f.internal && f.family == 'IPv4' && f.mac != '00:00:00:00:00:00' && f.address) ip.push(f.address);
971
+ });
972
+ return ip;
973
+ }
974
+
975
+ /** Get system info
976
+ @returns [sysOS, arch, cpuInfo] */
977
+ export async function getOS() {
978
+ await importNode();
979
+ let sysOS, arch;
980
+ switch(os.platform()) {
981
+ case 'win32': sysOS="Windows"; break;
982
+ case 'darwin': sysOS="MacOS"; break;
983
+ case 'linux': sysOS="Linux"; break;
984
+ default: sysOS=os.platform();
985
+ }
986
+ switch(os.arch()) {
987
+ case 'ia32': arch="32-bit"; break;
988
+ case 'x64': arch="64-bit"; break;
989
+ case 'arm': arch="ARM"; break;
990
+ default: arch=os.arch();
991
+ }
992
+ return [sysOS, arch, os.cpus()[0]?.model||''];
993
+ }
994
+ }
995
+
996
+ export default utils;