// 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'); /** * Test whether given object is a number * @param {*} object * @return {Boolean} isNumber */ exports.isNumber = function(object) { return (object instanceof Number || typeof object == 'number'); }; /** * 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; }; /** * Test whether given object is an instance of google.visualization.DataTable * @param {*} object * @return {Boolean} isDataTable */ exports.isDataTable = function(object) { return (typeof (google) !== 'undefined') && (google.visualization) && (google.visualization.DataTable) && (object instanceof google.visualization.DataTable); }; /** * Create a semi UUID * source: http://stackoverflow.com/a/105074/1262753 * @return {String} uuid */ exports.randomUUID = function() { var S4 = function () { return Math.floor( Math.random() * 0x10000 /* 65536 */ ).toString(16); }; return ( S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4() ); }; /** * 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, len = arguments.length; i < len; 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.} 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.} props * @param {Object} a * @param {... Object} b * @return {Object} a */ exports.selectiveDeepExtend = function (props, a, b) { // 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]); } else { a[prop] = b[prop]; } } else if (Array.isArray(b[prop])) { throw new TypeError('Arrays are not supported by deepExtend'); } 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.} props * @param {Object} a * @param {... Object} b * @return {Object} a */ exports.selectiveNotDeepExtend = function (props, a, b) { // 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 { a[prop] = b[prop]; } } else if (Array.isArray(b[prop])) { throw new TypeError('Arrays are not supported by deepExtend'); } 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) * @returns {Object} */ exports.deepExtend = function(a, b, protoExtend) { // 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) || 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 { a[prop] = b[prop]; } } else if (Array.isArray(b[prop])) { throw new TypeError('Arrays are not supported by deepExtend'); } 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': 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(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 new Date(object).toISOString(); // parse string } } 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'; } return type; }; /** * 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; }; /** * 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; } }; /** * 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; }; 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("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)) { if (exports.isValidRGB(color)) { var rgb = color.substr(4).substr(0,color.length-5).split(','); color = exports.RGBToHex(rgb[0],rgb[1],rgb[2]); } if (exports.isValidHex(color)) { var hsv = exports.hexToHSV(color); var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; var darkerColorHex = exports.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,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 || 'white'; c.border = color.border || c.background; if (exports.isString(color.highlight)) { c.highlight = { border: color.highlight, background: color.highlight } } else { c.highlight = {}; c.highlight.background = color.highlight && color.highlight.background || c.background; c.highlight.border = color.highlight && color.highlight.border || c.border; } if (exports.isString(color.hover)) { c.hover = { border: color.hover, background: color.hover } } else { c.hover = {}; c.hover.background = color.hover && color.hover.background || c.background; c.hover.border = color.hover && color.hover.border || c.border; } } 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; } /** * 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 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 * @private */ exports.mergeOptions = function (mergeTarget, options, option) { if (options[option] !== undefined) { if (typeof options[option] == 'boolean') { mergeTarget[option].enabled = options[option]; } else { 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} searchFunction | -1 is lower, 0 is found, 1 is higher * @param {String} field * @param {String} field2 * @returns {number} * @private */ exports.binarySearchCustom = function(orderedItems, searchFunction, 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 = searchFunction(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' * @returns {number} * @private */ exports.binarySearchValue = function(orderedItems, target, field, sidePreference) { var maxIterations = 10000; var iteration = 0; var low = 0; var high = orderedItems.length - 1; var prevValue, value, nextValue, middle; 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 (value == target) { // we found the target return middle; } else if (prevValue < target && value > target) { // target is in between of the previous and the current return sidePreference == 'before' ? Math.max(0,middle - 1) : middle; } else if (value < target && nextValue > target) { // 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 (value < target) { // 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; }; /** * Quadratic ease-in-out * http://gizma.com/easing/ * @param {number} t Current time * @param {number} start Start value * @param {number} end End value * @param {number} duration Duration * @returns {number} Value corresponding with current time */ exports.easeInOutQuad = function (t, start, end, duration) { var change = end - start; t /= duration/2; if (t < 1) return change/2*t*t + start; t--; return -change/2 * (t*(t-2) - 1) + start; }; /* * 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 } };