Source: Shorthand.js

/**
 * Returns all own properties of an object as an Array of name<->value pairs
 * @method
 * @instance
 * @memberOf Array
 * @name asEntries
 */

if(!Array.prototype.hasOwnProperty("asEntries")){
    Object.defineProperty(Array.prototype,"asEntries",{enumerable:false,value:function asEntries() {
        return Object.fromEntries(this);
    }});
}

/**
 * Checks if an array contains the given parameter
 * @method
 * @instance
 * @memberOf Array
 * @name contains
 * @returns {boolean}
 * @param b {Object} Value to search for
 */
if(!Array.prototype.hasOwnProperty("contains")){
    Object.defineProperty(Array.prototype,"contains",{enumerable:false,value:function contains(b) {
        return this.indexOf(b)>=0;
    }});
}

/**
 * Returns all distinct values of an array
 * @method
 * @instance
 * @memberOf Array
 * @name distinct
 * @returns {Array}
 * @param serialize {boolean} If set will return distinct JSON-serialized (deserialized again after distinct)
 */
if(!Array.prototype.hasOwnProperty("distinct")){
    Object.defineProperty(Array.prototype,"distinct",{enumerable:false,value:function distinct(serialize) {
        if(serialize){
            return this.map(a=>a.toJson()).distinct().map(a=>JSON.stringify(a));
        }else{
            return this.reduce((p,a)=>{
                if(p.indexOf(a)<0)
                    p.push(a);
                return p;
            },[]);
        }
    }});
}

/**
 * Returns the first element of an array with optional filtering and default value
 * @method
 * @instance
 * @memberOf Array
 * @name getFirst
 * @returns {Object|undefined}
 * @param filter {function} Optional callback to filter the array with before returning the first entry
 * @param default {Object} Optional return value if no entry in result.
 */
if(!Array.prototype.hasOwnProperty("getFirst")){
    Object.defineProperty(Array.prototype,"getFirst",{enumerable:false,value:function getFirst(filter,def) {
        if(filter)
            var r=this.filter(filter);
        else
            var r=this;
    
        return r.length?r[0]:def;
    }});
}

/**
 * Returns the last element of an array with optional filtering and default value
 * @method
 * @instance
 * @memberOf Array
 * @name getLast
 * @returns {Object|undefined}
 * @param filter {function} Optional callback to filter the array with before returning the last entry
 * @param default {Object} Optional return value if no entry in result.
 */
if(!Array.prototype.hasOwnProperty("getLast")){
    Object.defineProperty(Array.prototype,"getLast",{enumerable:false,value:function getLast(filter,def) {
        if(filter)
            var r= this.filter(filter);
        else
            var r=this;
        return r.length?r[r.length-1]:def;
    }});
}

/**
 * Groups an array into a nested tree meaning for each param the array that would be an array turns itself into a value:results dictionary
 * @method
 * @instance
 * @memberOf Array
 * @name groupBy
 * @returns {Object}
 * @param grouper1 {function|string} Either a callback to be used for grouping or the name of a field
 * @param grouper2 {function|string} Either a callback to be used for grouping or the name of a field to be performed on each result of grouper1
 * @param grouperN {function|string} Either a callback to be used for grouping or the name of a field to be performed on each result of previous grouper
 */
if(!Array.prototype.hasOwnProperty("groupBy")){
  Object.defineProperty(Array.prototype,"groupBy",{enumerable:false,value:function groupBy(args) {
    args=[...arguments];
    
    var data=Object.groupBy(this,typeof(args[0])=="function"?args[0]:function(field,line){ return line[field]; }.bind(null,args[0]) );
    if(args.length>1)
      for(var i in data)
          data[i]=data[i].groupBy.apply(data[i],args.slice(1));
    return data;
  }});
}

/**
 * Returns an array containing array with 0 being the item of this that was matches and 1 being an array of all entries of arr that returned true when passed to match
 * @method
 * @instance
 * @memberOf Array
 * @name joinLeft
 * @returns {Array}
 * @param secondArray {Array} Array to be joined
 * @param matcher {function} A callback to be called with each line of self (with params(selfLine,secondArrayLine)) to determine a match
 */
if(!Array.prototype.hasOwnProperty("joinLeft")){
    Object.defineProperty(Array.prototype,"joinLeft",{enumerable:false,value:function joinLeft(arr,match) {
        return this.map(a=>[a,arr.filter(b=>match(a,b))]);
    }});
}

/**
 * Returns the highest value in the array
 * @method
 * @instance
 * @memberOf Array
 * @name max
 * @returns {number}
 */

if(!Array.prototype.hasOwnProperty("max")){
    Object.defineProperty(Array.prototype,"max",{enumerable:false,max:function max() {
        return Math.max(...this);
    }});
}

/**
 * Returns the lowest value in the array
 * @method
 * @instance
 * @memberOf Array
 * @name min
 * @returns {number}
 */

if(!Array.prototype.hasOwnProperty("min")){
    Object.defineProperty(Array.prototype,"max",{enumerable:false,min:function min() {
        return Math.min(...this);
    }});
}

/**
 * Returns the sum of all numbers in the array
 * @method
 * @instance
 * @memberOf Array
 * @name sum
 * @returns {number}
 */

Object.defineProperty(Array.prototype,"sum",{value:function sum() {
    return this.reduce((p,n)=>p+n,0);
}});
/**
 * Returns an XLSX PlainZip with the given name containing a single sheet with the current Array interpreted as 2D string array as content
 * @method
 * @instance
 * @memberOf Array
 * @name toXlsx
 * @param [name] {string} Name of the file to be offered for download. Defaults to a GUID otherwise.
 * @returns {PlainZip}
 */

Object.defineProperty(Array.prototype,"toXlsx",{value:function toXlsx(name) {
    var doc = new DOMParser().parseFromString("<root />", "application/xml");
    var root=doc.documentElement;
    for(var i=0;i<this.length;i++){
        var row=doc.createElement("row");
        for(var j=0;j<this[i].length;j++){
            var cell='<c t="inlineStr"><is><t></t></is></c>'.asXml();
            cell.querySelector('t').textContent=""+this[i][j];
            row.appendChild(cell);
        }
        root.appendChild(row);
    }
    var content=doc.documentElement.innerHTML;
    var zip=new PlainZip(name,{
        '_rels/.rels':'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="workbook.xml"/></Relationships>',
        '_rels/workbook.xml.rels':'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheet.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>',
        '[Content_Types].xml':'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/worksheet.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/></Types>',
        'workbook.xml':'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Index" sheetId="1" r:id="rId1"/></sheets></workbook>',
        'worksheet.xml':'<?xml version="1.0" encoding="utf-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetData>'+content+'</sheetData></worksheet>'
    });
    zip.mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    return zip;
}});
/**
 * Returns a string representing the given date. Supported are yyyy(Year4), yy(Year2), MM(Month2), dd(Date2), d(Date), HH(Hour2), mm(Minutes2), ss(Seconds2)
 * @method
 * @instance
 * @memberOf Date
 * @name format
 * @param formatString {string} formatString to use for laying out the date
 * @returns {string}
 */
Date.prototype.format=function(str){
	return str.
		replace("yyyy",this.getFullYear().toString().padStart(4,'0')).
		replace("yy",this.getFullYear().toString().padStart(2,'0').substr(-2)).
		replace("MM",(this.getMonth()+1).toString().padStart(2,'0')).
		replace("dd",this.getDate().toString().padStart(2,'0')).
		replace("d",this.getDate().toString()).
		replace("HH",this.getHours().toString().padStart(2,'0')).
		replace("mm",this.getMinutes().toString().padStart(2,'0')).
		replace("ss",this.getSeconds().toString().padStart(2,'0'))
		;
};
/**
 * Returns an element matching the selector if the last part of the selector starts with #
 * Otherwise returns all elements matching the selector in the subtree as array.
 * @method
 * @instance
 * @memberOf Document
 * @name $
 * @param selector {string} Selector to search for
 * @returns {Array|Element|null}
*/
Document.prototype.$=function(sel){
  return /#[^\s>~|]+$/.exec(sel)?
    this.querySelector(sel):
    [...this.querySelectorAll(sel)]
};

/**
 * Returns an element matching the selector if the last part of the selector starts with #
 * Otherwise returns all elements matching the selector in the subtree as array.
 * @method
 * @instance
 * @memberOf Element
 * @name $
 * @param selector {string} Selector to search for
 * @returns {Array|Element|null}
*/
Element.prototype.$=Document.prototype.$;
/**
 * Returns either the element matching the selector or if it doesn't a promise to be fullfiled when an element matching the selector appears
 * @method
 * @instance
 * @memberOf Element
 * @name querySelectorAsync
 * @param selector {string} Selector to search for
 * @returns {Element|Promise<Element>}
*/
Element.prototype.querySelectorAsync=async function(selector){
	var initial=this.querySelector(selector);
	if(initial)
	  return initial;
	
	return new Promise(function(done,abort){
		new MutationObserver(function(selector,mutationList, observer){
			var r=this.querySelector(selector);
			if(r){
				observer.disconnect();
				done(r);
			}
		}.bind(this,selector)).observe(this, { attributes: true, childList: true, subtree: true });
	}.bind(this));
};

/**
 * Returns either the element matching the selector or if it doesn't a promise to be fullfiled when an element matching the selector appears
 * @method
 * @instance
 * @memberOf Document
 * @name querySelectorAsync
 * @param selector {string} Selector to search for
 * @returns {Element|Promise<Element>}
*/
Document.prototype.querySelectorAsync=async function(selector){
	return this.documentElement.querySelectorAsync(selector);
};
/**
 * Fetches a url as image asynchronously
 * @memberOf Image
 * @name fetch
 * @param path {string} Url of the image to be fetched
 * @returns {Promise<Image>}
 */

Image.fetch=function(path){
    var img=document.createElement("img");
    return new Promise(function(resolve,reject){
        img.onload=resolve.bind(null,img);
        img.onerror=reject.bind(null,img);
        img.src=path;
    });
}
/**
 * Returns a structured clone of the current object
 * @method
 * @instance
 * @memberOf Object
 * @name clone
 * @returns {Object}
 */

Object.defineProperty(Object.prototype,"clone",{value:function clone(name,property){
  return window.structuredClone(this);
}});


/**
 * Calls defineProperty on the current object and returns the current object
 * @method
 * @instance
 * @memberOf Object
 * @name defineProperty
 * @param name {string} Name of the property
 * @param propertyDescriptor {Object} Configuration for the property
 * @returns {Object}
 */

Object.defineProperty(Object.prototype,"defineProperty",{value:function defineProperty(name,property){
  Object.defineProperty(this,name,typeof(property)=="object"?property:{value:property});
  return this;
}});

/**
 * Calls freeze on the current object to turn it write protcted and returns the current object
 * @method
 * @instance
 * @memberOf Object
 * @name freeze
 * @returns {Object}
 */

Object.defineProperty(Object.prototype,"freeze",{value:function freeze(){
  return Object.freeze(this);
}});
/**
 * Create a new object containing only the fields given
 * @method
 * @instance
 * @memberOf Object
 * @name getProperties
 * @param propName1 {string} Property name
 * @param propName2 {string} Property name
 * @param propNameN {string} Property name
 * @returns {Object}
 */

Object.defineProperty(Object.prototype,"getProperties",{value:function getProperties(args){
  var args=[...arguments];
  return Object.fromEntries(Object.entries(this).filter(a=>args.contains(a[0])));
}});
/**
 * Returns all own keys of object
 * @method
 * @instance
 * @memberOf Object
 * @name keys
 * @returns {Array}
 */

Object.defineProperty(Object.prototype,"keys",{value:function keys(){
  return Object.keys(this);
}});
/**
 * Returns all own properties as Array of 2 entry name,value arrays
 * @method
 * @instance
 * @memberOf Object
 * @name toEntries
 * @returns {Array}
 */
Object.defineProperty(Object.prototype,"toEntries",{value:function toEntries(){
  return Object.entries(this);
}});
/**
 * Json stringifies current object
 * @method
 * @instance
 * @memberOf Object
 * @name toJson
 * @param replacer {function} Replacer
 * @param Spacer {string} Space
 * @returns {string}
 */
Object.defineProperty(Object.prototype,"toJson",{value:function toJson(repl,spc){
  return JSON.stringify(this,repl,spc);
}});
/**
 * Returns all own values of object
 * @method
 * @instance
 * @memberOf Object
 * @name values
 * @returns {Array}
 */

Object.defineProperty(Object.prototype,"values",{value:function values(){
  return Object.values(this);
}});
//Refactoring of https://github.com/pwasystem/zip/

/**
 * Uncompressed zip file
 *
 * @param nameOrData Either basename of zip file if no initial data or just the data if no specific name is required
 * @param data Data if a basename is required
 * @constructor
 */
function PlainZip(nameOrData,data){
    if(typeof(nameOrData)=="object"){
        data=nameOrData;
        nameOrData="";
    }
    this.name = nameOrData||this._guid();
    this.zip = [];
    this.file = [];
    this.finalized=false;
    if(data)
        this.addData(data);
}

PlainZip.prototype._guid=()=>Array.prototype.slice.call(crypto.getRandomValues(new Uint8Array(16))).map(a=>("0"+a.toString(16)).slice(-2)).join("");
PlainZip.prototype._dec2bin=(dec, size)=>dec.toString(2).padStart(size,'0');
PlainZip.prototype._str2hex= str=>[...new TextEncoder().encode(str)].map(x=>x.toString(16).padStart(2,'0'));
PlainZip.prototype._hex2buf= hex=>new Uint8Array(hex.split(' ').map(x=>parseInt(x,16)));
PlainZip.prototype._bin2hex= bin=>(parseInt(bin.slice(8),2).toString(16).padStart(2,'0')+' '+parseInt(bin.slice(0,8),2).toString(16).padStart(2,'0'));

PlainZip.prototype._reverse= hex=>{
    let hexArray=[];
    for(let i=0;i<hex.length;i=i+2)hexArray[i]=hex[i]+''+hex[i+1];
    return hexArray.filter((a)=>a).reverse().join(' ');
};

PlainZip.prototype._crc32=function(r){
    let a,o,c,n;
    for(o=[],c=0;c<256;c++){
        a=c;
        for(let f=0;f<8;f++)
            a=1&a?3988292384^a>>>1:a>>>1;
        o[c]=a;
    }
    for(n=-1,t=0;t<r.length;t++)n=n>>>8^o[255&(n^r[t])];
    return this._reverse(((-1^n)>>>0).toString(16).padStart(8,'0'));
};

/**
 * Adds data to the zip file
 * @method
 * @instance
 * @memberOf PlainZip
 * @name addData
 * @param arrayOfData {Array|Object} Data must be an array of objects with each object having string:name, string:data or Uint8Array data and unixTimestamp:modified. Alternatively a name/value dictionary may be given.
 * @returns {undefined}
 */
PlainZip.prototype.addData=function(arrayOfData){
    if(this.finalized)
        throw new Error("File already finalized");

    var enc=new TextEncoder();

    if(arrayOfData instanceof Array) {
        for (let i = 0; i < arrayOfData.length; i++) {
            let uint = typeof (arrayOfData[i].data) == "string" ? enc.encode(arrayOfData[i].data) : arrayOfData[i].data;
            uint.name = arrayOfData[i].name;
            uint.modTime = arrayOfData[i].lastModified || +new Date();
            uint.fileUrl = `${arrayOfData[i].name}`;
            this.zip[uint.fileUrl] = uint;
        }
    }else{
        for (let i in arrayOfData) {
            let uint = typeof (arrayOfData[i]) == "string" ? enc.encode(arrayOfData[i]) : arrayOfData[i];
            uint.name = i;
            uint.modTime = +new Date();
            uint.fileUrl = `${i}`;
            this.zip[uint.fileUrl] = uint;
        }
    }
};

/**
 * Locks the Zip file and returns the byte stream
 * @method
 * @instance
 * @memberOf PlainZip
 * @name finalize
 * @returns {Uint8Array}
 */
PlainZip.prototype.finalize=function(){
    if(this.finalized)
        return this.file;

    let count=0;
    let centralDirectoryFileHeader='';
    let directoryInit=0;
    let offSetLocalHeader='00 00 00 00';
    let zip=this.zip;
    for(const name in zip){
        if(!zip.hasOwnProperty(name))
            continue;
        let modTime=(()=>{
            const lastMod=new Date(zip[name].modTime);
            const hour=this._dec2bin(lastMod.getHours(),5);
            const minutes=this._dec2bin(lastMod.getMinutes(),6);
            const seconds=this._dec2bin(Math.round(lastMod.getSeconds()/2),5);
            const year=this._dec2bin(lastMod.getFullYear()-1980,7);
            const month=this._dec2bin(lastMod.getMonth()+1,4);
            const day=this._dec2bin(lastMod.getDate(),5);
            return this._bin2hex(`${hour}${minutes}${seconds}`)+' '+this._bin2hex(`${year}${month}${day}`);
        })();
        let crc=this._crc32(zip[name]);
        let size=this._reverse(parseInt(zip[name].length).toString(16).padStart(8,'0'));
        let nameFile=this._str2hex(zip[name].fileUrl).join(' ');
        let nameSize=this._reverse(zip[name].fileUrl.length.toString(16).padStart(4,'0'));
        let fileHeader=`50 4B 03 04 14 00 00 00 00 00 ${modTime} ${crc} ${size} ${size} ${nameSize} 00 00 ${nameFile}`;
        let fileHeaderBuffer=this._hex2buf(fileHeader);
        directoryInit=directoryInit+fileHeaderBuffer.length+zip[name].length;
        centralDirectoryFileHeader=`${centralDirectoryFileHeader}50 4B 01 02 14 00 14 00 00 00 00 00 ${modTime} ${crc} ${size} ${size} ${nameSize} 00 00 00 00 00 00 01 00 20 00 00 00 ${offSetLocalHeader} ${nameFile} `;
        offSetLocalHeader=this._reverse(directoryInit.toString(16).padStart(8,'0'));
        this.file.push(fileHeaderBuffer,new Uint8Array(zip[name]));
        count++;
    }
    centralDirectoryFileHeader=centralDirectoryFileHeader.trim();
    let entries=this._reverse(count.toString(16).padStart(4,'0'));
    let dirSize=this._reverse(centralDirectoryFileHeader.split(' ').length.toString(16).padStart(8,'0'));
    let dirInit=this._reverse(directoryInit.toString(16).padStart(8,'0'));
    let centralDirectory=`50 4b 05 06 00 00 00 00 ${entries} ${entries} ${dirSize} ${dirInit} 00 00`;
    this.file.push(this._hex2buf(centralDirectoryFileHeader),this._hex2buf(centralDirectory));
    return this.file;
};


PlainZip.prototype.mime='application/octet-stream';

/**
 * Locks the Zip file and returns an object url
 * @method
 * @instance
 * @memberOf PlainZip
 * @name getObjectURL
 * @returns {string}
 */
PlainZip.prototype.getObjectURL=function(){
    this.finalize();
    return URL.createObjectURL(new Blob([...this.file],{type:this.mime}));
};

/**
 * Locks the Zip file and initiates a download
 * @method
 * @instance
 * @memberOf PlainZip
 * @name download
 * @param [filename] {string} Filename to be offered for download (overrides previously configured name if set)
 * @returns {undefined}
 */
PlainZip.prototype.download=function(filename){
    let a = document.createElement('a');
    a.href = this.getObjectURL();
    a.download = filename||`${this.name}.zip`;
    a.click();
};
/**
 * Creates a crypto-secure guid
 * @memberOf String
 * @name guid
 * @returns {string}
 */

String.guid = function(){
  return Array.prototype.slice.call(crypto.getRandomValues(new Uint8Array(16))).map(a=>("0"+a.toString(16)).slice(-2)).join("");
};
/**
 * Decodes the current string as base64
 * @method
 * @instance
 * @memberOf String
 * @name asBase64
 * @returns {string}
 */

String.prototype.asBase64=function(){
  return window.atob(this);
};
/**
 * Parses the current string as JSON
 * @method
 * @instance
 * @memberOf String
 * @name asJson
 * @returns {object}
 */

String.prototype.asJson=function(){
    return JSON.parse(this);
};
/**
 * Parses the current string as xml and returns the document xml
 * @method
 * @instance
 * @memberOf String
 * @name asXml
 * @returns {Element}
 */

String.prototype.asXml=function(){
    return (new DOMParser().parseFromString(this, "application/xml")).documentElement;
};
/**
 * Encodes the current string to base64
 * @method
 * @instance
 * @memberOf String
 * @name toBase64
 * @returns {String}
 */

String.prototype.toBase64=function(){
  return window.btoa(this);
};
/**
 * Encodes the current String as HTML
 * @method
 * @instance
 * @memberOf String
 * @name toHtml
 * @returns {string}
 */

String.prototype.toHtml=function(){
    var s=document.createElement("span");
    s.textContent=this;
    return s.innerHTML;
}
/**
 * Encodes the current String as UTF-16 bytes
 * @method
 * @instance
 * @memberOf String
 * @name toUTF16
 * @returns {Uint8Array}
 */

String.prototype.toUTF16=function(){
    var out=new Uint8Array(2+this.length*2);
    out[0]=0xFF;
    out[1]=0xFE;
    for(var i=0;i<this.length;i++){
        var c=this.charCodeAt(i);
        out[2+i*2]=c&0x00ff;
        out[2+i*2+1]=(c&0xff00)>>>8;
    }
    return out;
};
/**
 * Parses the current string as XML and returns the documentElement
 * @method
 * @instance
 * @memberOf String
 * @name toXml
 * @returns {Element}
 */

String.prototype.toXml=function(){
    return (new DOMParser().parseFromString(this, "application/xml")).documentElement;
};
/** @class Array */
/** @class Element */
/** @class Date */
/** @class Document */
/** @class Image */
/** @class Object */
/** @class String */
/**
 * Sleeps a specified number of Milliseconds until fulfillment
 * @name sleep
 * @params time {number} Milliseconds until the promise is fullfilled
 * @returns {Promise}
 */

function sleep(ms){
  return new Promise((ok,err)=>{
    window.setTimeout(ok,ms);
  });
}