272 lines
7.7 KiB
JavaScript
272 lines
7.7 KiB
JavaScript
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)
|
|
}
|
|
}
|