/**
* @license jquery.panzoom.js v3.2.2
* Updated: Sat Aug 27 2016
* Add pan and zoom functionality to any element
* Copyright (c) timmy willison
* Released under the MIT license
* https://github.com/timmywil/jquery.panzoom/blob/master/MIT-License.txt
*/
(function(global, factory) {
// AMD
if (typeof define === 'function' && define.amd) {
define([ 'jquery' ], function(jQuery) {
return factory(global, jQuery);
});
// CommonJS/Browserify
} else if (typeof exports === 'object') {
factory(global, require('jquery'));
// Global
} else {
factory(global, global.jQuery);
}
}(typeof window !== 'undefined' ? window : this, function(window, $) {
'use strict';
var document = window.document;
var datakey = '__pz__';
var slice = Array.prototype.slice;
var rIE11 = /trident\/7./i;
var supportsInputEvent = (function() {
// IE11 returns a false positive
if (rIE11.test(navigator.userAgent)) {
return false;
}
var input = document.createElement('input');
input.setAttribute('oninput', 'return');
return typeof input.oninput === 'function';
})();
// Regex
var rupper = /([A-Z])/g;
var rsvg = /^http:[\w\.\/]+svg$/;
var floating = '(\\-?\\d[\\d\\.e-]*)';
var commaSpace = '\\,?\\s*';
var rmatrix = new RegExp(
'^matrix\\(' +
floating + commaSpace +
floating + commaSpace +
floating + commaSpace +
floating + commaSpace +
floating + commaSpace +
floating + '\\)$'
);
/**
* Utility for determining transform matrix equality
* Checks backwards to test translation first
* @param {Array} first
* @param {Array} second
*/
function matrixEquals(first, second) {
var i = first.length;
while(--i) {
if (Math.round(+first[i]) !== Math.round(+second[i])) {
return false;
}
}
return true;
}
/**
* Creates the options object for reset functions
* @param {Boolean|Object} opts See reset methods
* @returns {Object} Returns the newly-created options object
*/
function createResetOptions(opts) {
var options = { range: true, animate: true };
if (typeof opts === 'boolean') {
options.animate = opts;
} else {
$.extend(options, opts);
}
return options;
}
/**
* Represent a transformation matrix with a 3x3 matrix for calculations
* Matrix functions adapted from Louis Remi's jQuery.transform (https://github.com/louisremi/jquery.transform.js)
* @param {Array|Number} a An array of six values representing a 2d transformation matrix
*/
function Matrix(a, b, c, d, e, f, g, h, i) {
if ($.type(a) === 'array') {
this.elements = [
+a[0], +a[2], +a[4],
+a[1], +a[3], +a[5],
0, 0, 1
];
} else {
this.elements = [
a, b, c,
d, e, f,
g || 0, h || 0, i || 1
];
}
}
Matrix.prototype = {
/**
* Multiply a 3x3 matrix by a similar matrix or a vector
* @param {Matrix|Vector} matrix
* @return {Matrix|Vector} Returns a vector if multiplying by a vector
*/
x: function(matrix) {
var isVector = matrix instanceof Vector;
var a = this.elements,
b = matrix.elements;
if (isVector && b.length === 3) {
// b is actually a vector
return new Vector(
a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
a[6] * b[0] + a[7] * b[1] + a[8] * b[2]
);
} else if (b.length === a.length) {
// b is a 3x3 matrix
return new Matrix(
a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
);
}
return false; // fail
},
/**
* Generates an inverse of the current matrix
* @returns {Matrix}
*/
inverse: function() {
var d = 1 / this.determinant(),
a = this.elements;
return new Matrix(
d * ( a[8] * a[4] - a[7] * a[5]),
d * (-(a[8] * a[1] - a[7] * a[2])),
d * ( a[5] * a[1] - a[4] * a[2]),
d * (-(a[8] * a[3] - a[6] * a[5])),
d * ( a[8] * a[0] - a[6] * a[2]),
d * (-(a[5] * a[0] - a[3] * a[2])),
d * ( a[7] * a[3] - a[6] * a[4]),
d * (-(a[7] * a[0] - a[6] * a[1])),
d * ( a[4] * a[0] - a[3] * a[1])
);
},
/**
* Calculates the determinant of the current matrix
* @returns {Number}
*/
determinant: function() {
var a = this.elements;
return a[0] * (a[8] * a[4] - a[7] * a[5]) - a[3] * (a[8] * a[1] - a[7] * a[2]) + a[6] * (a[5] * a[1] - a[4] * a[2]);
}
};
/**
* Create a vector containing three values
*/
function Vector(x, y, z) {
this.elements = [ x, y, z ];
}
/**
* Get the element at zero-indexed index i
* @param {Number} i
*/
Vector.prototype.e = Matrix.prototype.e = function(i) {
return this.elements[ i ];
};
/**
* Create a Panzoom object for a given element
* @constructor
* @param {Element} elem - Element to use pan and zoom
* @param {Object} [options] - An object literal containing options to override default options
* (See Panzoom.defaults for ones not listed below)
* @param {jQuery} [options.$zoomIn] - zoom in buttons/links collection (you can also bind these yourself
* e.g. $button.on('click', function(e) { e.preventDefault(); $elem.panzoom('zoomIn'); });)
* @param {jQuery} [options.$zoomOut] - zoom out buttons/links collection on which to bind zoomOut
* @param {jQuery} [options.$zoomRange] - zoom in/out with this range control
* @param {jQuery} [options.$reset] - Reset buttons/links collection on which to bind the reset method
* @param {Function} [options.on[Start|Change|Zoom|Pan|End|Reset] - Optional callbacks for panzoom events
*/
function Panzoom(elem, options) {
// Allow instantiation without `new` keyword
if (!(this instanceof Panzoom)) {
return new Panzoom(elem, options);
}
// Sanity checks
if (elem.nodeType !== 1) {
$.error('Panzoom called on non-Element node');
}
if (!$.contains(document, elem)) {
$.error('Panzoom element must be attached to the document');
}
// Don't remake
var d = $.data(elem, datakey);
if (d) {
return d;
}
// Extend default with given object literal
// Each instance gets its own options
this.options = options = $.extend({}, Panzoom.defaults, options);
this.elem = elem;
var $elem = this.$elem = $(elem);
this.$set = options.$set && options.$set.length ? options.$set : $elem;
this.$doc = $(elem.ownerDocument || document);
this.$parent = $elem.parent();
this.parent = this.$parent[0];
// This is SVG if the namespace is SVG
// However, while <svg> elements are SVG, we want to treat those like other elements
this.isSVG = rsvg.test(elem.namespaceURI) && elem.nodeName.toLowerCase() !== 'svg';
this.panning = false;
// Save the original transform value
// Save the prefixed transform style key
// Set the starting transform
this._buildTransform();
// Build the appropriately-prefixed transform style property name
// De-camelcase
this._transform = $.cssProps.transform.replace(rupper, '-$1').toLowerCase();
// Build the transition value
this._buildTransition();
// Build containment dimensions
this.resetDimensions();
// Add zoom and reset buttons to `this`
var $empty = $();
var self = this;
$.each([ '$zoomIn', '$zoomOut', '$zoomRange', '$reset' ], function(i, name) {
self[ name ] = options[ name ] || $empty;
});
this.enable();
this.scale = this.getMatrix()[0];
this._checkPanWhenZoomed();
// Save the instance
$.data(elem, datakey, this);
}
// Attach regex for possible use (immutable)
Panzoom.rmatrix = rmatrix;
Panzoom.defaults = {
// Should always be non-empty
// Used to bind jQuery events without collisions
// A guid is not added here as different instantiations/versions of panzoom
// on the same element is not supported, so don't do it.
eventNamespace: '.panzoom',
// Whether or not to transition the scale
transition: true,
// Default cursor style for the element
cursor: 'move',
// There may be some use cases for zooming without panning or vice versa
disablePan: false,
disableZoom: false,
// Pan only on the X or Y axes
disableXAxis: false,
disableYAxis: false,
// Set whether you'd like to pan on left (1), middle (2), or right click (3)
which: 1,
// The increment at which to zoom
// adds/subtracts to the scale each time zoomIn/Out is called
increment: 0.3,
// Turns on exponential zooming
// If false, zooming will be incremented linearly
exponential: true,
// Pan only when the scale is greater than minScale
panOnlyWhenZoomed: false,
// min and max zoom scales
minScale: 0.3,
maxScale: 6,
// The default step for the range input
// Precendence: default < HTML attribute < option setting
rangeStep: 0.05,
// Animation duration (ms)
duration: 200,
// CSS easing used for scale transition
easing: 'ease-in-out',
// Indicate that the element should be contained within it's parent when panning
// Note: this does not affect zooming outside of the parent
// Set this value to 'invert' to only allow panning outside of the parent element (basically the opposite of the normal use of contain)
// 'invert' is useful for a large panzoom element where you don't want to show anything behind it
contain: false
};
Panzoom.prototype = {
constructor: Panzoom,
/**
* @returns {Panzoom} Returns the instance
*/
instance: function() {
return this;
},
/**
* Enable or re-enable the panzoom instance
*/
enable: function() {
// Unbind first
this._initStyle();
this._bind();
this.disabled = false;
},
/**
* Disable panzoom
*/
disable: function() {
this.disabled = true;
this._resetStyle();
this._unbind();
},
/**
* @returns {Boolean} Returns whether the current panzoom instance is disabled
*/
isDisabled: function() {
return this.disabled;
},
/**
* Destroy the panzoom instance
*/
destroy: function() {
this.disable();
$.removeData(this.elem, datakey);
},
/**
* Builds the restricing dimensions from the containment element
* Also used with focal points
* Call this method whenever the dimensions of the element or parent are changed
*/
resetDimensions: function() {
// Reset container properties
this.container = this.parent.getBoundingClientRect();
// Set element properties
var elem = this.elem;
// getBoundingClientRect() works with SVG, offsetWidth does not
var dims = elem.getBoundingClientRect();
var absScale = Math.abs(this.scale);
this.dimensions = {
width: dims.width,
height: dims.height,
left: $.css(elem, 'left', true) || 0,
top: $.css(elem, 'top', true) || 0,
// Borders and margins are scaled
border: {
top: $.css(elem, 'borderTopWidth', true) * absScale || 0,
bottom: $.css(elem, 'borderBottomWidth', true) * absScale || 0,
left: $.css(elem, 'borderLeftWidth', true) * absScale || 0,
right: $.css(elem, 'borderRightWidth', true) * absScale || 0
},
margin: {
top: $.css(elem, 'marginTop', true) * absScale || 0,
left: $.css(elem, 'marginLeft', true) * absScale || 0
}
};
},
/**
* Return the element to it's original transform matrix
* @param {Boolean} [options] If a boolean is passed, animate the reset (default: true). If an options object is passed, simply pass that along to setMatrix.
* @param {Boolean} [options.silent] Silence the reset event
*/
reset: function(options) {
options = createResetOptions(options);
// Reset the transform to its original value
var matrix = this.setMatrix(this._origTransform, options);
if (!options.silent) {
this._trigger('reset', matrix);
}
},
/**
* Only resets zoom level
* @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to zoom()
*/
resetZoom: function(options) {
options = createResetOptions(options);
var origMatrix = this.getMatrix(this._origTransform);
options.dValue = origMatrix[ 3 ];
this.zoom(origMatrix[0], options);
},
/**
* Only reset panning
* @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to pan()
*/
resetPan: function(options) {
var origMatrix = this.getMatrix(this._origTransform);
this.pan(origMatrix[4], origMatrix[5], createResetOptions(options));
},
/**
* Sets a transform on the $set
* For SVG, the style attribute takes precedence
* and allows us to animate
* @param {String} transform
*/
setTransform: function(transform) {
var $set = this.$set;
var i = $set.length;
while(i--) {
$.style($set[i], 'transform', transform);
// Support IE9-11, Edge 13-14+
// Set attribute alongside style attribute
// since IE and Edge do not respect style settings on SVG
// See https://css-tricks.com/transforms-on-svg-elements/
if (this.isSVG) {
$set[i].setAttribute('transform', transform);
}
}
},
/**
* Retrieving the transform is different for SVG
* (unless a style transform is already present)
* Uses the $set collection for retrieving the transform
* @param {String} [transform] Pass in an transform value (like 'scale(1.1)')
* to have it formatted into matrix format for use by Panzoom
* @returns {String} Returns the current transform value of the element
*/
getTransform: function(transform) {
var $set = this.$set;
var transformElem = $set[0];
if (transform) {
this.setTransform(transform);
} else {
// IE and Edge still set the transform style properly
// They just don't render it on SVG
// So we get a correct value here
transform = $.style(transformElem, 'transform');
if (this.isSVG && (!transform || transform === 'none')) {
transform = $.attr(transformElem, 'transform') || 'none';
}
}
// Convert any transforms set by the user to matrix format
// by setting to computed
if (transform !== 'none' && !rmatrix.test(transform)) {
// Get computed and set for next time
this.setTransform(transform = $.css(transformElem, 'transform'));
}
return transform || 'none';
},
/**
* Retrieve the current transform matrix for $elem (or turn a transform into it's array values)
* @param {String} [transform] matrix-formatted transform value
* @returns {Array} Returns the current transform matrix split up into it's parts, or a default matrix
*/
getMatrix: function(transform) {
var matrix = rmatrix.exec(transform || this.getTransform());
if (matrix) {
matrix.shift();
}
return matrix || [ 1, 0, 0, 1, 0, 0 ];
},
/**
* Given a matrix object, quickly set the current matrix of the element
* @param {Array|String} matrix
* @param {Object} [options]
* @param {Boolean|String} [options.animate] Whether to animate the transform change, or 'skip' indicating that it is unnecessary to set
* @param {Boolean} [options.contain] Override the global contain option
* @param {Boolean} [options.range] If true, $zoomRange's value will be updated.
* @param {Boolean} [options.silent] If true, the change event will not be triggered
* @returns {Array} Returns the newly-set matrix
*/
setMatrix: function(matrix, options) {
if (this.disabled) { return; }
if (!options) { options = {}; }
// Convert to array
if (typeof matrix === 'string') {
matrix = this.getMatrix(matrix);
}
var scale = +matrix[0];
var contain = typeof options.contain !== 'undefined' ? options.contain : this.options.contain;
// Apply containment
if (contain) {
var dims = options.dims;
if (!dims) {
this.resetDimensions();
dims = this.dimensions;
}
var spaceWLeft, spaceWRight, scaleDiff;
var container = this.container;
var width = dims.width;
var height = dims.height;
var conWidth = container.width;
var conHeight = container.height;
var zoomAspectW = conWidth / width;
var zoomAspectH = conHeight / height;
// If the element is not naturally centered,
// assume full space right
if (this.$parent.css('textAlign') !== 'center' || $.css(this.elem, 'display') !== 'inline') {
// offsetWidth gets us the width without the transform
scaleDiff = (width - this.elem.offsetWidth) / 2;
spaceWLeft = scaleDiff - dims.border.left;
spaceWRight = width - conWidth - scaleDiff + dims.border.right;
} else {
spaceWLeft = spaceWRight = ((width - conWidth) / 2);
}
var spaceHTop = ((height - conHeight) / 2) + dims.border.top;
var spaceHBottom = ((height - conHeight) / 2) - dims.border.top - dims.border.bottom;
if (contain === 'invert' || contain === 'automatic' && zoomAspectW < 1.01) {
matrix[4] = Math.max(Math.min(matrix[4], spaceWLeft - dims.border.left), -spaceWRight);
} else {
matrix[4] = Math.min(Math.max(matrix[4], spaceWLeft), -spaceWRight);
}
if (contain === 'invert' || (contain === 'automatic' && zoomAspectH < 1.01)) {
matrix[5] = Math.max(Math.min(matrix[5], spaceHTop - dims.border.top), -spaceHBottom);
} else {
matrix[5] = Math.min(Math.max(matrix[5], spaceHTop), -spaceHBottom);
}
}
// Animate
if (options.animate !== 'skip') {
// Set transition
this.transition(!options.animate);
}
// Update range element
if (options.range) {
this.$zoomRange.val(scale);
}
// Set the matrix on this.$set
if (this.options.disableXAxis || this.options.disableYAxis) {
var originalMatrix = this.getMatrix();
if (this.options.disableXAxis) {
matrix[4] = originalMatrix[4];
}
if (this.options.disableYAxis) {
matrix[5] = originalMatrix[5];
}
}
this.setTransform('matrix(' + matrix.join(',') + ')');
this.scale = scale;
// Disable/enable panning if zooming is at minimum and panOnlyWhenZoomed is true
this._checkPanWhenZoomed(scale);
if (!options.silent) {
this._trigger('change', matrix);
}
return matrix;
},
/**
* @returns {Boolean} Returns whether the panzoom element is currently being dragged
*/
isPanning: function() {
return this.panning;
},
/**
* Apply the current transition to the element, if allowed
* @param {Boolean} [off] Indicates that the transition should be turned off
*/
transition: function(off) {
if (!this._transition) { return; }
var transition = off || !this.options.transition ? 'none' : this._transition;
var $set = this.$set;
var i = $set.length;
while(i--) {
// Avoid reflows when zooming
if ($.style($set[i], 'transition') !== transition) {
$.style($set[i], 'transition', transition);
}
}
},
/**
* Pan the element to the specified translation X and Y
* Note: this is not the same as setting jQuery#offset() or jQuery#position()
* @param {Number} x
* @param {Number} y
* @param {Object} [options] These options are passed along to setMatrix
* @param {Array} [options.matrix] The matrix being manipulated (if already known so it doesn't have to be retrieved again)
* @param {Boolean} [options.silent] Silence the pan event. Note that this will also silence the setMatrix change event.
* @param {Boolean} [options.relative] Make the x and y values relative to the existing matrix
*/
pan: function(x, y, options) {
if (this.options.disablePan) { return; }
if (!options) { options = {}; }
var matrix = options.matrix;
if (!matrix) {
matrix = this.getMatrix();
}
// Cast existing matrix values to numbers
if (options.relative) {
x += +matrix[4];
y += +matrix[5];
}
matrix[4] = x;
matrix[5] = y;
this.setMatrix(matrix, options);
if (!options.silent) {
this._trigger('pan', matrix[4], matrix[5]);
}
},
/**
* Zoom in/out the element using the scale properties of a transform matrix
* @param {Number|Boolean} [scale] The scale to which to zoom or a boolean indicating to transition a zoom out
* @param {Object} [opts] All global options can be overwritten by this options object. For example, override the default increment.
* @param {Boolean} [opts.noSetRange] Specify that the method should not set the $zoomRange value (as is the case when $zoomRange is calling zoom on change)
* @param {jQuery.Event|Object} [opts.focal] A focal point on the panzoom element on which to zoom.
* If an object, set the clientX and clientY properties to the position relative to the parent
* @param {Boolean} [opts.animate] Whether to animate the zoom (defaults to true if scale is not a number, false otherwise)
* @param {Boolean} [opts.silent] Silence the zoom event
* @param {Array} [opts.matrix] Optionally pass the current matrix so it doesn't need to be retrieved
* @param {Number} [opts.dValue] Think of a transform matrix as four values a, b, c, d
* where a/d are the horizontal/vertical scale values and b/c are the skew values
* (5 and 6 of matrix array are the tx/ty transform values).
* Normally, the scale is set to both the a and d values of the matrix.
* This option allows you to specify a different d value for the zoom.
* For instance, to flip vertically, you could set -1 as the dValue.
*/
zoom: function(scale, opts) {
// Shuffle arguments
if (typeof scale === 'object') {
opts = scale;
scale = null;
} else if (!opts) {
opts = {};
}
var options = $.extend({}, this.options, opts);
// Check if disabled
if (options.disableZoom) { return; }
var animate = false;
var matrix = options.matrix || this.getMatrix();
var startScale = +matrix[0];
// Calculate zoom based on increment
if (typeof scale !== 'number') {
// Just use a number a little greater than 1
// Below 1 can use normal increments
if (options.exponential && startScale - options.increment >= 1) {
scale = Math[scale ? 'sqrt' : 'pow'](startScale, 2);
} else {
scale = startScale + (options.increment * (scale ? -1 : 1));
}
animate = true;
}
// Constrain scale
if (scale > options.maxScale) {
scale = options.maxScale;
} else if (scale < options.minScale) {
scale = options.minScale;
}
// Calculate focal point based on scale
var focal = options.focal;
if (focal && !options.disablePan) {
// Adapted from code by Florian Günther
// https://github.com/florianguenther/zui53
this.resetDimensions();
var dims = options.dims = this.dimensions;
var clientX = focal.clientX;
var clientY = focal.clientY;
// Adjust the focal point for transform-origin 50% 50%
// SVG elements have a transform origin of 0 0
if (!this.isSVG) {
clientX -= (dims.width / startScale) / 2;
clientY -= (dims.height / startScale) / 2;
}
var clientV = new Vector(clientX, clientY, 1);
var surfaceM = new Matrix(matrix);
// Supply an offset manually if necessary
var o = this.parentOffset || this.$parent.offset();
var offsetM = new Matrix(1, 0, o.left - this.$doc.scrollLeft(), 0, 1, o.top - this.$doc.scrollTop());
var surfaceV = surfaceM.inverse().x(offsetM.inverse().x(clientV));
var scaleBy = scale / matrix[0];
surfaceM = surfaceM.x(new Matrix([scaleBy, 0, 0, scaleBy, 0, 0]));
clientV = offsetM.x(surfaceM.x(surfaceV));
matrix[4] = +matrix[4] + (clientX - clientV.e(0));
matrix[5] = +matrix[5] + (clientY - clientV.e(1));
}
// Set the scale
matrix[0] = scale;
matrix[3] = typeof options.dValue === 'number' ? options.dValue : scale;
// Calling zoom may still pan the element
this.setMatrix(matrix, {
animate: typeof options.animate !== 'undefined' ? options.animate : animate,
// Set the zoomRange value
range: !options.noSetRange
});
// Trigger zoom event
if (!options.silent) {
this._trigger('zoom', matrix[0], options);
}
},
/**
* Get/set option on an existing instance
* @returns {Array|undefined} If getting, returns an array of all values
* on each instance for a given key. If setting, continue chaining by returning undefined.
*/
option: function(key, value) {
var options;
if (!key) {
// Avoids returning direct reference
return $.extend({}, this.options);
}
if (typeof key === 'string') {
if (arguments.length === 1) {
return this.options[ key ] !== undefined ?
this.options[ key ] :
null;
}
options = {};
options[ key ] = value;
} else {
options = key;
}
this._setOptions(options);
},
/**
* Internally sets options
* @param {Object} options - An object literal of options to set
* @private
*/
_setOptions: function(options) {
$.each(options, $.proxy(function(key, value) {
switch(key) {
case 'disablePan':
this._resetStyle();
/* falls through */
case '$zoomIn':
case '$zoomOut':
case '$zoomRange':
case '$reset':
case 'disableZoom':
case 'onStart':
case 'onChange':
case 'onZoom':
case 'onPan':
case 'onEnd':
case 'onReset':
case 'eventNamespace':
this._unbind();
}
this.options[ key ] = value;
switch(key) {
case 'disablePan':
this._initStyle();
/* falls through */
case '$zoomIn':
case '$zoomOut':
case '$zoomRange':
case '$reset':
// Set these on the instance
this[ key ] = value;
/* falls through */
case 'disableZoom':
case 'onStart':
case 'onChange':
case 'onZoom':
case 'onPan':
case 'onEnd':
case 'onReset':
case 'eventNamespace':
this._bind();
break;
case 'cursor':
$.style(this.elem, 'cursor', value);
break;
case 'minScale':
this.$zoomRange.attr('min', value);
break;
case 'maxScale':
this.$zoomRange.attr('max', value);
break;
case 'rangeStep':
this.$zoomRange.attr('step', value);
break;
case 'startTransform':
this._buildTransform();
break;
case 'duration':
case 'easing':
this._buildTransition();
/* falls through */
case 'transition':
this.transition();
break;
case 'panOnlyWhenZoomed':
this._checkPanWhenZoomed();
break;
case '$set':
if (value instanceof $ && value.length) {
this.$set = value;
// Reset styles
this._initStyle();
this._buildTransform();
}
}
}, this));
},
/**
* Disable/enable panning depending on whether the current scale
* matches the minimum
* @param {Number} [scale]
* @private
*/
_checkPanWhenZoomed: function(scale) {
var options = this.options;
if (options.panOnlyWhenZoomed) {
if (!scale) {
scale = this.getMatrix()[0];
}
var toDisable = scale <= options.minScale;
if (options.disablePan !== toDisable) {
this.option('disablePan', toDisable);
}
}
},
/**
* Initialize base styles for the element and its parent
* @private
*/
_initStyle: function() {
var styles = {
// Set the same default whether SVG or HTML
// transform-origin cannot be changed to 50% 50% in IE9-11 or Edge 13-14+
'transform-origin': this.isSVG ? '0 0' : '50% 50%'
};
// Set elem styles
if (!this.options.disablePan) {
styles.cursor = this.options.cursor;
}
this.$set.css(styles);
// Set parent to relative if set to static
var $parent = this.$parent;
// No need to add styles to the body
if ($parent.length && !$.nodeName(this.parent, 'body')) {
styles = {
overflow: 'hidden'
};
if ($parent.css('position') === 'static') {
styles.position = 'relative';
}
$parent.css(styles);
}
},
/**
* Undo any styles attached in this plugin
* @private
*/
_resetStyle: function() {
this.$elem.css({
'cursor': '',
'transition': ''
});
this.$parent.css({
'overflow': '',
'position': ''
});
},
/**
* Binds all necessary events
* @private
*/
_bind: function() {
var self = this;
var options = this.options;
var ns = options.eventNamespace;
var str_down = 'mousedown' + ns + ' pointerdown' + ns + ' MSPointerDown' + ns;
var str_start = 'touchstart' + ns + ' ' + str_down;
var str_click = 'touchend' + ns + ' click' + ns + ' pointerup' + ns + ' MSPointerUp' + ns;
var events = {};
var $reset = this.$reset;
var $zoomRange = this.$zoomRange;
// Bind panzoom events from options
$.each([ 'Start', 'Change', 'Zoom', 'Pan', 'End', 'Reset' ], function() {
var m = options[ 'on' + this ];
if ($.isFunction(m)) {
events[ 'panzoom' + this.toLowerCase() + ns ] = m;
}
});
// Bind $elem drag and click/touchdown events
// Bind touchstart if either panning or zooming is enabled
if (!options.disablePan || !options.disableZoom) {
events[ str_start ] = function(e) {
var touches;
if (e.type === 'touchstart' ?
// Touch
(touches = e.touches || e.originalEvent.touches) &&
((touches.length === 1 && !options.disablePan) || touches.length === 2) :
// Mouse/Pointer: Ignore unexpected click types
// Support: IE10 only
// IE10 does not support e.button for MSPointerDown, but does have e.which
!options.disablePan && (e.which || e.originalEvent.which) === options.which) {
e.preventDefault();
e.stopPropagation();
self._startMove(e, touches);
}
};
// Prevent the contextmenu event
// if we're binding to right-click
if (options.which === 3) {
events.contextmenu = false;
}
}
this.$elem.on(events);
// Bind reset
if ($reset.length) {
$reset.on(str_click, function(e) {
e.preventDefault();
self.reset();
});
}
// Set default attributes for the range input
if ($zoomRange.length) {
$zoomRange.attr({
// Only set the range step if explicit or
// set the default if there is no attribute present
step: options.rangeStep === Panzoom.defaults.rangeStep &&
$zoomRange.attr('step') ||
options.rangeStep,
min: options.minScale,
max: options.maxScale
}).prop({
value: this.getMatrix()[0]
});
}
// No bindings if zooming is disabled
if (options.disableZoom) {
return;
}
var $zoomIn = this.$zoomIn;
var $zoomOut = this.$zoomOut;
// Bind zoom in/out
// Don't bind one without the other
if ($zoomIn.length && $zoomOut.length) {
// preventDefault cancels future mouse events on touch events
$zoomIn.on(str_click, function(e) {
e.preventDefault();
self.zoom();
});
$zoomOut.on(str_click, function(e) {
e.preventDefault();
self.zoom(true);
});
}
if ($zoomRange.length) {
events = {};
// Cannot prevent default action here
events[ str_down ] = function() {
self.transition(true);
};
// Zoom on input events if available and change events
// See https://github.com/timmywil/jquery.panzoom/issues/90
events[ (supportsInputEvent ? 'input' : 'change') + ns ] = function() {
self.zoom(+this.value, { noSetRange: true });
};
$zoomRange.on(events);
}
},
/**
* Unbind all events
* @private
*/
_unbind: function() {
this.$elem
.add(this.$zoomIn)
.add(this.$zoomOut)
.add(this.$reset)
.off(this.options.eventNamespace);
},
/**
* Builds the original transform value
* @private
*/
_buildTransform: function() {
// Save the original transform
// Retrieving this also adds the correct prefixed style name
// to jQuery's internal $.cssProps
return this._origTransform = this.getTransform(this.options.startTransform);
},
/**
* Set transition property for later use when zooming
* @private
*/
_buildTransition: function() {
if (this._transform) {
var options = this.options;
this._transition = this._transform + ' ' + options.duration + 'ms ' + options.easing;
}
},
/**
* Calculates the distance between two touch points
* Remember pythagorean?
* @param {Array} touches
* @returns {Number} Returns the distance
* @private
*/
_getDistance: function(touches) {
var touch1 = touches[0];
var touch2 = touches[1];
return Math.sqrt(Math.pow(Math.abs(touch2.clientX - touch1.clientX), 2) + Math.pow(Math.abs(touch2.clientY - touch1.clientY), 2));
},
/**
* Constructs an approximated point in the middle of two touch points
* @returns {Object} Returns an object containing pageX and pageY
* @private
*/
_getMiddle: function(touches) {
var touch1 = touches[0];
var touch2 = touches[1];
return {
clientX: ((touch2.clientX - touch1.clientX) / 2) + touch1.clientX,
clientY: ((touch2.clientY - touch1.clientY) / 2) + touch1.clientY
};
},
/**
* Trigger a panzoom event on our element
* The event is passed the Panzoom instance
* @param {String|jQuery.Event} event
* @param {Mixed} arg1[, arg2, arg3, ...] Arguments to append to the trigger
* @private
*/
_trigger: function (event) {
if (typeof event === 'string') {
event = 'panzoom' + event;
}
this.$elem.triggerHandler(event, [this].concat(slice.call(arguments, 1)));
},
/**
* Starts the pan
* This is bound to mouse/touchmove on the element
* @param {jQuery.Event} event An event with pageX, pageY, and possibly the touches list
* @param {TouchList} [touches] The touches list if present
* @private
*/
_startMove: function(event, touches) {
if (this.panning) {
return;
}
var moveEvent, endEvent,
startDistance, startScale, startMiddle,
startPageX, startPageY, touch;
var self = this;
var options = this.options;
var ns = options.eventNamespace;
var matrix = this.getMatrix();
var original = matrix.slice(0);
var origPageX = +original[4];
var origPageY = +original[5];
var panOptions = { matrix: matrix, animate: 'skip' };
var type = event.type;
// Use proper events
if (type === 'pointerdown') {
moveEvent = 'pointermove';
endEvent = 'pointerup';
} else if (type === 'touchstart') {
moveEvent = 'touchmove';
endEvent = 'touchend';
} else if (type === 'MSPointerDown') {
moveEvent = 'MSPointerMove';
endEvent = 'MSPointerUp';
} else {
moveEvent = 'mousemove';
endEvent = 'mouseup';
}
// Add namespace
moveEvent += ns;
endEvent += ns;
// Remove any transitions happening
this.transition(true);
// Indicate that we are currently panning
this.panning = true;
// Trigger start event
this._trigger('start', event, touches);
var setStart = function(event, touches) {
if (touches) {
if (touches.length === 2) {
if (startDistance != null) {
return;
}
startDistance = self._getDistance(touches);
startScale = +matrix[0];
startMiddle = self._getMiddle(touches);
return;
}
if (startPageX != null) {
return;
}
if ((touch = touches[0])) {
startPageX = touch.pageX;
startPageY = touch.pageY;
}
}
if (startPageX != null) {
return;
}
startPageX = event.pageX;
startPageY = event.pageY;
};
setStart(event, touches);
var move = function(e) {
var coords;
e.preventDefault();
touches = e.touches || e.originalEvent.touches;
setStart(e, touches);
if (touches) {
if (touches.length === 2) {
// Calculate move on middle point
var middle = self._getMiddle(touches);
var diff = self._getDistance(touches) - startDistance;
// Set zoom
self.zoom(diff * (options.increment / 100) + startScale, {
focal: middle,
matrix: matrix,
animate: 'skip'
});
// Set pan
self.pan(
+matrix[4] + middle.clientX - startMiddle.clientX,
+matrix[5] + middle.clientY - startMiddle.clientY,
panOptions
);
startMiddle = middle;
return;
}
coords = touches[0] || { pageX: 0, pageY: 0 };
}
if (!coords) {
coords = e;
}
self.pan(
origPageX + coords.pageX - startPageX,
origPageY + coords.pageY - startPageY,
panOptions
);
};
// Bind the handlers
$(document)
.off(ns)
.on(moveEvent, move)
.on(endEvent, function(e) {
e.preventDefault();
// Unbind all document events
$(this).off(ns);
self.panning = false;
// Trigger our end event
// Simply set the type to "panzoomend" to pass through all end properties
// jQuery's `not` is used here to compare Array equality
e.type = 'panzoomend';
self._trigger(e, matrix, !matrixEquals(matrix, original));
});
}
};
// Add Panzoom as a static property
$.Panzoom = Panzoom;
/**
* Extend jQuery
* @param {Object|String} options - The name of a method to call on the prototype
* or an object literal of options
* @returns {jQuery|Mixed} jQuery instance for regular chaining or the return value(s) of a panzoom method call
*/
$.fn.panzoom = function(options) {
var instance, args, m, ret;
// Call methods widget-style
if (typeof options === 'string') {
ret = [];
args = slice.call(arguments, 1);
this.each(function() {
instance = $.data(this, datakey);
if (!instance) {
ret.push(undefined);
// Ignore methods beginning with `_`
} else if (options.charAt(0) !== '_' &&
typeof (m = instance[ options ]) === 'function' &&
// If nothing is returned, do not add to return values
(m = m.apply(instance, args)) !== undefined) {
ret.push(m);
}
});
// Return an array of values for the jQuery instances
// Or the value itself if there is only one
// Or keep chaining
return ret.length ?
(ret.length === 1 ? ret[0] : ret) :
this;
}
return this.each(function() { new Panzoom(this, options); });
};
return Panzoom;
}));