// utility functions
|
|
|
|
// first check if moment.js is already loaded in the browser window, if so,
|
|
// use this instance. Else, load via commonjs.
|
|
|
|
|
|
var moment = require('./module/moment');
|
|
var uuid = require('./module/uuid');
|
|
|
|
/**
|
|
* Test whether given object is a number
|
|
* @param {*} object
|
|
* @return {Boolean} isNumber
|
|
*/
|
|
exports.isNumber = function (object) {
|
|
return (object instanceof Number || typeof object == 'number');
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove everything in the DOM object
|
|
* @param DOMobject
|
|
*/
|
|
exports.recursiveDOMDelete = function (DOMobject) {
|
|
if (DOMobject) {
|
|
while (DOMobject.hasChildNodes() === true) {
|
|
exports.recursiveDOMDelete(DOMobject.firstChild);
|
|
DOMobject.removeChild(DOMobject.firstChild);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value.
|
|
*
|
|
* @param min
|
|
* @param max
|
|
* @param total
|
|
* @param value
|
|
* @returns {number}
|
|
*/
|
|
exports.giveRange = function (min, max, total, value) {
|
|
if (max == min) {
|
|
return 0.5;
|
|
}
|
|
else {
|
|
var scale = 1 / (max - min);
|
|
return Math.max(0, (value - min) * scale);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test whether given object is a string
|
|
* @param {*} object
|
|
* @return {Boolean} isString
|
|
*/
|
|
exports.isString = function (object) {
|
|
return (object instanceof String || typeof object == 'string');
|
|
};
|
|
|
|
/**
|
|
* Test whether given object is a Date, or a String containing a Date
|
|
* @param {Date | String} object
|
|
* @return {Boolean} isDate
|
|
*/
|
|
exports.isDate = function (object) {
|
|
if (object instanceof Date) {
|
|
return true;
|
|
}
|
|
else if (exports.isString(object)) {
|
|
// test whether this string contains a date
|
|
var match = ASPDateRegex.exec(object);
|
|
if (match) {
|
|
return true;
|
|
}
|
|
else if (!isNaN(Date.parse(object))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Create a semi UUID
|
|
* source: http://stackoverflow.com/a/105074/1262753
|
|
* @return {String} uuid
|
|
*/
|
|
exports.randomUUID = function () {
|
|
return uuid.v4();
|
|
};
|
|
|
|
/**
|
|
* assign all keys of an object that are not nested objects to a certain value (used for color objects).
|
|
* @param obj
|
|
* @param value
|
|
*/
|
|
exports.assignAllKeys = function (obj, value) {
|
|
for (var prop in obj) {
|
|
if (obj.hasOwnProperty(prop)) {
|
|
if (typeof obj[prop] !== 'object') {
|
|
obj[prop] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Fill an object with a possibly partially defined other object. Only copies values if the a object has an object requiring values.
|
|
* That means an object is not created on a property if only the b object has it.
|
|
* @param obj
|
|
* @param value
|
|
*/
|
|
exports.fillIfDefined = function (a, b, allowDeletion = false) {
|
|
for (var prop in a) {
|
|
if (b[prop] !== undefined) {
|
|
if (typeof b[prop] !== 'object') {
|
|
if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
|
|
delete a[prop];
|
|
}
|
|
else {
|
|
a[prop] = b[prop];
|
|
}
|
|
}
|
|
else {
|
|
if (typeof a[prop] === 'object') {
|
|
exports.fillIfDefined(a[prop], b[prop], allowDeletion);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Extend object a with the properties of object b or a series of objects
|
|
* Only properties with defined values are copied
|
|
* @param {Object} a
|
|
* @param {... Object} b
|
|
* @return {Object} a
|
|
*/
|
|
exports.protoExtend = function (a, b) {
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
var other = arguments[i];
|
|
for (var prop in other) {
|
|
a[prop] = other[prop];
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Extend object a with the properties of object b or a series of objects
|
|
* Only properties with defined values are copied
|
|
* @param {Object} a
|
|
* @param {... Object} b
|
|
* @return {Object} a
|
|
*/
|
|
exports.extend = function (a, b) {
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
var other = arguments[i];
|
|
for (var prop in other) {
|
|
if (other.hasOwnProperty(prop)) {
|
|
a[prop] = other[prop];
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Extend object a with selected properties of object b or a series of objects
|
|
* Only properties with defined values are copied
|
|
* @param {Array.<String>} props
|
|
* @param {Object} a
|
|
* @param {Object} b
|
|
* @return {Object} a
|
|
*/
|
|
exports.selectiveExtend = function (props, a, b) {
|
|
if (!Array.isArray(props)) {
|
|
throw new Error('Array with property names expected as first argument');
|
|
}
|
|
|
|
for (var i = 2; i < arguments.length; i++) {
|
|
var other = arguments[i];
|
|
|
|
for (var p = 0; p < props.length; p++) {
|
|
var prop = props[p];
|
|
if (other.hasOwnProperty(prop)) {
|
|
a[prop] = other[prop];
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Extend object a with selected properties of object b or a series of objects
|
|
* Only properties with defined values are copied
|
|
* @param {Array.<String>} props
|
|
* @param {Object} a
|
|
* @param {Object} b
|
|
* @return {Object} a
|
|
*/
|
|
exports.selectiveDeepExtend = function (props, a, b, allowDeletion = false) {
|
|
// TODO: add support for Arrays to deepExtend
|
|
if (Array.isArray(b)) {
|
|
throw new TypeError('Arrays are not supported by deepExtend');
|
|
}
|
|
for (var i = 2; i < arguments.length; i++) {
|
|
var other = arguments[i];
|
|
for (var p = 0; p < props.length; p++) {
|
|
var prop = props[p];
|
|
if (other.hasOwnProperty(prop)) {
|
|
if (b[prop] && b[prop].constructor === Object) {
|
|
if (a[prop] === undefined) {
|
|
a[prop] = {};
|
|
}
|
|
if (a[prop].constructor === Object) {
|
|
exports.deepExtend(a[prop], b[prop], false, allowDeletion);
|
|
}
|
|
else {
|
|
if ((b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
|
|
delete a[prop];
|
|
}
|
|
else {
|
|
a[prop] = b[prop];
|
|
}
|
|
}
|
|
} else if (Array.isArray(b[prop])) {
|
|
throw new TypeError('Arrays are not supported by deepExtend');
|
|
} else {
|
|
if ((b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
|
|
delete a[prop];
|
|
}
|
|
else {
|
|
a[prop] = b[prop];
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Extend object a with selected properties of object b or a series of objects
|
|
* Only properties with defined values are copied
|
|
* @param {Array.<String>} props
|
|
* @param {Object} a
|
|
* @param {Object} b
|
|
* @return {Object} a
|
|
*/
|
|
exports.selectiveNotDeepExtend = function (props, a, b, allowDeletion = false) {
|
|
// TODO: add support for Arrays to deepExtend
|
|
if (Array.isArray(b)) {
|
|
throw new TypeError('Arrays are not supported by deepExtend');
|
|
}
|
|
for (var prop in b) {
|
|
if (b.hasOwnProperty(prop)) {
|
|
if (props.indexOf(prop) == -1) {
|
|
if (b[prop] && b[prop].constructor === Object) {
|
|
if (a[prop] === undefined) {
|
|
a[prop] = {};
|
|
}
|
|
if (a[prop].constructor === Object) {
|
|
exports.deepExtend(a[prop], b[prop]);
|
|
}
|
|
else {
|
|
if ((b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
|
|
delete a[prop];
|
|
}
|
|
else {
|
|
a[prop] = b[prop];
|
|
}
|
|
}
|
|
} else if (Array.isArray(b[prop])) {
|
|
a[prop] = [];
|
|
for (let i = 0; i < b[prop].length; i++) {
|
|
a[prop].push(b[prop][i]);
|
|
}
|
|
} else {
|
|
if ((b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
|
|
delete a[prop];
|
|
}
|
|
else {
|
|
a[prop] = b[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Deep extend an object a with the properties of object b
|
|
* @param {Object} a
|
|
* @param {Object} b
|
|
* @param [Boolean] protoExtend --> optional parameter. If true, the prototype values will also be extended.
|
|
* (ie. the options objects that inherit from others will also get the inherited options)
|
|
* @param [Boolean] global --> optional parameter. If true, the values of fields that are null will not deleted
|
|
* @returns {Object}
|
|
*/
|
|
exports.deepExtend = function (a, b, protoExtend, allowDeletion) {
|
|
for (var prop in b) {
|
|
if (b.hasOwnProperty(prop) || protoExtend === true) {
|
|
if (b[prop] && b[prop].constructor === Object) {
|
|
if (a[prop] === undefined) {
|
|
a[prop] = {};
|
|
}
|
|
if (a[prop].constructor === Object) {
|
|
exports.deepExtend(a[prop], b[prop], protoExtend);
|
|
}
|
|
else {
|
|
if ((b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
|
|
delete a[prop];
|
|
}
|
|
else {
|
|
a[prop] = b[prop];
|
|
}
|
|
}
|
|
} else if (Array.isArray(b[prop])) {
|
|
a[prop] = [];
|
|
for (let i = 0; i < b[prop].length; i++) {
|
|
a[prop].push(b[prop][i]);
|
|
}
|
|
} else {
|
|
if ((b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
|
|
delete a[prop];
|
|
}
|
|
else {
|
|
a[prop] = b[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Test whether all elements in two arrays are equal.
|
|
* @param {Array} a
|
|
* @param {Array} b
|
|
* @return {boolean} Returns true if both arrays have the same length and same
|
|
* elements.
|
|
*/
|
|
exports.equalArray = function (a, b) {
|
|
if (a.length != b.length) return false;
|
|
|
|
for (var i = 0, len = a.length; i < len; i++) {
|
|
if (a[i] != b[i]) return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Convert an object to another type
|
|
* @param {Boolean | Number | String | Date | Moment | Null | undefined} object
|
|
* @param {String | undefined} type Name of the type. Available types:
|
|
* 'Boolean', 'Number', 'String',
|
|
* 'Date', 'Moment', ISODate', 'ASPDate'.
|
|
* @return {*} object
|
|
* @throws Error
|
|
*/
|
|
exports.convert = function (object, type) {
|
|
var match;
|
|
|
|
if (object === undefined) {
|
|
return undefined;
|
|
}
|
|
if (object === null) {
|
|
return null;
|
|
}
|
|
|
|
if (!type) {
|
|
return object;
|
|
}
|
|
if (!(typeof type === 'string') && !(type instanceof String)) {
|
|
throw new Error('Type must be a string');
|
|
}
|
|
|
|
//noinspection FallthroughInSwitchStatementJS
|
|
switch (type) {
|
|
case 'boolean':
|
|
case 'Boolean':
|
|
return Boolean(object);
|
|
|
|
case 'number':
|
|
case 'Number':
|
|
if (!isNaN(Date.parse(object))) {
|
|
return moment(object).valueOf();
|
|
} else {
|
|
return Number(object.valueOf());
|
|
}
|
|
case 'string':
|
|
case 'String':
|
|
return String(object);
|
|
|
|
case 'Date':
|
|
if (exports.isNumber(object)) {
|
|
return new Date(object);
|
|
}
|
|
if (object instanceof Date) {
|
|
return new Date(object.valueOf());
|
|
}
|
|
else if (moment.isMoment(object)) {
|
|
return new Date(object.valueOf());
|
|
}
|
|
if (exports.isString(object)) {
|
|
match = ASPDateRegex.exec(object);
|
|
if (match) {
|
|
// object is an ASP date
|
|
return new Date(Number(match[1])); // parse number
|
|
}
|
|
else {
|
|
return moment(new Date(object)).toDate(); // parse string
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(
|
|
'Cannot convert object of type ' + exports.getType(object) +
|
|
' to type Date');
|
|
}
|
|
|
|
case 'Moment':
|
|
if (exports.isNumber(object)) {
|
|
return moment(object);
|
|
}
|
|
if (object instanceof Date) {
|
|
return moment(object.valueOf());
|
|
}
|
|
else if (moment.isMoment(object)) {
|
|
return moment(object);
|
|
}
|
|
if (exports.isString(object)) {
|
|
match = ASPDateRegex.exec(object);
|
|
if (match) {
|
|
// object is an ASP date
|
|
return moment(Number(match[1])); // parse number
|
|
}
|
|
else {
|
|
return moment(object); // parse string
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(
|
|
'Cannot convert object of type ' + exports.getType(object) +
|
|
' to type Date');
|
|
}
|
|
|
|
case 'ISODate':
|
|
if (exports.isNumber(object)) {
|
|
return new Date(object);
|
|
}
|
|
else if (object instanceof Date) {
|
|
return object.toISOString();
|
|
}
|
|
else if (moment.isMoment(object)) {
|
|
return object.toDate().toISOString();
|
|
}
|
|
else if (exports.isString(object)) {
|
|
match = ASPDateRegex.exec(object);
|
|
if (match) {
|
|
// object is an ASP date
|
|
return new Date(Number(match[1])).toISOString(); // parse number
|
|
}
|
|
else {
|
|
return moment(object).format(); // ISO 8601
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(
|
|
'Cannot convert object of type ' + exports.getType(object) +
|
|
' to type ISODate');
|
|
}
|
|
|
|
case 'ASPDate':
|
|
if (exports.isNumber(object)) {
|
|
return '/Date(' + object + ')/';
|
|
}
|
|
else if (object instanceof Date) {
|
|
return '/Date(' + object.valueOf() + ')/';
|
|
}
|
|
else if (exports.isString(object)) {
|
|
match = ASPDateRegex.exec(object);
|
|
var value;
|
|
if (match) {
|
|
// object is an ASP date
|
|
value = new Date(Number(match[1])).valueOf(); // parse number
|
|
}
|
|
else {
|
|
value = new Date(object).valueOf(); // parse string
|
|
}
|
|
return '/Date(' + value + ')/';
|
|
}
|
|
else {
|
|
throw new Error(
|
|
'Cannot convert object of type ' + exports.getType(object) +
|
|
' to type ASPDate');
|
|
}
|
|
|
|
default:
|
|
throw new Error('Unknown type "' + type + '"');
|
|
}
|
|
};
|
|
|
|
// parse ASP.Net Date pattern,
|
|
// for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
|
|
// code from http://momentjs.com/
|
|
var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
|
|
|
|
/**
|
|
* Get the type of an object, for example exports.getType([]) returns 'Array'
|
|
* @param {*} object
|
|
* @return {String} type
|
|
*/
|
|
exports.getType = function (object) {
|
|
var type = typeof object;
|
|
|
|
if (type == 'object') {
|
|
if (object === null) {
|
|
return 'null';
|
|
}
|
|
if (object instanceof Boolean) {
|
|
return 'Boolean';
|
|
}
|
|
if (object instanceof Number) {
|
|
return 'Number';
|
|
}
|
|
if (object instanceof String) {
|
|
return 'String';
|
|
}
|
|
if (Array.isArray(object)) {
|
|
return 'Array';
|
|
}
|
|
if (object instanceof Date) {
|
|
return 'Date';
|
|
}
|
|
return 'Object';
|
|
}
|
|
else if (type == 'number') {
|
|
return 'Number';
|
|
}
|
|
else if (type == 'boolean') {
|
|
return 'Boolean';
|
|
}
|
|
else if (type == 'string') {
|
|
return 'String';
|
|
}
|
|
else if (type === undefined) {
|
|
return 'undefined';
|
|
}
|
|
|
|
|
|
return type;
|
|
};
|
|
|
|
|
|
/**
|
|
* Used to extend an array and copy it. This is used to propagate paths recursively.
|
|
*
|
|
* @param arr
|
|
* @param newValue
|
|
* @returns {Array}
|
|
*/
|
|
exports.copyAndExtendArray = function (arr, newValue) {
|
|
let newArr = [];
|
|
for (let i = 0; i < arr.length; i++) {
|
|
newArr.push(arr[i]);
|
|
}
|
|
newArr.push(newValue);
|
|
return newArr;
|
|
}
|
|
|
|
/**
|
|
* Used to extend an array and copy it. This is used to propagate paths recursively.
|
|
*
|
|
* @param arr
|
|
* @param newValue
|
|
* @returns {Array}
|
|
*/
|
|
exports.copyArray = function (arr) {
|
|
let newArr = [];
|
|
for (let i = 0; i < arr.length; i++) {
|
|
newArr.push(arr[i]);
|
|
}
|
|
return newArr;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the absolute left value of a DOM element
|
|
* @param {Element} elem A dom element, for example a div
|
|
* @return {number} left The absolute left position of this element
|
|
* in the browser page.
|
|
*/
|
|
exports.getAbsoluteLeft = function (elem) {
|
|
return elem.getBoundingClientRect().left;
|
|
};
|
|
|
|
exports.getAbsoluteRight = function (elem) {
|
|
return elem.getBoundingClientRect().right;
|
|
};
|
|
|
|
/**
|
|
* Retrieve the absolute top value of a DOM element
|
|
* @param {Element} elem A dom element, for example a div
|
|
* @return {number} top The absolute top position of this element
|
|
* in the browser page.
|
|
*/
|
|
exports.getAbsoluteTop = function (elem) {
|
|
return elem.getBoundingClientRect().top;
|
|
};
|
|
|
|
/**
|
|
* add a className to the given elements style
|
|
* @param {Element} elem
|
|
* @param {String} className
|
|
*/
|
|
exports.addClassName = function (elem, className) {
|
|
var classes = elem.className.split(' ');
|
|
if (classes.indexOf(className) == -1) {
|
|
classes.push(className); // add the class to the array
|
|
elem.className = classes.join(' ');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* add a className to the given elements style
|
|
* @param {Element} elem
|
|
* @param {String} className
|
|
*/
|
|
exports.removeClassName = function (elem, className) {
|
|
var classes = elem.className.split(' ');
|
|
var index = classes.indexOf(className);
|
|
if (index != -1) {
|
|
classes.splice(index, 1); // remove the class from the array
|
|
elem.className = classes.join(' ');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* For each method for both arrays and objects.
|
|
* In case of an array, the built-in Array.forEach() is applied.
|
|
* In case of an Object, the method loops over all properties of the object.
|
|
* @param {Object | Array} object An Object or Array
|
|
* @param {function} callback Callback method, called for each item in
|
|
* the object or array with three parameters:
|
|
* callback(value, index, object)
|
|
*/
|
|
exports.forEach = function (object, callback) {
|
|
var i,
|
|
len;
|
|
if (Array.isArray(object)) {
|
|
// array
|
|
for (i = 0, len = object.length; i < len; i++) {
|
|
callback(object[i], i, object);
|
|
}
|
|
}
|
|
else {
|
|
// object
|
|
for (i in object) {
|
|
if (object.hasOwnProperty(i)) {
|
|
callback(object[i], i, object);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Convert an object into an array: all objects properties are put into the
|
|
* array. The resulting array is unordered.
|
|
* @param {Object} object
|
|
* @param {Array} array
|
|
*/
|
|
exports.toArray = function (object) {
|
|
var array = [];
|
|
|
|
for (var prop in object) {
|
|
if (object.hasOwnProperty(prop)) array.push(object[prop]);
|
|
}
|
|
|
|
return array;
|
|
};
|
|
|
|
/**
|
|
* Update a property in an object
|
|
* @param {Object} object
|
|
* @param {String} key
|
|
* @param {*} value
|
|
* @return {Boolean} changed
|
|
*/
|
|
exports.updateProperty = function (object, key, value) {
|
|
if (object[key] !== value) {
|
|
object[key] = value;
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Throttle the given function to be only executed once per animation frame
|
|
* @param {function} fn
|
|
* @returns {function} Returns the throttled function
|
|
*/
|
|
exports.throttle = function (fn) {
|
|
var scheduled = false;
|
|
|
|
return function throttled () {
|
|
if (!scheduled) {
|
|
scheduled = true;
|
|
requestAnimationFrame(function () {
|
|
scheduled = false;
|
|
fn();
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add and event listener. Works for all browsers
|
|
* @param {Element} element An html element
|
|
* @param {string} action The action, for example "click",
|
|
* without the prefix "on"
|
|
* @param {function} listener The callback function to be executed
|
|
* @param {boolean} [useCapture]
|
|
*/
|
|
exports.addEventListener = function (element, action, listener, useCapture) {
|
|
if (element.addEventListener) {
|
|
if (useCapture === undefined)
|
|
useCapture = false;
|
|
|
|
if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
|
|
action = "DOMMouseScroll"; // For Firefox
|
|
}
|
|
|
|
element.addEventListener(action, listener, useCapture);
|
|
} else {
|
|
element.attachEvent("on" + action, listener); // IE browsers
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove an event listener from an element
|
|
* @param {Element} element An html dom element
|
|
* @param {string} action The name of the event, for example "mousedown"
|
|
* @param {function} listener The listener function
|
|
* @param {boolean} [useCapture]
|
|
*/
|
|
exports.removeEventListener = function (element, action, listener, useCapture) {
|
|
if (element.removeEventListener) {
|
|
// non-IE browsers
|
|
if (useCapture === undefined)
|
|
useCapture = false;
|
|
|
|
if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
|
|
action = "DOMMouseScroll"; // For Firefox
|
|
}
|
|
|
|
element.removeEventListener(action, listener, useCapture);
|
|
} else {
|
|
// IE browsers
|
|
element.detachEvent("on" + action, listener);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Cancels the event if it is cancelable, without stopping further propagation of the event.
|
|
*/
|
|
exports.preventDefault = function (event) {
|
|
if (!event)
|
|
event = window.event;
|
|
|
|
if (event.preventDefault) {
|
|
event.preventDefault(); // non-IE browsers
|
|
}
|
|
else {
|
|
event.returnValue = false; // IE browsers
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get HTML element which is the target of the event
|
|
* @param {Event} event
|
|
* @return {Element} target element
|
|
*/
|
|
exports.getTarget = function (event) {
|
|
// code from http://www.quirksmode.org/js/events_properties.html
|
|
if (!event) {
|
|
event = window.event;
|
|
}
|
|
|
|
var target;
|
|
|
|
if (event.target) {
|
|
target = event.target;
|
|
}
|
|
else if (event.srcElement) {
|
|
target = event.srcElement;
|
|
}
|
|
|
|
if (target.nodeType != undefined && target.nodeType == 3) {
|
|
// defeat Safari bug
|
|
target = target.parentNode;
|
|
}
|
|
|
|
return target;
|
|
};
|
|
|
|
/**
|
|
* Check if given element contains given parent somewhere in the DOM tree
|
|
* @param {Element} element
|
|
* @param {Element} parent
|
|
*/
|
|
exports.hasParent = function (element, parent) {
|
|
var e = element;
|
|
|
|
while (e) {
|
|
if (e === parent) {
|
|
return true;
|
|
}
|
|
e = e.parentNode;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
exports.option = {};
|
|
|
|
/**
|
|
* Convert a value into a boolean
|
|
* @param {Boolean | function | undefined} value
|
|
* @param {Boolean} [defaultValue]
|
|
* @returns {Boolean} bool
|
|
*/
|
|
exports.option.asBoolean = function (value, defaultValue) {
|
|
if (typeof value == 'function') {
|
|
value = value();
|
|
}
|
|
|
|
if (value != null) {
|
|
return (value != false);
|
|
}
|
|
|
|
return defaultValue || null;
|
|
};
|
|
|
|
/**
|
|
* Convert a value into a number
|
|
* @param {Boolean | function | undefined} value
|
|
* @param {Number} [defaultValue]
|
|
* @returns {Number} number
|
|
*/
|
|
exports.option.asNumber = function (value, defaultValue) {
|
|
if (typeof value == 'function') {
|
|
value = value();
|
|
}
|
|
|
|
if (value != null) {
|
|
return Number(value) || defaultValue || null;
|
|
}
|
|
|
|
return defaultValue || null;
|
|
};
|
|
|
|
/**
|
|
* Convert a value into a string
|
|
* @param {String | function | undefined} value
|
|
* @param {String} [defaultValue]
|
|
* @returns {String} str
|
|
*/
|
|
exports.option.asString = function (value, defaultValue) {
|
|
if (typeof value == 'function') {
|
|
value = value();
|
|
}
|
|
|
|
if (value != null) {
|
|
return String(value);
|
|
}
|
|
|
|
return defaultValue || null;
|
|
};
|
|
|
|
/**
|
|
* Convert a size or location into a string with pixels or a percentage
|
|
* @param {String | Number | function | undefined} value
|
|
* @param {String} [defaultValue]
|
|
* @returns {String} size
|
|
*/
|
|
exports.option.asSize = function (value, defaultValue) {
|
|
if (typeof value == 'function') {
|
|
value = value();
|
|
}
|
|
|
|
if (exports.isString(value)) {
|
|
return value;
|
|
}
|
|
else if (exports.isNumber(value)) {
|
|
return value + 'px';
|
|
}
|
|
else {
|
|
return defaultValue || null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Convert a value into a DOM element
|
|
* @param {HTMLElement | function | undefined} value
|
|
* @param {HTMLElement} [defaultValue]
|
|
* @returns {HTMLElement | null} dom
|
|
*/
|
|
exports.option.asElement = function (value, defaultValue) {
|
|
if (typeof value == 'function') {
|
|
value = value();
|
|
}
|
|
|
|
return value || defaultValue || null;
|
|
};
|
|
|
|
/**
|
|
* http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
|
|
*
|
|
* @param {String} hex
|
|
* @returns {{r: *, g: *, b: *}} | 255 range
|
|
*/
|
|
exports.hexToRGB = function (hex) {
|
|
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
|
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
|
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
|
|
return r + r + g + g + b + b;
|
|
});
|
|
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
return result ? {
|
|
r: parseInt(result[1], 16),
|
|
g: parseInt(result[2], 16),
|
|
b: parseInt(result[3], 16)
|
|
} : null;
|
|
};
|
|
|
|
/**
|
|
* This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string.
|
|
* @param color
|
|
* @param opacity
|
|
* @returns {*}
|
|
*/
|
|
exports.overrideOpacity = function (color, opacity) {
|
|
if (color.indexOf("rgba") != -1) {
|
|
return color;
|
|
}
|
|
else if (color.indexOf("rgb") != -1) {
|
|
var rgb = color.substr(color.indexOf("(") + 1).replace(")", "").split(",");
|
|
return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + opacity + ")"
|
|
}
|
|
else {
|
|
var rgb = exports.hexToRGB(color);
|
|
if (rgb == null) {
|
|
return color;
|
|
}
|
|
else {
|
|
return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + opacity + ")"
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param red 0 -- 255
|
|
* @param green 0 -- 255
|
|
* @param blue 0 -- 255
|
|
* @returns {string}
|
|
* @constructor
|
|
*/
|
|
exports.RGBToHex = function (red, green, blue) {
|
|
return "#" + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1);
|
|
};
|
|
|
|
/**
|
|
* Parse a color property into an object with border, background, and
|
|
* highlight colors
|
|
* @param {Object | String} color
|
|
* @return {Object} colorObject
|
|
*/
|
|
exports.parseColor = function (color) {
|
|
var c;
|
|
if (exports.isString(color) === true) {
|
|
if (exports.isValidRGB(color) === true) {
|
|
var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) { return parseInt(value) });
|
|
color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]);
|
|
}
|
|
if (exports.isValidHex(color) === true) {
|
|
var hsv = exports.hexToHSV(color);
|
|
var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) };
|
|
var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 };
|
|
var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v);
|
|
var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v);
|
|
c = {
|
|
background: color,
|
|
border: darkerColorHex,
|
|
highlight: {
|
|
background: lighterColorHex,
|
|
border: darkerColorHex
|
|
},
|
|
hover: {
|
|
background: lighterColorHex,
|
|
border: darkerColorHex
|
|
}
|
|
};
|
|
}
|
|
else {
|
|
c = {
|
|
background: color,
|
|
border: color,
|
|
highlight: {
|
|
background: color,
|
|
border: color
|
|
},
|
|
hover: {
|
|
background: color,
|
|
border: color
|
|
}
|
|
};
|
|
}
|
|
}
|
|
else {
|
|
c = {};
|
|
c.background = color.background || undefined;
|
|
c.border = color.border || undefined;
|
|
|
|
if (exports.isString(color.highlight)) {
|
|
c.highlight = {
|
|
border: color.highlight,
|
|
background: color.highlight
|
|
}
|
|
}
|
|
else {
|
|
c.highlight = {};
|
|
c.highlight.background = color.highlight && color.highlight.background || undefined;
|
|
c.highlight.border = color.highlight && color.highlight.border || undefined;
|
|
}
|
|
|
|
if (exports.isString(color.hover)) {
|
|
c.hover = {
|
|
border: color.hover,
|
|
background: color.hover
|
|
}
|
|
}
|
|
else {
|
|
c.hover = {};
|
|
c.hover.background = color.hover && color.hover.background || undefined;
|
|
c.hover.border = color.hover && color.hover.border || undefined;
|
|
}
|
|
}
|
|
|
|
return c;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* http://www.javascripter.net/faq/rgb2hsv.htm
|
|
*
|
|
* @param red
|
|
* @param green
|
|
* @param blue
|
|
* @returns {*}
|
|
* @constructor
|
|
*/
|
|
exports.RGBToHSV = function (red, green, blue) {
|
|
red = red / 255; green = green / 255; blue = blue / 255;
|
|
var minRGB = Math.min(red, Math.min(green, blue));
|
|
var maxRGB = Math.max(red, Math.max(green, blue));
|
|
|
|
// Black-gray-white
|
|
if (minRGB == maxRGB) {
|
|
return { h: 0, s: 0, v: minRGB };
|
|
}
|
|
|
|
// Colors other than black-gray-white:
|
|
var d = (red == minRGB) ? green - blue : ((blue == minRGB) ? red - green : blue - red);
|
|
var h = (red == minRGB) ? 3 : ((blue == minRGB) ? 1 : 5);
|
|
var hue = 60 * (h - d / (maxRGB - minRGB)) / 360;
|
|
var saturation = (maxRGB - minRGB) / maxRGB;
|
|
var value = maxRGB;
|
|
return { h: hue, s: saturation, v: value };
|
|
};
|
|
|
|
var cssUtil = {
|
|
// split a string with css styles into an object with key/values
|
|
split: function (cssText) {
|
|
var styles = {};
|
|
|
|
cssText.split(';').forEach(function (style) {
|
|
if (style.trim() != '') {
|
|
var parts = style.split(':');
|
|
var key = parts[0].trim();
|
|
var value = parts[1].trim();
|
|
styles[key] = value;
|
|
}
|
|
});
|
|
|
|
return styles;
|
|
},
|
|
|
|
// build a css text string from an object with key/values
|
|
join: function (styles) {
|
|
return Object.keys(styles)
|
|
.map(function (key) {
|
|
return key + ': ' + styles[key];
|
|
})
|
|
.join('; ');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Append a string with css styles to an element
|
|
* @param {Element} element
|
|
* @param {String} cssText
|
|
*/
|
|
exports.addCssText = function (element, cssText) {
|
|
var currentStyles = cssUtil.split(element.style.cssText);
|
|
var newStyles = cssUtil.split(cssText);
|
|
var styles = exports.extend(currentStyles, newStyles);
|
|
|
|
element.style.cssText = cssUtil.join(styles);
|
|
};
|
|
|
|
/**
|
|
* Remove a string with css styles from an element
|
|
* @param {Element} element
|
|
* @param {String} cssText
|
|
*/
|
|
exports.removeCssText = function (element, cssText) {
|
|
var styles = cssUtil.split(element.style.cssText);
|
|
var removeStyles = cssUtil.split(cssText);
|
|
|
|
for (var key in removeStyles) {
|
|
if (removeStyles.hasOwnProperty(key)) {
|
|
delete styles[key];
|
|
}
|
|
}
|
|
|
|
element.style.cssText = cssUtil.join(styles);
|
|
};
|
|
|
|
/**
|
|
* https://gist.github.com/mjijackson/5311256
|
|
* @param h
|
|
* @param s
|
|
* @param v
|
|
* @returns {{r: number, g: number, b: number}}
|
|
* @constructor
|
|
*/
|
|
exports.HSVToRGB = function (h, s, v) {
|
|
var r, g, b;
|
|
|
|
var i = Math.floor(h * 6);
|
|
var f = h * 6 - i;
|
|
var p = v * (1 - s);
|
|
var q = v * (1 - f * s);
|
|
var t = v * (1 - (1 - f) * s);
|
|
|
|
switch (i % 6) {
|
|
case 0: r = v, g = t, b = p; break;
|
|
case 1: r = q, g = v, b = p; break;
|
|
case 2: r = p, g = v, b = t; break;
|
|
case 3: r = p, g = q, b = v; break;
|
|
case 4: r = t, g = p, b = v; break;
|
|
case 5: r = v, g = p, b = q; break;
|
|
}
|
|
|
|
return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) };
|
|
};
|
|
|
|
exports.HSVToHex = function (h, s, v) {
|
|
var rgb = exports.HSVToRGB(h, s, v);
|
|
return exports.RGBToHex(rgb.r, rgb.g, rgb.b);
|
|
};
|
|
|
|
exports.hexToHSV = function (hex) {
|
|
var rgb = exports.hexToRGB(hex);
|
|
return exports.RGBToHSV(rgb.r, rgb.g, rgb.b);
|
|
};
|
|
|
|
exports.isValidHex = function (hex) {
|
|
var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
|
|
return isOk;
|
|
};
|
|
|
|
exports.isValidRGB = function (rgb) {
|
|
rgb = rgb.replace(" ", "");
|
|
var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb);
|
|
return isOk;
|
|
}
|
|
exports.isValidRGBA = function (rgba) {
|
|
rgba = rgba.replace(" ", "");
|
|
var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba);
|
|
return isOk;
|
|
}
|
|
|
|
/**
|
|
* This recursively redirects the prototype of JSON objects to the referenceObject
|
|
* This is used for default options.
|
|
*
|
|
* @param referenceObject
|
|
* @returns {*}
|
|
*/
|
|
exports.selectiveBridgeObject = function (fields, referenceObject) {
|
|
if (typeof referenceObject == "object") {
|
|
var objectTo = Object.create(referenceObject);
|
|
for (var i = 0; i < fields.length; i++) {
|
|
if (referenceObject.hasOwnProperty(fields[i])) {
|
|
if (typeof referenceObject[fields[i]] == "object") {
|
|
objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]);
|
|
}
|
|
}
|
|
}
|
|
return objectTo;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This recursively redirects the prototype of JSON objects to the referenceObject
|
|
* This is used for default options.
|
|
*
|
|
* @param referenceObject
|
|
* @returns {*}
|
|
*/
|
|
exports.bridgeObject = function (referenceObject) {
|
|
if (typeof referenceObject == "object") {
|
|
var objectTo = Object.create(referenceObject);
|
|
for (var i in referenceObject) {
|
|
if (referenceObject.hasOwnProperty(i)) {
|
|
if (typeof referenceObject[i] == "object") {
|
|
objectTo[i] = exports.bridgeObject(referenceObject[i]);
|
|
}
|
|
}
|
|
}
|
|
return objectTo;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method provides a stable sort implementation, very fast for presorted data
|
|
*
|
|
* @param a the array
|
|
* @param a order comparator
|
|
* @returns {the array}
|
|
*/
|
|
exports.insertSort = function (a,compare) {
|
|
for (var i = 0; i < a.length; i++) {
|
|
var k = a[i];
|
|
for (var j = i; j > 0 && compare(k,a[j - 1])<0; j--) {
|
|
a[j] = a[j - 1];
|
|
}
|
|
a[j] = k;
|
|
}
|
|
return a;
|
|
}
|
|
|
|
/**
|
|
* this is used to set the options of subobjects in the options object. A requirement of these subobjects
|
|
* is that they have an 'enabled' element which is optional for the user but mandatory for the program.
|
|
*
|
|
* @param [object] mergeTarget | this is either this.options or the options used for the groups.
|
|
* @param [object] options | options
|
|
* @param [String] option | this is the option key in the options argument
|
|
*/
|
|
exports.mergeOptions = function (mergeTarget, options, option, allowDeletion = false, globalOptions = {}) {
|
|
if (options[option] === null) {
|
|
mergeTarget[option] = Object.create(globalOptions[option]);
|
|
}
|
|
else {
|
|
if (options[option] !== undefined) {
|
|
if (typeof options[option] === 'boolean') {
|
|
mergeTarget[option].enabled = options[option];
|
|
}
|
|
else {
|
|
if (options[option].enabled === undefined) {
|
|
mergeTarget[option].enabled = true;
|
|
}
|
|
for (var prop in options[option]) {
|
|
if (options[option].hasOwnProperty(prop)) {
|
|
mergeTarget[option][prop] = options[option][prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses
|
|
* this function will then iterate in both directions over this sorted list to find all visible items.
|
|
*
|
|
* @param {Item[]} orderedItems | Items ordered by start
|
|
* @param {function} comparator | -1 is lower, 0 is equal, 1 is higher
|
|
* @param {String} field
|
|
* @param {String} field2
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
exports.binarySearchCustom = function (orderedItems, comparator, field, field2) {
|
|
var maxIterations = 10000;
|
|
var iteration = 0;
|
|
var low = 0;
|
|
var high = orderedItems.length - 1;
|
|
|
|
while (low <= high && iteration < maxIterations) {
|
|
var middle = Math.floor((low + high) / 2);
|
|
|
|
var item = orderedItems[middle];
|
|
var value = (field2 === undefined) ? item[field] : item[field][field2];
|
|
|
|
var searchResult = comparator(value);
|
|
if (searchResult == 0) { // jihaa, found a visible item!
|
|
return middle;
|
|
}
|
|
else if (searchResult == -1) { // it is too small --> increase low
|
|
low = middle + 1;
|
|
}
|
|
else { // it is too big --> decrease high
|
|
high = middle - 1;
|
|
}
|
|
|
|
iteration++;
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of
|
|
* two values, we return either the one before or the one after, depending on user input
|
|
* If it is found, we return the index, else -1.
|
|
*
|
|
* @param {Array} orderedItems
|
|
* @param {{start: number, end: number}} target
|
|
* @param {String} field
|
|
* @param {String} sidePreference 'before' or 'after'
|
|
* @param {function} comparator an optional comparator, returning -1,0,1 for <,==,>.
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
exports.binarySearchValue = function (orderedItems, target, field, sidePreference, comparator) {
|
|
var maxIterations = 10000;
|
|
var iteration = 0;
|
|
var low = 0;
|
|
var high = orderedItems.length - 1;
|
|
var prevValue, value, nextValue, middle;
|
|
|
|
var comparator = comparator != undefined ? comparator : function (a, b) {
|
|
return a == b ? 0 : a < b ? -1 : 1
|
|
};
|
|
|
|
while (low <= high && iteration < maxIterations) {
|
|
// get a new guess
|
|
middle = Math.floor(0.5 * (high + low));
|
|
prevValue = orderedItems[Math.max(0, middle - 1)][field];
|
|
value = orderedItems[middle][field];
|
|
nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field];
|
|
|
|
if (comparator(value, target) == 0) { // we found the target
|
|
return middle;
|
|
}
|
|
else if (comparator(prevValue, target) < 0 && comparator(value, target) > 0) { // target is in between of the previous and the current
|
|
return sidePreference == 'before' ? Math.max(0, middle - 1) : middle;
|
|
}
|
|
else if (comparator(value, target) < 0 && comparator(nextValue, target) > 0) { // target is in between of the current and the next
|
|
return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1);
|
|
}
|
|
else { // didnt find the target, we need to change our boundaries.
|
|
if (comparator(value, target) < 0) { // it is too small --> increase low
|
|
low = middle + 1;
|
|
}
|
|
else { // it is too big --> decrease high
|
|
high = middle - 1;
|
|
}
|
|
}
|
|
iteration++;
|
|
}
|
|
|
|
// didnt find anything. Return -1.
|
|
return -1;
|
|
};
|
|
|
|
/*
|
|
* Easing Functions - inspired from http://gizma.com/easing/
|
|
* only considering the t value for the range [0, 1] => [0, 1]
|
|
* https://gist.github.com/gre/1650294
|
|
*/
|
|
exports.easingFunctions = {
|
|
// no easing, no acceleration
|
|
linear: function (t) {
|
|
return t
|
|
},
|
|
// accelerating from zero velocity
|
|
easeInQuad: function (t) {
|
|
return t * t
|
|
},
|
|
// decelerating to zero velocity
|
|
easeOutQuad: function (t) {
|
|
return t * (2 - t)
|
|
},
|
|
// acceleration until halfway, then deceleration
|
|
easeInOutQuad: function (t) {
|
|
return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
},
|
|
// accelerating from zero velocity
|
|
easeInCubic: function (t) {
|
|
return t * t * t
|
|
},
|
|
// decelerating to zero velocity
|
|
easeOutCubic: function (t) {
|
|
return (--t) * t * t + 1
|
|
},
|
|
// acceleration until halfway, then deceleration
|
|
easeInOutCubic: function (t) {
|
|
return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
|
|
},
|
|
// accelerating from zero velocity
|
|
easeInQuart: function (t) {
|
|
return t * t * t * t
|
|
},
|
|
// decelerating to zero velocity
|
|
easeOutQuart: function (t) {
|
|
return 1 - (--t) * t * t * t
|
|
},
|
|
// acceleration until halfway, then deceleration
|
|
easeInOutQuart: function (t) {
|
|
return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
|
|
},
|
|
// accelerating from zero velocity
|
|
easeInQuint: function (t) {
|
|
return t * t * t * t * t
|
|
},
|
|
// decelerating to zero velocity
|
|
easeOutQuint: function (t) {
|
|
return 1 + (--t) * t * t * t * t
|
|
},
|
|
// acceleration until halfway, then deceleration
|
|
easeInOutQuint: function (t) {
|
|
return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
|
|
}
|
|
};
|
|
|
|
exports.getScrollBarWidth = function () {
|
|
var inner = document.createElement('p');
|
|
inner.style.width = "100%";
|
|
inner.style.height = "200px";
|
|
|
|
var outer = document.createElement('div');
|
|
outer.style.position = "absolute";
|
|
outer.style.top = "0px";
|
|
outer.style.left = "0px";
|
|
outer.style.visibility = "hidden";
|
|
outer.style.width = "200px";
|
|
outer.style.height = "150px";
|
|
outer.style.overflow = "hidden";
|
|
outer.appendChild (inner);
|
|
|
|
document.body.appendChild (outer);
|
|
var w1 = inner.offsetWidth;
|
|
outer.style.overflow = 'scroll';
|
|
var w2 = inner.offsetWidth;
|
|
if (w1 == w2) w2 = outer.clientWidth;
|
|
|
|
document.body.removeChild (outer);
|
|
|
|
return (w1 - w2);
|
|
};
|
|
|
|
exports.topMost = function (pile, accessors) {
|
|
let candidate;
|
|
if (!Array.isArray(accessors)) {
|
|
accessors = [accessors];
|
|
}
|
|
for (const member of pile) {
|
|
if (member) {
|
|
candidate = member[accessors[0]];
|
|
for (let i = 1; i < accessors.length; i++){
|
|
if (candidate) {
|
|
candidate = candidate[accessors[i]]
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
if (candidate) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return candidate;
|
|
};
|