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/README.md +110 -118
- package/dist/router.d.ts +25 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +1 -0
- package/dist/router.js.map +1 -0
- package/dist/schema.d.ts +113 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +1 -0
- package/dist/schema.js.map +1 -0
- package/dist/utils.d.ts +288 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -0
- package/dist/uuid.d.ts +29 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +1 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +22 -7
- package/src/router.ts +131 -0
- package/src/schema.ts +259 -0
- package/src/utils.ts +996 -0
- package/src/uuid.ts +102 -0
- package/router.js +0 -109
- package/schema.js +0 -148
- package/utils.js +0 -911
- package/utils.min.js +0 -2
- package/uuid.js +0 -69
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;
|