/**
* 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);
});
}