mirror of
https://github.com/xevidos/codiad.git
synced 2024-11-13 07:11:14 +01:00
577 lines
No EOL
22 KiB
Text
Executable file
577 lines
No EOL
22 KiB
Text
Executable file
s:22351:"/**
|
|
Author: Abbas Abdulmalik
|
|
Created: ~ May, 2017
|
|
Revised: June 9, 2018
|
|
Original Filename: L.js
|
|
Purpose: a small (but growing) personal re-usable js library for a simple MVC architecture
|
|
Notes: Now qualifyFunction helper doesn't return true for empty arrays (no vacuous truth)
|
|
|
|
Added UploadFiles:
|
|
uploadFiles takes a callback -- progressReporter-- as it FIRST argument (parameter)
|
|
to allow for an optional fourth parameter of an upload path for the server.
|
|
progressReporter will be passed three arguments when called:
|
|
1.) the amount of bytes uploaded so far
|
|
2.) the total size of the file in bytes
|
|
3.) the index of the file in the "array" of files being uploaded
|
|
|
|
Added sortByExtension that alphabetizes an array of strings 'in place' by filename extension
|
|
Added arrayStringMatch that matches a collection of string arrays to a search string.
|
|
later (6-9-2018) included an option for a "maximum array index" to eliminate
|
|
searching irrelevant fields at the end of the array, such as image name and primary key.
|
|
Added loopCall as a 'better' version of setInterval
|
|
Removed L.attributes. It's a reserved word: an object belonging to DOM elements
|
|
Now it uses L.attribs
|
|
Added L.loopCall.stop() so that user can easily stop L.loopCall
|
|
Added L.symDiff for comparing arrays to determine their symmetric difference = conjunctive union =
|
|
exclusive-or
|
|
Restored an updated version of uploadFiles that signals the final file has uploaded
|
|
Added secToMinSec
|
|
Added runQualifiedFunctions that mirrors runQualifiedMethods using different parameters,
|
|
namely: functionQualifiers, model, view, controller
|
|
Added attachNewElement(tagname, id, view). Create new element, gives it an id and
|
|
attaches it to object provided:
|
|
L.attachNewElement(`div`, `picHolder`, view)
|
|
Added createListMixer and scrammbleThis, which depends on createListMixer
|
|
Added sortArrayOfStringArrays, with option of using a "link token" of choice as the 3rd argument
|
|
Added an optional argument for arrayStringMatch for a maximum array index
|
|
to eliminate searching irrelevant fields at the end of the array,
|
|
such as image name and primary key
|
|
*/
|
|
|
|
var L = {}
|
|
L.styles = function(styleString){
|
|
const colonPosition = styleString.indexOf(':')
|
|
const property = styleString.slice(0, colonPosition)
|
|
const value = styleString.slice(colonPosition + 1)
|
|
this.style[property] = value
|
|
|
|
return this.styles
|
|
}
|
|
|
|
L.attribs = function(attributeString){
|
|
const assignmentPosition = attributeString.indexOf('=')
|
|
const attribute = attributeString.slice(0, assignmentPosition)
|
|
const value = attributeString.slice(assignmentPosition + 1)
|
|
this.setAttribute(attribute, value)
|
|
|
|
return this.attribs
|
|
}
|
|
|
|
L.attachAllElementsById = function(here){
|
|
let allElements = document.getElementsByTagName('*')
|
|
let array = []
|
|
array.forEach.call(allElements, function(element) {
|
|
if(element.id){
|
|
here[element.id] = element
|
|
element.styles = L.styles.bind(element) // attach L's styles() method here
|
|
element.attribs = L.attribs.bind(element) // attach L's attribs() method here
|
|
}
|
|
})
|
|
}
|
|
///////////////| START of L.attachNewElement |/////////////
|
|
/**
|
|
L.attachNewElement(string tagname, string id, object view) ...
|
|
... creates a new element of type "tagname" given as the first argument,
|
|
and gives it the id "id" given as the second argument.
|
|
The third argument is the view object where this new reference will be attached.
|
|
The element can then be referenced as view.id.
|
|
*/
|
|
L.attachNewElement = function(tagname, id, view){
|
|
if(arguments.length !== 3){
|
|
console.log(`Error: requires 3 arguments: tagname, id, view`)
|
|
return
|
|
}
|
|
try{
|
|
if(typeof tagname === `string`){
|
|
var newElement = document.createElement(tagname)
|
|
}
|
|
else{
|
|
console.log(`Error: tagname needs to be a string`)
|
|
return
|
|
}
|
|
if(typeof id === `string`){
|
|
newElement.id = id
|
|
}
|
|
else{
|
|
console.log(`Error: id needs to be a string`)
|
|
return
|
|
}
|
|
if(view.toString() === `[object Object]`){
|
|
view[newElement.id] = newElement
|
|
newElement.styles = L.styles.bind(newElement) // attach L's styles() method here
|
|
newElement.attribs = L.attribs.bind(newElement) // attach L's attribs() method here
|
|
return newElement
|
|
}
|
|
else{
|
|
console.log(`Error: view needs to be an object`)
|
|
return
|
|
}
|
|
}
|
|
catch(e){
|
|
console.log(`Error in L.attachNewElement: ${e}`)
|
|
return
|
|
}
|
|
}
|
|
///////////////| END of L.attachNewElement |/////////////
|
|
|
|
L.noPinchZoom = function(){
|
|
window.ontouchstart = function(eventObject){
|
|
if(eventObject.touches && eventObject.touches.length > 1){
|
|
eventObject.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
|
|
L.runQualifiedMethods = function(functionQualifiers, object, runNextUpdate){
|
|
Object
|
|
.keys(functionQualifiers)
|
|
.filter(qualifyFunction)
|
|
.forEach(runFunction)
|
|
if(typeof runNextUpdate === 'function'){runNextUpdate()}
|
|
|
|
//-----| helpers |-----//
|
|
function qualifyFunction(functionName){
|
|
const isQualified = functionQualifiers[functionName].every( qualifier => qualifier) &&
|
|
!!functionQualifiers[functionName].length
|
|
return isQualified
|
|
}
|
|
function runFunction(functionName){
|
|
if(typeof object[functionName] === 'function'){
|
|
object[functionName]()
|
|
}
|
|
|
|
/**
|
|
If the prefix of this function's name is 'set' (for updating the MODEL),
|
|
and there is a similarly named function with a prefix of 'show' (for updating the VIEW),
|
|
then run the 'show' version as well.
|
|
*/
|
|
let prefix = functionName.slice(0,3)
|
|
let newFunctionName = 'show' + functionName.slice(3)
|
|
|
|
if(prefix === 'set' && typeof object[newFunctionName] === 'function'){
|
|
object[newFunctionName]()
|
|
}
|
|
}
|
|
}
|
|
|
|
L.runQualifiedFunctions = function(functionQualifiers, model, view, controller){
|
|
Object
|
|
.keys(functionQualifiers)
|
|
.filter(qualifyFunction)
|
|
.forEach(runFunction)
|
|
//-----| helpers |-----//
|
|
function qualifyFunction(functionName){
|
|
const isQualified = functionQualifiers[functionName].every( qualifier => qualifier) &&
|
|
!!functionQualifiers[functionName].length
|
|
return isQualified
|
|
}
|
|
function runFunction(functionName){
|
|
if(typeof controller[functionName] === 'function'){
|
|
controller[functionName](model)
|
|
}
|
|
/**
|
|
If the prefix of this function's name is 'set' (for updating the MODEL),
|
|
and there is a similarly named function with a prefix of 'show' (for updating the VIEW),
|
|
then run the 'show' version as well.
|
|
*/
|
|
let prefix = functionName.slice(0,3)
|
|
let newFunctionName = 'show' + functionName.slice(3)
|
|
if(prefix === 'set' && typeof controller[newFunctionName] === 'function'){
|
|
controller[newFunctionName](view)
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
Use a php script that reads contents of file from $_POST['contents'] that was convert by client
|
|
as DataURL, and expects filename and uploadPath from: $_POST['filename'] and $_POST['uploadPath']
|
|
with trailing slash (/) provided by client (though script could check for this).
|
|
*/
|
|
L.uploadFiles = function(progressReporter, fileElement, phpScriptName, uploadPath='../uploads/'){
|
|
let doneCounter = 0
|
|
let fileCount = fileElement.files.length
|
|
const array = [] // make a real array to borrow it's forEach method
|
|
array.forEach.call(fileElement.files, (file, index) => {
|
|
const postman = new XMLHttpRequest() // make a file deliverer for each file
|
|
const uploadObject = postman.upload // This object keeps track of upload progress
|
|
const envelope = new FormData() // make a holder for the file's name and content
|
|
envelope.stuff = envelope.append // give 'append' the nickname 'stuff'
|
|
const reader = new FileReader() // make a file reader (the raw file element is useless)
|
|
|
|
reader.readAsDataURL(file) // process the file's contents
|
|
reader.onload = function(){ // when done ...
|
|
const contents = reader.result // collect the result, and ...
|
|
envelope.stuff('contents', contents) // place it in the envelope along with ...
|
|
envelope.stuff('filename', file.name) // its filename ...
|
|
envelope.stuff('uploadPath', uploadPath) // and its upload path on the server
|
|
|
|
postman.open(`POST`, phpScriptName)// open up a POST to the server's php script
|
|
postman.send(envelope) // send the file
|
|
|
|
//check when file loads and when there is an error
|
|
postman.onload = eventObject => {
|
|
postman.status !== 200 ? showMessage() : checkLastFileDone()
|
|
//-----| helper |------//
|
|
function showMessage(){
|
|
const message = `Trouble with file: ${postman.status}`
|
|
console.log(message)
|
|
alert(message)
|
|
}
|
|
function checkLastFileDone(){
|
|
doneCounter++
|
|
if(typeof progressReporter === 'function'){
|
|
if(doneCounter === fileCount){
|
|
progressReporter(1, 1, index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
postman.onerror = eventObject => {
|
|
const message = `Trouble connecting to server`
|
|
console.log(message)
|
|
alert(message)
|
|
}
|
|
|
|
//invoke the callback for each upload progress report
|
|
uploadObject.onprogress = function(progressObject){
|
|
if(typeof progressReporter === 'function'){
|
|
progressReporter(progressObject.loaded, progressObject.total, index)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
//---------------------------------------------------------//
|
|
/**
|
|
Given an array of strings (array), sorts the array 'in place' by filename EXTENSION,
|
|
and returns a copy of the array as well. Since it mutates the array, it is decidedly not
|
|
functionistic (but it functions).
|
|
*/
|
|
L.sortByExtension = function (array) {
|
|
const type = {}.toString.call(array, null);
|
|
if (type !== '[object Array]') {
|
|
return array;
|
|
}
|
|
if (array.length === 0 || array.some(member => typeof member !== 'string')) {
|
|
return array;
|
|
}
|
|
//-------------------------------------//
|
|
let extension = ``;
|
|
let nudeWord = ``;
|
|
array.forEach((m, i, a) => {
|
|
if (m.lastIndexOf(`.`) !== -1) {
|
|
//get the extension
|
|
extension = m.slice(m.lastIndexOf(`.`) + 1);
|
|
nudeWord = m.slice(0, m.lastIndexOf(`.`));
|
|
a[i] = `${extension}.${nudeWord}`;
|
|
}
|
|
});
|
|
|
|
array.sort();
|
|
|
|
array.forEach((m, i, a) => {
|
|
if (m.indexOf(`.`) !== -1){
|
|
//get prefix (formerly the extension)
|
|
extension = m.slice(0, m.indexOf(`.`))
|
|
nudeWord = m.slice(m.indexOf(`.`) + 1)
|
|
a[i] = `${nudeWord}.${extension}`
|
|
}
|
|
});
|
|
|
|
const newArray = []
|
|
array.forEach( m => newArray.push(m))
|
|
|
|
return newArray;
|
|
}
|
|
|
|
/**
|
|
From an array of string arrays, return a possibly smaller array
|
|
of only those string arrays whose member strings contain the given subString
|
|
regardless of case.
|
|
1. For arrayOfStringArrays, use the filter method (a function property of an array)
|
|
that expects a function argument that operates on each array member
|
|
2. Let's call the function argument 'match'
|
|
3. 'match' should test each member array for a match of the substring as follows:
|
|
a.) join the members strings together into a bigString that is lowerCased
|
|
b.) lowerCase the subString
|
|
c.) use indexOf to match substring to the bigString
|
|
d.) return true for a match, otherwise return false
|
|
4. the filter creates a new array after doing this.
|
|
5. final step: return the new array that the filter produced
|
|
*/
|
|
L.arrayStringMatch = function(subString, arrayOfStringArrays, maxIndex){
|
|
//============================================================//
|
|
return arrayOfStringArrays.filter(match)
|
|
//-------| Helper function 'match' |---------//
|
|
function match(memberArray){
|
|
//on 6-9-2018, added option of maximum index to eliminate searching through irrelevant fields
|
|
let bigString = ''
|
|
if(maxIndex && typeof maxIndex === "number" && maxIndex > 0){
|
|
bigString = memberArray
|
|
.filter((m,i) => i <= maxIndex)
|
|
.join(` `)
|
|
.toLowerCase()
|
|
}
|
|
else{
|
|
bigString = memberArray.join(` `).toLowerCase()
|
|
}
|
|
const substringToMatch = subString.toLowerCase()
|
|
return bigString.indexOf(substringToMatch) !== -1
|
|
}
|
|
}
|
|
//-------------------------------------------------//
|
|
|
|
/**
|
|
L.loopCall can be used to replace setInterval, which has been somewhat discredited.
|
|
See this blog post:
|
|
https://dev.to/akanksha_9560/why-not-to-use-setinterval--2na9
|
|
|
|
L.loopCall uses setTimeout recursively, which is a technique
|
|
reportedly more reliabale than setInterval.
|
|
|
|
L.loopCall repeatedly calls (invokes) the callback function provided as its first argument.
|
|
The first call is immediate, but subsequent calls are delayed by the milliseconds
|
|
provided as the second argument. All additional arguments are optional
|
|
to be used by the callback if required.
|
|
|
|
If needed, you can delay the initial call as well,
|
|
by having loopCall invoked by setTimeout using the same delay:
|
|
|
|
setTimeout(L.loopCall, delay, callback, delay, arg1, arg2 ...)
|
|
|
|
or the more readable, but more risky ...
|
|
|
|
setTimeout("L.loopCall(callback, delay, arg1, arg2 ...)", delay)
|
|
//Doug Crockford would not be pleased
|
|
|
|
To stop the loop, the callback function can test some external state condition,
|
|
(or test its own arguments, if they are passed by reference):
|
|
|
|
if(externalStateCondition){
|
|
L.loopCall.stop()
|
|
}
|
|
*/
|
|
L.loopCall = function (callback, delay, ...args){
|
|
L.loopCall.stopLoop = setTimeout(L.loopCall, delay, callback, delay, ...args)
|
|
callback(...args)
|
|
}
|
|
|
|
L.loopCall.stop = () => {
|
|
clearTimeout(L.loopCall.stopLoop)
|
|
}
|
|
|
|
/**
|
|
Returns an array that is the "mathematical or logical" symmetric difference among or between
|
|
any number of arrays provided as arguments (usually two). If the order of members is ignored
|
|
(as is done for mathematical sets), the result acts as the the exclusive-or (XOR), also know as
|
|
the disjunctive union. For the trivial cases of comparing one or two arrays, the result is
|
|
not surprising: for one array, the result is itself: L.symDiff(A, []) => A ⊕ [] = A.
|
|
For two arrays, the result is an array that has only members which are not shared in common:
|
|
L.symDiff(A, B) => A ⊕ B .
|
|
When comparing more than two arrays, the result may ne surprising. The proper result can be verified
|
|
by comparing only two at a time: L.symDiff(A, B, C) => A ⊕ B ⊕ C = (A ⊕ B) ⊕ C
|
|
*/
|
|
L.symDiff = function symDiff(arrayA, arrayB){ // dummy paramters NOT referenced in body of the function
|
|
var partialSymDiff = [],
|
|
argsArray = arguments
|
|
;
|
|
//============THE CRUX=================
|
|
return findSymDiff(partialSymDiff,0);
|
|
//============UNDER THE HOOD===========
|
|
function findSymDiff(partialSymDiff,index){
|
|
if (argsArray[index] === undefined){
|
|
return partialSymDiff;
|
|
}
|
|
else{
|
|
partialSymDiff = sd(partialSymDiff, argsArray[index] );
|
|
return findSymDiff( partialSymDiff, index + 1 );
|
|
}
|
|
}
|
|
//=====================================
|
|
function sd(arrayI, arrayJ){
|
|
var diff = [],
|
|
blackList = [],
|
|
i = 0,
|
|
j = 0,
|
|
maxI = arrayI.length,
|
|
maxJ = arrayJ.length
|
|
;
|
|
//-------------------------------------------------
|
|
//1.) Combine the arrays into a third array.
|
|
//2.) Find the matched elements and place them into a blacklist array.
|
|
//3.) Pull blacklisted elements from the combined array.
|
|
//4.) return the "reduced" combined array.
|
|
//---------------------------------------------------
|
|
// 1.) Combine the arrays into a third array.
|
|
diff = arrayI.concat(arrayJ);
|
|
//---------------------------------------------------
|
|
// 2.) Find the matched elements and place them into a blacklist array.
|
|
for ( i=0; i < maxI; i++ ){
|
|
for( j=0; j< maxJ; j++ ){
|
|
if(arrayI[i] === arrayJ[j]){
|
|
blackList.push(arrayI[i] );
|
|
}
|
|
}
|
|
}
|
|
//----------------------------------------------------
|
|
// 3.) Pull blacklisted elements from the combined array.
|
|
diff = diff.filter( (element) => blackList.indexOf(element) === -1 )
|
|
//----------------------------------------------------
|
|
// 4.) return the "reduced" combined array.
|
|
return killDupes(diff);
|
|
}
|
|
//========================================================
|
|
function killDupes(array){
|
|
var kept = []; // Record of the "keepers"
|
|
return array.filter(function(element){
|
|
if ( kept.indexOf(element) === -1 ){ //if not already retained ...
|
|
kept.push(element); // Record it as retained now, and...
|
|
return true; // allow this element to be kept (true)
|
|
}
|
|
else{
|
|
return false; // otherwise, don't keep it (already kept)
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Pass in a numerical seconds: it returns a string in the format
|
|
* mm : ss, like ...
|
|
* 35 : 37 in minutes and seconds
|
|
*/
|
|
L.secToMinSec = (seconds) =>{
|
|
var min = Math.floor(seconds / 60);
|
|
var sec = Math.floor(seconds % 60);
|
|
if(isNaN(min)){min = 0}
|
|
if(isNaN(sec)){sec = 0}
|
|
var zeroMin = ((min < 10) ? ("0" + min) : ("" + min));
|
|
var zeroSec = ((sec < 10) ? ("0" + sec) : ("" + sec));
|
|
var minSec = zeroMin + ":" + zeroSec;
|
|
return minSec;
|
|
};
|
|
//====| END of secToMinSec |====//
|
|
|
|
///////////////////| START of CreateListMixer |//////////////////////
|
|
/**
|
|
* CreateListMixer: a factory that creates and returns a function that
|
|
* returns a random item from the collection (array or object) provided.
|
|
* Notes: Example-> var list = ["a", "short", "list"];//three (3) items to test
|
|
* var getRandomItem = CreateListMixer();
|
|
* getRandomItem(list);//returns first of randomized list
|
|
* getRandomItem();//returns next item
|
|
* getRandomItem();//returns next item (last of three)
|
|
* getRandomItem();//new first item from re-randomized list
|
|
*
|
|
* // a new list;
|
|
* var list2 = { record1: "string", record2: "anotherString", ...};
|
|
* getRandomItem(list2);//returns first of new randomized list2
|
|
* getRandomItem();//etc.
|
|
* It returns a property name for objects or an array member for arrays;
|
|
* It returns 'false' if argument of function is not an object
|
|
* or an array (fails typeof arg === 'object')
|
|
*
|
|
*/
|
|
L.CreateListMixer = function(){
|
|
var list=[],
|
|
randList= [],
|
|
listLength= 0,
|
|
itemReturned= null,
|
|
itemReturnedIndex= -1
|
|
;
|
|
return function(){
|
|
if(arguments[0]){
|
|
if(typeof arguments[0] === 'object'){
|
|
list = arguments[0];
|
|
if({}.toString.call(arguments[0]) === '[object Object]'){
|
|
list = Object.keys(list);
|
|
}
|
|
randList = randomize(list);
|
|
listLength = list.length;
|
|
}
|
|
else{
|
|
return false;
|
|
}
|
|
}
|
|
//----| no args activity: return next random item |----
|
|
if(itemReturnedIndex >= listLength-1){
|
|
do{
|
|
randList = randomize(list);
|
|
itemReturnedIndex = -1;
|
|
}
|
|
while(randList[itemReturnedIndex +1] === itemReturned);
|
|
}
|
|
itemReturnedIndex++;
|
|
itemReturned = randList[itemReturnedIndex];
|
|
return itemReturned;
|
|
//-----helpers-----
|
|
function randomize(x){
|
|
var mixedIndexes = [];
|
|
var randomList = [];
|
|
randomizeIndexes();
|
|
return randomList;
|
|
//----sub helper----
|
|
function randomizeIndexes(){
|
|
// random numbers for mixedIndexes
|
|
while(mixedIndexes.length !== x.length){
|
|
var match = false;
|
|
var possibleIndex = (x.length)*Math.random();
|
|
possibleIndex = Math.floor(possibleIndex);
|
|
mixedIndexes.forEach(function(m){
|
|
if(m === possibleIndex){
|
|
match = true;
|
|
}
|
|
});
|
|
if(!match){
|
|
mixedIndexes.push(possibleIndex);
|
|
}
|
|
}
|
|
for(var i = 0; i < x.length; i++){
|
|
var newIndex = mixedIndexes[i];
|
|
randomList.push(list[newIndex]);
|
|
}
|
|
}
|
|
}
|
|
};//===| END returned function |======
|
|
}//===| END enclosing factory function======
|
|
///////////////////| END of CreateListMixer |//////////////////////
|
|
|
|
///////////////////| START of scrammbleThis |//////////////////////
|
|
/**
|
|
scrammbleThis: (depends on createListMixer, above)
|
|
It returns an array of randomly arranged items of the collection provided.
|
|
The argument must be an array, an object, or a string.
|
|
If the argument is an object, a random array of its property names is returned.
|
|
If the argument is a string, a random array of its characters is returned.
|
|
If the argument is an array, a random array of its members is returned.
|
|
*/
|
|
L.scrammbleThis = function(collection){
|
|
if ( !(typeof collection === 'object' || typeof collection === 'string') ){return collection}
|
|
var mix = L.CreateListMixer();
|
|
var list = (Object.prototype.toString.call(collection) === '[object Array]')
|
|
? collection
|
|
: (typeof collection === 'string')
|
|
? collection.split('')
|
|
: (typeof collection === 'object')
|
|
? Object.keys(collection)
|
|
: null
|
|
return list.map((m,i,a)=> (i === 0) ? mix(a) : mix())
|
|
}
|
|
///////////////////| END of scrammbleThis |//////////////////////
|
|
|
|
/**
|
|
Given an array of string arrays, this function returns an alphabetized version
|
|
*/
|
|
L.sortArrayOfStringArrays = function(arrayOfStringArrays, linkToken='```'){
|
|
//use a unique token (default = triple back-ticks ```) to join the strings of each array of strings
|
|
const arrayOfStrings = arrayOfStringArrays.map(stringArray => stringArray.join(linkToken))
|
|
|
|
//case-insensitive sort this array of strings, mutating it in place:
|
|
//https://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
|
|
arrayOfStrings.sort( (a,b) => a.toLowerCase().localeCompare(b.toLowerCase()) )
|
|
|
|
//return a new array of string arrays after splitting the strings on the unique token
|
|
return arrayOfStrings.map( string => string.split(linkToken))
|
|
}
|
|
"; |