svg-pan-zoom-m
Version:
JavaScript library for panning and zooming an SVG image from the mouse, touches and programmatically.
297 lines (274 loc) • 8.16 kB
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.
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; // eslint-disable-line consistent-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;
};
},
/**
* 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);
};
}