module.exports = { /** * Extends an object * * @param {Object} target object to extend * @param {Object} source object to take properties from * @return {Object} extended object */ extend: function(target, source) { target = target || {}; for (var prop in source) { // Go recursively if (this.isObject(source[prop])) { target[prop] = this.extend(target[prop], source[prop]) } else { target[prop] = source[prop] } } return target; } /** * Checks if an object is a DOM element * * @param {Object} o HTML element or String * @return {Boolean} returns true if object is a DOM element */ , isElement: function(o){ return ( o instanceof HTMLElement || o instanceof SVGElement || o instanceof SVGSVGElement || //DOM2 (o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string') ); } /** * Checks if an object is an Object * * @param {Object} o Object * @return {Boolean} returns true if object is an Object */ , isObject: function(o){ return Object.prototype.toString.call(o) === '[object Object]'; } /** * Checks if variable is Number * * @param {Integer|Float} n * @return {Boolean} returns true if variable is Number */ , isNumber: function(n) { return !isNaN(parseFloat(n)) && isFinite(n); } /** * Search for an SVG element * * @param {Object|String} elementOrSelector DOM Element or selector String * @return {Object|Null} SVG or null */ , getSvg: function(elementOrSelector) { var element , svg; if (!this.isElement(elementOrSelector)) { // If selector provided if (typeof elementOrSelector === 'string' || elementOrSelector instanceof String) { // Try to find the element element = document.querySelector(elementOrSelector) if (!element) { throw new Error('Provided selector did not find any elements. Selector: ' + elementOrSelector) return null } } else { throw new Error('Provided selector is not an HTML object nor String') return null } } else { element = elementOrSelector } if (element.tagName.toLowerCase() === 'svg') { svg = element; } else { if (element.tagName.toLowerCase() === 'object') { svg = element.contentDocument.documentElement; } else { if (element.tagName.toLowerCase() === 'embed') { svg = element.getSVGDocument().documentElement; } else { if (element.tagName.toLowerCase() === 'img') { throw new Error('Cannot script an SVG in an "img" element. Please use an "object" element or an in-line SVG.'); } else { throw new Error('Cannot get SVG.'); } return null } } } return svg } /** * Attach a given context to a function * @param {Function} fn Function * @param {Object} context Context * @return {Function} Function with certain context */ , proxy: function(fn, context) { return function() { return fn.apply(context, arguments) } } /** * Returns object type * Uses toString that returns [object SVGPoint] * And than parses object type from string * * @param {Object} o Any object * @return {String} Object type */ , getType: function(o) { return Object.prototype.toString.apply(o).replace(/^\[object\s/, '').replace(/\]$/, '') } /** * If it is a touch event than add clientX and clientY to event object * * @param {Event} evt * @param {SVGSVGElement} svg */ , mouseAndTouchNormalize: function(evt, svg) { // If no clientX then fallback if (evt.clientX === void 0 || evt.clientX === null) { // Fallback evt.clientX = 0 evt.clientY = 0 // If it is a touch event if (evt.touches !== void 0 && evt.touches.length) { if (evt.touches[0].clientX !== void 0) { evt.clientX = evt.touches[0].clientX evt.clientY = evt.touches[0].clientY } else if (evt.touches[0].pageX !== void 0) { var rect = svg.getBoundingClientRect(); evt.clientX = evt.touches[0].pageX - rect.left evt.clientY = evt.touches[0].pageY - rect.top } // If it is a custom event } else if (evt.originalEvent !== void 0) { if (evt.originalEvent.clientX !== void 0) { evt.clientX = evt.originalEvent.clientX evt.clientY = evt.originalEvent.clientY } } } } /** * Check if an event is a double click/tap * TODO: For touch gestures use a library (hammer.js) that takes in account other events * (touchmove and touchend). It should take in account tap duration and traveled distance * * @param {Event} evt * @param {Event} prevEvt Previous Event * @return {Boolean} */ , isDblClick: function(evt, prevEvt) { // Double click detected by browser if (evt.detail === 2) { return true; } // Try to compare events else if (prevEvt !== void 0 && prevEvt !== null) { var timeStampDiff = evt.timeStamp - prevEvt.timeStamp // should be lower than 250 ms , touchesDistance = Math.sqrt(Math.pow(evt.clientX - prevEvt.clientX, 2) + Math.pow(evt.clientY - prevEvt.clientY, 2)) return timeStampDiff < 250 && touchesDistance < 10 } // Nothing found return false; } /** * Returns current timestamp as an integer * * @return {Number} */ , now: Date.now || function() { return new Date().getTime(); } // From underscore. // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. Normally, the throttled function will run // as much as it can, without ever going more than once per `wait` duration; // but if you'd like to disable the execution on the leading edge, pass // `{leading: false}`. To disable execution on the trailing edge, ditto. // jscs:disable // jshint ignore:start , throttle: function(func, wait, options) { var that = this; var context, args, result; var timeout = null; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : that.now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = that.now(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; } // jshint ignore:end // jscs:enable /** * Create a requestAnimationFrame simulation * * @param {Number|String} refreshRate * @return {Function} */ , createRequestAnimationFrame: function(refreshRate) { var timeout = null // Convert refreshRate to timeout if (refreshRate !== 'auto' && refreshRate < 60 && refreshRate > 1) { timeout = Math.floor(1000 / refreshRate) } if (timeout === null) { return window.requestAnimationFrame || requestTimeout(33) } else { return requestTimeout(timeout) } } } /** * Create a callback that will execute after a given timeout * * @param {Function} timeout * @return {Function} */ function requestTimeout(timeout) { return function(callback) { window.setTimeout(callback, timeout) } }