raiutils
Version:
The ultimate JavaScript companion library. You'll never need jQuery again!
910 lines (826 loc) • 35.1 kB
JavaScript
//https://github.com/Pecacheu/Utils.js; GNU GPL v3
'use strict';
const utils = {VER:'v8.7.8'},
_uNJS = typeof global!='undefined';
//Node.js compat
let UtilRect, P=(typeof window=='undefined')?
[{}, {back:()=>{},forward:()=>{}}, class{}, class{}, class{}, class{}, ()=>{}]:
[window, history, DOMRect, HTMLCollection, Element, NodeList, addEventListener];
(() => { //Utils Library
const [window, history, DOMRect, HTMLCollection,
Element, NodeList, addEventListener] = P;
//Add getter/setter to object
utils.define = (obj, name, get, set) => {
const t={}; if(get) t.get=get; if(set) t.set=set;
if(Array.isArray(name)) for(let n of name) Object.defineProperty(obj,n,t);
else Object.defineProperty(obj,name,t);
}
//Define prop in object prototype
utils.proto = (obj, name, val, st) => {
const t={value:val}; if(!st) obj=obj.prototype;
if(Array.isArray(name)) for(let n of name) Object.defineProperty(obj,n,t);
else Object.defineProperty(obj,name,t);
}
//Cookies! (Yum)
utils.setCookie = (key,value,exp,secure) => {
let c=`${encodeURIComponent(key)}=${value==null?'':encodeURIComponent(value)};path=/`;
if(exp != null) {
if(exp === -1) exp=new Date(Number.MAX_SAFE_INTEGER/10);
if(exp instanceof Date) c+=';expires='+exp.toUTCString();
else c+=';max-age='+exp;
}
if(secure) c+=';secure';
if(_uNJS) return c;
document.cookie=c;
}
utils.remCookie = key => {
let c=encodeURIComponent(key)+'=;max-age=0';
if(_uNJS) return c;
document.cookie=c;
}
utils.getCookies = ckStr => {
if(ckStr == null) ckStr=document.cookie;
if(!ckStr) return {};
let l=ckStr.split('; '),c,e,d={};
for(c of l) {
e=c.indexOf('=');
d[decodeURIComponent(c.slice(0,e))]=decodeURIComponent(c.slice(e+1));
}
return d;
}
utils.getCookie = (key, ckStr) => {
if(ckStr == null) ckStr=document.cookie;
key=encodeURIComponent(key)+'=';
let l=ckStr.split('; '),c;
for(c of l) if(c.startsWith(key))
return decodeURIComponent(c.slice(key.length));
}
//Wrap a function so that it always has a preset argument list when called
//In the called function, 'this' is set to the caller's arguments, granting access to both
utils.proto(Function, 'wrap', function(/*...*/) {
const f=this, a=arguments; return function() {return f.apply(arguments,a)}
})
//Deep (recursive) Object.create
//Copies down to given sub levels. All levels if undefined
utils.copy = (o, sub) => {
if(sub===0 || typeof o != 'object') return o;
sub=sub>0?sub-1:null; let o2;
if(Array.isArray(o)) o2=new Array(o.length),
o.forEach((v,i) => {o2[i]=utils.copy(v,sub)});
else {o2={};for(let k in o) o2[k]=utils.copy(o[k],sub)}
return o2;
}
//UserAgent-based Mobile device detection
utils.deviceInfo = ua => {
const d={}; if(!ua) ua=navigator.userAgent;
if(!ua.startsWith("Mozilla/5.0 ")) return d;
let o=ua.indexOf(')'), os=d.rawOS=ua.slice(13,o), o2,o3;
if(os.startsWith("Windows")) {
o2=os.split('; '), d.os = "Windows";
d.type = o2.indexOf('WOW64')!=-1?'x64 PC; x86 Browser':o2.indexOf('x64')!=-1?'x64 PC':'x86 PC';
o2=os.indexOf("Windows NT "), d.version = os.slice(o2+11,os.indexOf(';',o2+12));
} else if(os.startsWith("iP")) {
o2=os.indexOf("OS"), d.os = "iOS", d.type = os.slice(0,os.indexOf(';'));
d.version = os.slice(o2+3, os.indexOf(' ',o2+4)).replace(/_/g,'.');
} else if(os.startsWith("Macintosh;")) {
o2=os.indexOf(" Mac OS X"), d.os = "MacOS", d.type = os.slice(11,o2)+" Mac";
d.version = os.slice(o2+10).replace(/_/g,'.');
} else if((o2=os.indexOf("Android"))!=-1) {
d.os = "Android", d.version = os.slice(o2+8, os.indexOf(';',o2+9));
o2=os.lastIndexOf(';'), o3=os.indexOf(" Build",o2+2);
d.type = os.slice(o2+2, o3==-1?undefined:o3);
} else if(os.startsWith("X11;")) {
os=os.slice(5).split(/[;\s]+/), o2=os.length;
d.os = (os[0]=="Linux"?'':"Linux ")+os[0];
d.type = os[o2-2], d.version = os[o2-1];
}
if(o2=Number(d.version)) d.version=o2;
o2=ua.indexOf(' ',o+2), o3=ua.indexOf(')',o2+1), o3=o3==-1?o2+1:o3+2;
d.engine = ua.slice(o+2,o2), d.browser = ua.slice(o3);
d.mobile = !!ua.match(/Mobi/i); return d;
}
if(!_uNJS) {
utils.device = utils.deviceInfo();
utils.mobile = utils.device.mobile;
}
//Get touch by id, returns null if none found
if(window.TouchList) utils.proto(TouchList, 'get', function(id) {
for(let k in this) if(this[k].identifier == id) return this[k]; return 0;
})
const R_NFZ=/\.0*$/;
/*Turns your boring <input> into a mobile-friendly number entry field with max/min & negative support!
min: Min value, default min safe int
max: Max value, default max safe int
decMax: Max decimal precision (eg. 3 is 0.001), default 0
sym: If a symbol (eg. '$') is given, uses currency mode
Tips:
- Use field.onnuminput in place of oninput, get number value with field.num
- On mobile, use star key for decimal point and pound key for negative
- You can set field.step in order to change the up/down arrow step size
- Use field.setRange to change min, max, and decMax*/
utils.numField=(f, min, max, decMax, sym) => {
const RM=RegExp(`[,${sym?RegExp.escape(sym):''}]`,'g');
f.type=(utils.mobile||decMax||sym)?'tel':'number';
f.setAttribute('pattern',"\\d*");
if(!f.step) f.step=1;
f.addEventListener('keydown',e => {
if(e.ctrlKey) return;
let k=e.key, kn=k.length===1&&Number.isFinite(Number(k)),
ns=f.ns, len=ns.length, dec=ns.indexOf('.');
if(k==='Tab' || k==='Enter') return;
else if(kn) {if(dec===-1 || len-dec < decMax+1) ns+=k} //Number
else if(k==='.' || k==='*') {if(decMax && dec==-1
&& f.num!=max && (min>=0 || f.num!=min)) { //Decimal
if(!len && min>0) ns=Math.floor(min)+'.';
else ns+='.';
}} else if(k==='Backspace' || k==='Delete') { //Backspace
if(min>0 && f.num===min && ns.endsWith('.')) ns='';
else ns=ns.slice(0,-1);
} else if(k==='-' || k==='#') {if(min<0 && !len) ns='-'} //Negative
else if(k==='ArrowUp') ns=null, f.set(f.num+Number(f.step)); //Up
else if(k==='ArrowDown') ns=null, f.set(f.num-Number(f.step)); //Down
if(ns !== null && ns !== f.ns) {
len=ns.length, dec=ns.indexOf('.');
let neg=ns==='-'||ns==='-.', s=neg?'0':ns+(ns.endsWith('.')?'0':''),
nr=Number(s), n=Math.min(max,Math.max(min,nr));
if(!kn || !ns || f.num !== n || (dec!==-1 && len-dec < decMax+1)) {
f.ns=ns, f.num=n;
f.value = sym ? neg?sym+'-0.00':utils.formatCost(n,sym):
(ns[0]==='-'?'-':'')+Math.floor(Math.abs(n))
+(dec!==-1?ns.slice(dec)+(R_NFZ.test(ns)?'0':''):'');
if(f.onnuminput) f.onnuminput.call(f);
}
}
e.preventDefault();
});
function numRng(n) {
if(typeof n==='string') n=n.replace(RM,'');
n=Math.min(max,Math.max(min,Number(n)||0));
return decMax?Number(n.toFixed(decMax)):Math.round(n);
}
f.set=n => {
f.num = numRng(n);
f.ns = f.num.toString();
f.value = sym?utils.formatCost(f.num,sym):f.ns;
f.ns=f.ns.replace(/^(-?)0+/,'$1');
if(f.onnuminput) f.onnuminput.call(f);
}
f.setRange=(nMin, nMax, nDecMax) => {
min=nMin==null ? Number.MIN_SAFE_INTEGER : nMin;
max=nMax==null ? Number.MAX_SAFE_INTEGER : nMax;
decMax=nDecMax==null ? sym?2:0 : nDecMax;
if(numRng(f.num) !== f.num) f.set(f.num);
}
f.addEventListener('input',() => f.set(f.value));
f.addEventListener('paste',e => {f.set(e.clipboardData.getData('text')); e.preventDefault()});
f.setRange(min, max, decMax);
return f;
}
//Auto-resizing textarea, dynamically scales lineHeight based on input
//Use `el.set(value)` to set value & update size
//By Rick Kukiela @ StackOverflow
utils.autosize = (el, maxRows=5, minRows=1) => {
el.set=v => {el.value=v,cb()};
let s=el.style;
s.maxHeight=s.resize='none', s.minHeight=0, s.height='auto';
el.setAttribute('rows',minRows);
function cb() {
if(el.scrollHeight===0) return setTimeout(cb,1); //Still loading
el.setAttribute('rows',1);
//Override style
let cs=getComputedStyle(el);
s.setProperty('overflow','hidden','important');
s.width=el.innerRect.w+'px', s.boxSizing='content-box', s.borderWidth=s.paddingInline=0;
//Calc scroll height
let pad=parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom),
lh=cs.lineHeight==='normal' ? parseFloat(cs.height) : parseFloat(cs.lineHeight),
rows=Math.round((Math.round(el.scrollHeight) - pad)/lh);
//Undo overrides & apply
s.overflow=s.width=s.boxSizing=s.borderWidth=s.paddingInline='';
el.setAttribute('rows',utils.bounds(rows, minRows, maxRows));
}
el.addEventListener('input',cb);
}
//Format Number as currency. Uses '$' by default
utils.formatCost = (n, sym='$') => {
if(!n) return sym+'0.00';
const p=n.toFixed(2).split('.');
return sym+p[0].split('').reverse().reduce((a,n,i) =>
n=='-'?n+a:n+(i&&!(i%3)?',':'')+a,'')+'.'+p[1];
}
//Convert Number to fixed-length
//Set radix to 16 for HEX
utils.fixedNum = function(n,len,radix=10) {
let s=Math.abs(n).toString(radix).toUpperCase();
return (n<0?'-':'')+(radix==16?'0x':radix==2?'0b':'')+'0'.repeat(Math.max(len-s.length,0))+s;
}
utils.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function fixed2(n) {return n<=9?'0'+n:n}
//Set 'datetime-local' or 'date' input from JS Date object or string, adjusting for local timezone
utils.setDateTime = (el, date) => {
if(!(date instanceof Date)) date=new Date(date);
el.value = new Date(date.getTime() - date.getTimezoneOffset()*60000).
toISOString().slice(0, el.type==='date'?10:19);
}
//Get value of 'datetime-local' or 'date' input as JS Date
utils.getDateTime = el => new Date(el.value+(el.type==='date'?'T00:00':''));
//Format Date object into human-readable string
//opt:
// sec: True to include seconds
// ms: True or 3 to include milliseconds (requires sec), 2 or 1 to limit precision
// h24: True to use 24-hour time
// time: True to show time only, false to show date only, null to show both
// suf: False to drop date suffix (1st, 2nd, etc.)
// year: False to hide year, or a number to show year only if it differs from given year
// df: True to put date first instead of time
utils.formatDate = (d, opt={}) => {
let t='',yy,dd;
if(d==null || !d.getDate || !((yy=d.getFullYear())>1969)) return "[Invalid Date]";
if(opt.time==null||opt.time) {
let h=d.getHours(),pm=''; if(!opt.h24) {pm=' AM'; if(h>=12) pm=' PM',h-=12; if(!h) h=12}
t=h+':'+fixed2(d.getMinutes())+(opt.sec?':'+fixed2(d.getSeconds())
+(opt.ms?(d.getMilliseconds()/1000).toFixed(Number.isFinite(opt.ms)?opt.ms:3).slice(1):''):'');
t+=pm; if(opt.time) return t;
}
dd=d.getDate();
dd=utils.months[d.getMonth()]+' '+((opt.suf==null||opt.suf)?utils.suffix(dd):dd);
if((opt.year==null||opt.year) && opt.year!==yy) dd=dd+', '+yy;
return opt.df?dd+(t&&' '+t):(t&&t+' ')+dd;
}
//Add appropriate suffix to number. (ex. 31st, 12th, 22nd)
utils.suffix = n => {
let j=n%10, k=n%100;
if(j==1 && k!=11) return n+"st";
if(j==2 && k!=12) return n+"nd";
if(j==3 && k!=13) return n+"rd";
return n+"th";
}
//Virtual Page Navigation
let H=history;
utils.goBack = H.back.bind(H);
utils.goForward = H.forward.bind(H);
utils.go = (url,st) => {H.pushState(st,'',url||location.pathname),doNav(st)}
addEventListener('popstate', (e) => doNav(e.state));
addEventListener('load', () => setTimeout(doNav.wrap(H.state),1));
function doNav(s) {if(utils.onNav) utils.onNav.call(null,s)}
//Create element of type with parent, className, style object, and innerHTML string
//(Just remember the order PCSI!) Use null to leave any parameter blank
utils.mkEl = (t,p,c,s,i) => {
const e=document.createElement(t);
if(c!=null) e.className=c; if(i!=null) e.innerHTML=i;
if(s && typeof s=='object') for(let k in s) {
if(k in e.style) e.style[k]=s[k]; else e.style.setProperty(k,s[k]);
}
if(p!=null) p.appendChild(e); return e;
}
utils.mkDiv = (p,c,s,i) => utils.mkEl('div',p,c,s,i);
utils.addText = (el, text) => el.appendChild(document.createTextNode(text));
//Get predicted width of text given CSS font style
utils.textWidth = (txt, font) => {
const c=window.TWCanvas||(window.TWCanvas=utils.mkEl('canvas')),
ctx=c.getContext('2d'); ctx.font=font; return ctx.measureText(txt).width;
}
//It's useful for any canvas-style app to have the page dimensions on hand
utils.define(utils, 'w', ()=>innerWidth);
utils.define(utils, 'h', ()=>innerHeight);
/*Set a nested/recursive property in an object, even if the higher levels don't exist. Useful for defining settings in a complex config object
obj: Object to set property in
path: String (dot separated) or array defining the path to the property
val: Value to set
onlyNull: True to set value only if it doesn't exist*/
utils.setPropSafe = (obj, path, val, onlyNull=false) => {
if(typeof path=='string') path=path.split('.');
let li=path.length-1;
path.each(p => {typeof obj[p]=='object'?obj=obj[p]:obj=obj[p]={}},0,li);
li=path[li]; if(!onlyNull || obj[li]==null) return obj[li]=val;
return obj[li];
}
/*Gets a nested/recursive property in an object, returning undefined if it or any higher level doesn't exist. Useful for reading settings from a complex config object
obj: Object to read property from
path: String (dot separated) or array defining the path to the property*/
utils.getPropSafe = (obj, path) => {
if(typeof path=='string') path=path.split('.');
try {for(let p of path) obj=obj[p]; return obj} catch(_) {}
}
//Remove 'empty' elements like 0, false, ' ', undefined, and NaN from array
//Often useful in combination with Array.split. Set 'keepZero' to true to keep '0's
//Function by: Pecacheu & https://stackoverflow.com/users/5445/cms
utils.proto(Array, 'clean', function(kz) {
for(let i=0,e,l=this.length; i<l; ++i) {
e=this[i]; if(utils.isBlank(e) || e === false ||
!kz && e === 0) this.splice(i--,1),l--;
} return this;
})
//Remove first instance of item from array. Returns false if not found
//Use a while loop to remove all instances
utils.proto(Array, 'remove', function(itm) {
const i=this.indexOf(itm); if(i==-1) return false;
this.splice(i,1); return true;
})
//Calls fn on each index of array
//fn: Callback function(element, index, length)
//st: Start index, optional. If negative, relative to end of array
//en: End index, optional. If negative, relative to end of array
//If fn returns '!', it will remove the element from the array
//Otherwise, if fn returns any non-null value,
//the loop is broken and the value is returned by each
function each(fn,st,en) {
let l=this.length,i=Math.max(st<0?l+st:(st||0),0),r;
if(en!=null) l=Math.min(en<0?l+en:en,l);
for(; i<l; ++i) if((r=fn(this[i],i,l))==='!') {
this instanceof HTMLCollection?this[i].remove():this.splice(i,1); --i,--l;
} else if(r!=null) return r;
}
//Adds async support to each
//pe: true (default) to enable parallel async execution
async function eachAsync(fn,st,en,pe=true) {
let l=this.length,i=st=Math.max(st<0?l+st:(st||0),0),n,r=[];
if(en!=null) l=Math.min(en<0?l+en:en,l);
for(; i<l; ++i) {
n=fn(this[i],i,l);
if(!pe) { n=await n; if(n!=='!' && n!=null) return n; }
r.push(n);
}
if(pe) r=await Promise.all(r);
for(i=st,n=0; i<l; ++i,++n) if(r[n]==='!') {
this instanceof HTMLCollection?this[i].remove():this.splice(i,1); --i,--l;
} else if(r[n]!=null) return r[n];
}
[Array,HTMLCollection,NodeList].forEach(p => {utils.proto(p,'each',each), utils.proto(p,'eachAsync',eachAsync)});
const B64='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', B64URL=B64.replace('+/','-_'),
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,
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,
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,
117:46,118:47,119:48,120:49,121:50,122:51,45:62,95:63};
//Polyfill for Uint8Array.toBase64
if(!('toBase64' in Uint8Array.prototype)) utils.proto(Uint8Array, 'toBase64', function(opt) {
let l=this.byteLength, br=l%3, b=opt&&opt.alphabet==='base64url'?B64URL:B64, i=0,str='',chk; l-=br;
for(; i<l; i+=3) {
chk = (this[i]<<16)|(this[i+1]<<8)|this[i+2];
str += b[(chk&16515072)>>18] + b[(chk&258048)>>12] + b[(chk&4032)>>6] + b[chk&63];
}
if(br==1) {
chk = this[l];
str += b[(chk&252)>>2] + b[(chk&3)<<4];
if(!opt || !opt.omitPadding) str += '=';
} else if(br==2) {
chk = (this[l]<<8)|this[l+1];
str += b[(chk&64512)>>10] + b[(chk&1008)>>4] + b[(chk&15)<<2];
if(!opt || !opt.omitPadding) str += '==';
}
return str;
})
function b64Char(s,i) {
s=B64F[s.charCodeAt(i)];
if(s==null) throw "Bad char at "+i;
return s;
}
//Polyfill for Uint8Array.fromBase64
if(!('fromBase64' in Uint8Array)) utils.proto(Uint8Array, 'fromBase64', str => {
let l=str.length, i=l-1;
for(; i>=0; --i) if(str.charCodeAt(i)!==61) break;
l=i+1,i=0; let br=l%4; l-=br; if(br==1) throw "Bad b64 len";
let arr=new Uint8Array(l*3/4+(br?br-1:0)), b=-1,chk;
for(; i<l; i+=4) {
chk = (b64Char(str,i)<<18)|(b64Char(str,i+1)<<12)|(b64Char(str,i+2)<<6)|b64Char(str,i+3);
arr[++b]=chk>>16, arr[++b]=chk>>8, arr[++b]=chk;
}
if(br==2) {
arr[++b] = (b64Char(str,i)<<2)|(b64Char(str,i+1)>>4);
} else if(br==3) {
chk = (b64Char(str,i)<<10)|(b64Char(str,i+1)<<4)|(b64Char(str,i+2)>>2);
arr[++b]=chk>>8, arr[++b]=chk;
}
return arr;
},1)
const R_ESC1=/[|\\{}()[\]^$+*?.]/g, R_ESC2=/-/g;
//Polyfill for RegExp.escape by https://github.com/sindresorhus
if(!('escape' in RegExp)) utils.proto(RegExp, 'escape', s => {
return s.replace(R_ESC1,'\\$&').replace(R_ESC2,'\\x2d');
},1)
//Get an element's index in its parent. Returns -1 if the element has no parent
utils.define(Element.prototype, 'index', function() {
const p=this.parentElement; if(!p) return -1;
return Array.prototype.indexOf.call(p.children, this);
})
//Insert child at index
utils.proto(Element, 'insertChildAt', function(el, i) {
if(i<0) i=0; if(i >= this.children.length) this.appendChild(el);
else this.insertBefore(el, this.children[i]);
})
//Get element bounding rect as UtilRect object
utils.boundingRect=e => new UtilRect(e.getBoundingClientRect());
utils.innerRect=e => {
let r=e.getBoundingClientRect(), s=getComputedStyle(e);
return new UtilRect(r.top+parseFloat(s.paddingTop)+parseFloat(s.borderTopWidth),
r.bottom-parseFloat(s.paddingBottom)-parseFloat(s.borderBottomWidth),
r.left+parseFloat(s.paddingLeft)+parseFloat(s.borderLeftWidth),
r.right-parseFloat(s.paddingRight)-parseFloat(s.borderRightWidth));
};
utils.define(Element.prototype,'boundingRect',function() {return utils.boundingRect(this)});
utils.define(Element.prototype,'innerRect',function() {return utils.innerRect(this)});
//No idea why this isn't built-in, but it's not
Math.cot = x => 1/Math.tan(x);
//Check if string, array, or other object is empty
utils.isBlank = s => {
if(s == null) return true;
if(typeof s == 'string') return !/\S/.test(s);
if(typeof s == 'object') {
if(typeof s.length == 'number') return s.length === 0;
return Object.keys(s).length === 0;
}
return false;
}
//Finds first empty (undefined) slot in array
utils.firstEmpty = arr => {
const len = arr.length;
for(let i=0; i<len; ++i) if(arr[i] == null) return i;
return len;
}
//Like 'firstEmpty', but uses letters a-Z instead
utils.firstEmptyChar = obj => {
const keys = Object.keys(obj), len = keys.length;
for(let i=0; i<len; ++i) if(obj[keys[i]] == null) return keys[i];
return utils.numToChar(len);
}
//Converts a number into letters (upper and lower) from a to Z
utils.numToChar = n => {
if(n<=25) return String.fromCharCode(n+97);
else if(n>=26 && n<=51) return String.fromCharCode(n+39);
let mVal, fVal;
if(n<2756) { mVal=rstCount(Math.floor(n/52)-1,52); fVal=rstCount(n,52); }
else if(n<143364) { mVal=rstCount(Math.floor((n-52)/2704)-1,52); fVal=rstCount(n-52,2704)+52; }
else if(n<7454980) { mVal=rstCount(Math.floor((n-2756)/140608)-1,52); fVal=rstCount(n-2756,140608)+2756; }
else return false; //More than "ZZZZ"? No. Just, no
return utils.numToChar(mVal)+utils.numToChar(fVal);
}
//Use this to reset your counter each time 'maxVal' is reached
function rstCount(val, maxVal) { while(val >= maxVal) val -= maxVal; return val; }
//Semi-recursively merges two (or more) objects, giving the last precedence
//If both objects contain a property at the same index, and both are Arrays/Objects, they are merged
utils.merge = function(o/*, src1, src2...*/) {
for(let a=1,al=arguments.length,n,oP,nP; a<al; ++a) {
n = arguments[a]; for(let k in n) {
oP = o[k]; nP = n[k]; if(oP && nP) { //Conflict
if(oP.length >= 0 && nP.length >= 0) { //Both Array-like
for(let i=0,l=nP.length,ofs=oP.length; i<l; ++i) oP[i+ofs] = nP[i]; continue;
} else if(typeof oP == 'object' && typeof nP == 'object') { //Both Objects
for(let pk in nP) oP[pk] = nP[pk]; continue;
}
}
o[k] = nP;
}
}
return o;
}
//Keeps value within max/min bounds. Also handles NaN or null
utils.bounds = (n, min=0, max=1) => {
if(!(n>=min)) return min; if(!(n<=max)) return max; return n;
}
//'Normalizes' a value so that it ranges from min to max, but unlike utils.bounds,
//this function retains input's offset. This can be used to normalize angles
utils.norm = utils.normalize = (n, min=0, max=1) => {
const c = Math.abs(max-min);
if(n < min) while(n < min) n += c; else while(n >= max) n -= c;
return n;
}
//Finds and removes all instances of 'rem' contained within s
utils.cutStr = (s, rem) => {
let fnd; while((fnd=s.indexOf(rem)) != -1) {
s = s.slice(0, fnd)+s.slice(fnd+rem.length);
}
return s;
}
//Cuts text out of 'data' from first instance of 'startString' to next instance of 'endString'
//(data,startString,endString[,index[,searchStart]])
//index: Optional object. index.s and index.t will be set to start and end indexes
utils.dCut = (d, ss, es, sd, st) => {
let is = d.indexOf(ss,st?st:undefined)+ss.length, it = d.indexOf(es,is);
if(sd) sd.s=is,sd.t=it; return (is < ss.length || it <= is)?'':d.slice(is,it);
}
utils.dCutToLast = (d, ss, es, sd, st) => {
let is = d.indexOf(ss,st?st:undefined)+ss.length, it = d.lastIndexOf(es);
if(sd) sd.s=is,sd.t=it; return (is < ss.length || it <= is)?'':d.slice(is,it);
}
utils.dCutLast = (d, ss, es, sd, st) => {
let is = d.lastIndexOf(ss,st?st:undefined)+ss.length, it = d.indexOf(es,is);
if(sd) sd.s=is,sd.t=it; return (is < ss.length || it <= is)?'':d.slice(is,it);
}
//Given CSS property value 'prop', returns object with
//space-separated values from the property string
utils.parseCSS = prop => {
let pArr={}, pKey="", keyNum=0; prop=prop.trim();
function parseInner(str) {
if(str.indexOf(',') !== -1) {
const arr = utils.clean(str.split(','));
for(let i=0, l=arr.length; i<l; ++i) arr[i]=arr[i].trim();
return arr;
}
return str.trim();
}
while(prop.length > 0) {
if(prop[0] == '(' && prop.indexOf(')') !== -1 && pKey) {
let end=prop.indexOf(')'), pStr=prop.slice(1,end);
pArr[pKey] = parseInner(pStr);
pKey = ""; prop = prop.slice(end+1);
} else if(prop.search(/[#!\w]/) == 0) {
if(pKey) pArr[keyNum++] = pKey;
let end=prop.search(/[^#!\w-%]/); if(end==-1) end=prop.length;
pKey = prop.slice(0,end); prop = prop.slice(end);
} else prop = prop.slice(1);
}
if(pKey) pArr[keyNum] = pKey; return pArr;
}
//Rebuilds CSS string from a parseCSS object
utils.buildCSS = propArr => {
const keyArr=Object.keys(propArr), l=keyArr.length; let pStr='', i=0;
while(i<l) {
const k=keyArr[i], v=propArr[keyArr[i]]; ++i;
if(0<=Number(k)) pStr+=v+' '; else pStr+=`${k}(${v}) `;
}
return pStr.slice(0,-1);
}
function defaultStyle() {
const ss=document.styleSheets;
for(let s=0,j=ss.length; s<j; ++s) try { ss[s].cssRules; return ss[s]; } catch(e) {}
let ns=utils.mkEl('style',document.head); utils.addText(ns,''); return ns.sheet;
}
function toKey(k) {
return k.replace(/[A-Z]/g, s => '-'+s.toLowerCase());
}
//Create a CSS class and append it to the current document. Fill 'propList' object
//with key/value pairs representing the properties you want to add to the class
utils.addClass = (className, propList) => {
const style = defaultStyle(), keys = Object.keys(propList); let str='';
for(let i=0,l=keys.length; i<l; ++i) str += toKey(keys[i])+":"+propList[keys[i]]+";";
style.addRule("."+className,str);
}
//Create a CSS selector and append it to the current document
utils.addId = (idName, propList) => {
const style = defaultStyle(), keys = Object.keys(propList); let str='';
for(let i=0,l=keys.length; i<l; ++i) str += toKey(keys[i])+":"+propList[keys[i]]+";";
style.addRule("#"+idName,str);
}
//Create a CSS keyframe and append it to the current document
utils.addKeyframe = (name, content) => {
defaultStyle().addRule("@keyframes "+name,content);
}
//Remove a specific css selector (including the '.' or '#') from all stylesheets in the current document
utils.removeSelector = name => {
for(let s=0,style,rList,j=document.styleSheets.length; s<j; ++s) {
style = document.styleSheets[s]; try { rList=style.cssRules; } catch(e) { continue; }
for(let key in rList) if(rList[key].constructor.name == "CSSStyleRule"
&& rList[key].selectorText == name) style.deleteRule(key);
}
}
//Converts HEX color to 24-bit RGB
utils.hexToRgb = hex => {
const c = parseInt(hex.slice(1),16);
return [(c >> 16) & 255, (c >> 8) & 255, c & 255];
}
//By mjackson @ GitHub
utils.rgbToHsl = (r,g,b) => {
r /= 255, g /= 255, b /= 255;
let max=Math.max(r,g,b), min=Math.min(r,g,b), h,s,l=(max+min)/2;
if(max===min) h=s=0; //Achromatic
else {
let d=max-min;
s=l>.5 ? d/(2-max-min) : d/(max+min);
switch(max) {
case r: h=(g-b)/d + (g<b?6:0); break;
case g: h=(b-r)/d + 2; break;
case b: h=(r-g)/d + 4;
}
h /= 6;
}
return [h*360, s*100, l*100];
}
//Generates random integer from min to max
utils.rand = (min, max, res, ease) => {
res=res||1; max*=res,min*=res; let r=Math.random();
return Math.round((ease?ease(r):r)*(max-min)+min)/res;
}
//Parses a url query string into an Object
utils.fromQuery = str => {
if(str.startsWith('?')) str=str.slice(1);
function parse(pl, pairs) {
const pair=pairs[0], spl=pair.indexOf('='),
key=decodeURIComponent(pair.slice(0,spl)),
val=decodeURIComponent(pair.slice(spl+1));
//Handle multiple params of the same name
if(pl[key] == null) pl[key] = val;
else if(Array.isArray(pl[key])) pl[key].push(val);
else pl[key] = [pl[key],val];
return pairs.length===1 ? pl : parse(pl, pairs.slice(1));
}
return str ? parse({}, str.split('&')) : {};
}
//Converts an object into a url query string
utils.toQuery = obj => {
let str='',key,val;
if(typeof obj !== 'object') return encodeURIComponent(obj);
for(key in obj) {
val = obj[key];
if(typeof val === 'object' && val != null) val = JSON.stringify(val);
str += '&'+key+'='+encodeURIComponent(val);
}
return str.slice(1);
}
//Various methods of centering objects using JavaScript
//obj: Object to center
//only: 'x' for only x axis centering, 'y' for only y axis, null for both
//type: 'trans' or null, modes explained below
utils.center = (obj, only, type) => {
let os=obj.style;
if(type == 'trans') { //Responsive, doesn't create container
if(!os.position) os.position='absolute';
let trans=utils.cutStr(os.transform, 'translateX(-50%)');
trans=utils.cutStr(trans, 'translateY(-50%)');
if(!only || only == 'x') os.left='50%', trans+='translateX(-50%)';
if(!only || only == 'y') os.top='50%', trans+='translateY(-50%)';
if(trans) os.transform=trans;
} else { //New flexbox method
let cont=utils.mkDiv(obj.parentNode, null, {display:'flex', top:0, left:0});
cont.appendChild(obj), cont=cont.style;
if(!only || only == 'x') cont.justifyContent='center', cont.width='100%';
if(!only || only == 'y') cont.alignItems='center',
cont.height='100%', cont.position='absolute';
}
}
//Loads a file and returns its contents using HTTP GET
//Callback parameters: (err, data, req)
//err: Non-zero on error. Standard HTTP error codes
//data: Response text
//req: Full XMLHttpRequest object
//meth: Optional HTTP method, default is GET
//body: Optional body content
//hd: Optional header list
utils.loadAjax = (path, cb, meth, body, hd) => {
let R; try {R=new XMLHttpRequest()} catch(e) {return cb(e)}
if(hd) for(let k in hd) R.setRequestHeader(k,hd[k]);
R.open(meth||'GET',path); R.onreadystatechange = () => {
let s=R.status||-1;
if(R.readyState == R.DONE) cb(s==200?0:s, R.response, R);
}
R.send(body||undefined);
}
//Good fallback for loadAjax. Loads a file at the address via HTML object tag
//Callback is fired with either received data, or 'false' if unsuccessful
utils.loadFile = (path, cb, timeout) => {
const obj = utils.mkEl('object', document.body, null, {position:'fixed', opacity:0});
obj.data = path;
let tmr = setTimeout(() => {
obj.remove(); tmr = null; cb(false);
}, timeout||4000);
obj.onload = () => {
if(!tmr) return; clearTimeout(tmr);
cb(obj.contentDocument.documentElement.outerHTML);
obj.remove();
}
}
//Loads a file at the address from a JSONP-enabled server. Callback
//is fired with either received data, or 'false' if unsuccessful
utils.loadJSONP = (path, cb, timeout) => {
const script = utils.mkEl('script', document.head), id = utils.firstEmptyChar(utils.lJSONCall);
script.type = 'application/javascript';
script.src = path+(path.indexOf('?')==-1?'?':'&')+'callback=utils.lJSONCall.'+id;
let tmr = setTimeout(() => { delete utils.lJSONCall[id]; cb(false); }, timeout||4000);
utils.lJSONCall[id] = data => {
if(tmr) clearTimeout(tmr); delete utils.lJSONCall[id]; cb(data);
}
document.head.removeChild(script);
}; utils.lJSONCall = [];
//Downloads a file from a link
utils.dlFile = (fn,uri) => {
return fetch(uri).then(r => { if(r.status != 200) throw "Code "+r.status; return r.blob(); })
.then(b => { utils.dlData(fn,b); });
}
//Downloads a file generated from a Blob or ArrayBuffer
utils.dlData = (fn,d) => {
let o,e=utils.mkEl('a',document.body,null,{display:'none'});
if(typeof d=='string') o=d; else {
if(!(d instanceof Blob)) d=Blob(d); o=URL.createObjectURL(d);
}
e.href=o,e.download=fn; e.click(); e.remove(); URL.revokeObjectURL(o);
}
//Converts from radians to degrees, so you can work in degrees
//Function by: The a**hole who invented radians
utils.deg = rad => rad*180/Math.PI;
//Converts from degrees to radians, so you can convert back for given stupid library
//Function by: The a**hole who invented radians
utils.rad = deg => deg*Math.PI/180;
//Pecacheu's ultimate unit translation formula!
//This Version -- Bounds Checking: NO, Rounding: NO, Max/Min Switching: NO, Easing: YES
utils.map = (input, minIn, maxIn, minOut, maxOut, ease) => {
let i=(input-minIn)/(maxIn-minIn); return ((ease?ease(i):i)*(maxOut-minOut))+minOut;
}
//setTimeout but async
utils.delay = ms => new Promise(r => setTimeout(r,ms));
UtilRect = function(t,b,l,r) {
if(!(this instanceof UtilRect)) return new UtilRect(t,b,l,r);
const f=Number.isFinite; let tt=0,bb=0,ll=0,rr=0;
utils.define(this,'x', ()=>ll, v=>{f(v)?(rr+=v-ll,ll=v):0});
utils.define(this,'y', ()=>tt, v=>{f(v)?(bb+=v-tt,tt=v):0});
utils.define(this,'top', ()=>tt, v=>{tt=f(v)?v:0});
utils.define(this,['bottom','y2'], ()=>bb, v=>{bb=f(v)?v:0});
utils.define(this,'left', ()=>ll, v=>{ll=f(v)?v:0});
utils.define(this,['right','x2'], ()=>rr, v=>{rr=f(v)?v:0});
utils.define(this,['width','w'], ()=>rr-ll, v=>{rr=v>=0?ll+v:0});
utils.define(this,['height','h'], ()=>bb-tt, v=>{bb=v>=0?tt+v:0});
utils.define(this,'centerX', ()=>ll/2+rr/2);
utils.define(this,'centerY', ()=>tt/2+bb/2);
if(t instanceof DOMRect || t instanceof UtilRect) tt=t.top, bb=t.bottom, ll=t.left, rr=t.right;
else tt=t, bb=b, ll=l, rr=r;
}
//Check if rect contains point, other rect, or Element
utils.proto(UtilRect, 'contains', function(x,y) {
if(x instanceof Element) return this.contains(x.boundingRect);
if(x instanceof UtilRect) return x.x >= this.x && x.x2 <= this.x2 && x.y >= this.y && x.y2 <= this.y2;
return x >= this.x && x <= this.x2 && y >= this.y && y <= this.y2;
})
//Check if rect overlaps rect or Element
utils.proto(UtilRect, 'overlaps', function(r) {
if(r instanceof Element) return this.overlaps(r.boundingRect);
if(!(r instanceof UtilRect)) return 0; let x,y;
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;
else x = r.x >= this.x && r.x <= this.x2 || r.x2 >= this.x && r.x2 <= this.x2;
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;
else y = r.y >= this.y && r.y <= this.y2 || r.y2 >= this.y && r.y2 <= this.y2;
return x&&y;
})
//Get distance from this rect to point, other rect, or Element
utils.proto(UtilRect, 'dist', function(x,y) {
if(x instanceof Element) return this.dist(x.boundingRect); let n=(x instanceof UtilRect);
y=Math.abs((n?x.centerY:y)-this.centerY), x=Math.abs((n?x.centerX:x)-this.centerX);
return Math.sqrt(x*x+y*y);
})
//Expand (or contract if negative) a UtilRect by num of pixels
//Useful for using UtilRect objects as element hitboxes. Returns self for chaining
utils.proto(UtilRect, 'expand', function(by) {
this.top -= by; this.left -= by; this.bottom += by;
this.right += by; return this;
})
})();
//JavaScript Easing Library by: https://github.com/gre & https://gizma.com/easing
//t should be between 0 and 1
const Easing = {
//no easing, no acceleration
linear:t => t,
//accelerating from zero velocity
easeInQuad:t => t*t,
//decelerating to zero velocity
easeOutQuad:t => t*(2-t),
//acceleration until halfway, then deceleration
easeInOutQuad:t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
//accelerating from zero velocity
easeInCubic:t => t*t*t,
//decelerating to zero velocity
easeOutCubic:t => (--t)*t*t+1,
//acceleration until halfway, then deceleration
easeInOutCubic:t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
//accelerating from zero velocity
easeInQuart:t => t*t*t*t,
//decelerating to zero velocity
easeOutQuart:t => 1-(--t)*t*t*t,
//acceleration until halfway, then deceleration
easeInOutQuart:t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
//accelerating from zero velocity
easeInQuint:t => t*t*t*t*t,
//decelerating to zero velocity
easeOutQuint:t => 1+(--t)*t*t*t*t,
//acceleration until halfway, then deceleration
easeInOutQuint:t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t
}
//Node.js compat
if(_uNJS && !global.utils) {
let os = import('os').then(m => os=m);
//Get list of system IPs
utils.getIPs = () => {
const ip=[], fl=os.networkInterfaces();
for(let k in fl) fl[k].forEach(f => {
if(!f.internal && f.family == 'IPv4' && f.mac != '00:00:00:00:00:00' && f.address) ip.push(f.address);
});
return ip.length?ip:0;
}
//Get system OS, arch, and CPU info
utils.getOS = () => {
let sysOS, arch, cpu;
switch(os.platform()) {
case 'win32': sysOS="Windows"; break;
case 'darwin': sysOS="MacOS"; break;
case 'linux': sysOS="Linux"; break;
default: sysOS=os.platform();
}
switch(os.arch()) {
case 'ia32': arch="32-bit"; break;
case 'x64': arch="64-bit"; break;
case 'arm': arch="ARM"; break;
default: arch=os.arch();
}
cpu=os.cpus()[0].model;
return [sysOS, arch, cpu];
}
global.utils=utils;
global.UtilRect=UtilRect;
global.Easing=Easing;
}