Difference between revisions of "Team:Tianjin/Resources/JS:jquerypanzoom"

(Created page with " (function(global, factory) { // AMD if (typeof define === 'function' && define.amd) { define([ 'jquery' ], function(jQuery) { return factory(global, jQuery); }); //...")
 
 
Line 1: Line 1:
 
 
(function(global, factory) {
 
(function(global, factory) {
// AMD
+
        // AMD
if (typeof define === 'function' && define.amd) {
+
        if (typeof define === 'function' && define.amd) {
define([ 'jquery' ], function(jQuery) {
+
                define(['jquery'],
return factory(global, jQuery);
+
                function(jQuery) {
});
+
                        return factory(global, jQuery);
// CommonJS/Browserify
+
                });
} else if (typeof exports === 'object') {
+
                // CommonJS/Browserify
factory(global, require('jquery'));
+
        } else if (typeof exports === 'object') {
// Global
+
                factory(global, require('jquery'));
} else {
+
                // Global
factory(global, global.jQuery);
+
        } else {
}
+
                factory(global, global.jQuery);
}(typeof window !== 'undefined' ? window : this, function(window, $) {
+
        }
'use strict';
+
} (typeof window !== 'undefined' ? window: this,
 +
function(window, $) {
 +
        'use strict';
  
// Common properties to lift for touch or pointer events
+
        // Common properties to lift for touch or pointer events
var list = 'over out down up move enter leave cancel'.split(' ');
+
        var list = 'over out down up move enter leave cancel'.split(' ');
var hook = $.extend({}, $.event.mouseHooks);
+
        var hook = $.extend({},
var events = {};
+
        $.event.mouseHooks);
 +
        var events = {};
  
// Support pointer events in IE11+ if available
+
        // Support pointer events in IE11+ if available
if ( window.PointerEvent ) {
+
        if (window.PointerEvent) {
$.each(list, function( i, name ) {
+
                $.each(list,
// Add event name to events property and add fixHook
+
                function(i, name) {
$.event.fixHooks[
+
                        // Add event name to events property and add fixHook
(events[name] = 'pointer' + name)
+
                        $.event.fixHooks[(events[name] = 'pointer' + name)] = hook;
] = hook;
+
                });
});
+
        } else {
} else {
+
                var mouseProps = hook.props;
var mouseProps = hook.props;
+
                // Add touch properties for the touch hook
// Add touch properties for the touch hook
+
                hook.props = mouseProps.concat(['touches', 'changedTouches', 'targetTouches', 'altKey', 'ctrlKey', 'metaKey', 'shiftKey']);
hook.props = mouseProps.concat(['touches', 'changedTouches', 'targetTouches', 'altKey', 'ctrlKey', 'metaKey', 'shiftKey']);
+
  
/**
+
                /**
 
* Support: Android
 
* Support: Android
 
* Android sets pageX/Y to 0 for any touch event
 
* Android sets pageX/Y to 0 for any touch event
 
* Attach first touch's pageX/pageY and clientX/clientY if not set correctly
 
* Attach first touch's pageX/pageY and clientX/clientY if not set correctly
 
*/
 
*/
hook.filter = function( event, originalEvent ) {
+
                hook.filter = function(event, originalEvent) {
var touch;
+
                        var touch;
var i = mouseProps.length;
+
                        var i = mouseProps.length;
if ( !originalEvent.pageX && originalEvent.touches && (touch = originalEvent.touches[0]) ) {
+
                        if (!originalEvent.pageX && originalEvent.touches && (touch = originalEvent.touches[0])) {
// Copy over all mouse properties
+
                                // Copy over all mouse properties
while(i--) {
+
                                while (i--) {
event[mouseProps[i]] = touch[mouseProps[i]];
+
                                        event[mouseProps[i]] = touch[mouseProps[i]];
}
+
                                }
}
+
                        }
return event;
+
                        return event;
};
+
                };
  
$.each(list, function( i, name ) {
+
                $.each(list,
// No equivalent touch events for over and out
+
                function(i, name) {
if (i < 2) {
+
                        // No equivalent touch events for over and out
events[ name ] = 'mouse' + name;
+
                        if (i < 2) {
} else {
+
                                events[name] = 'mouse' + name;
var touch = 'touch' +
+
                        } else {
(name === 'down' ? 'start' : name === 'up' ? 'end' : name);
+
                                var touch = 'touch' + (name === 'down' ? 'start': name === 'up' ? 'end': name);
// Add fixHook
+
                                // Add fixHook
$.event.fixHooks[ touch ] = hook;
+
                                $.event.fixHooks[touch] = hook;
// Add event names to events property
+
                                // Add event names to events property
events[ name ] = touch + ' mouse' + name;
+
                                events[name] = touch + ' mouse' + name;
}
+
                        }
});
+
                });
}
+
        }
  
$.pointertouch = events;
+
        $.pointertouch = events;
  
var document = window.document;
+
        var document = window.document;
var datakey = '__pz__';
+
        var datakey = '__pz__';
var slice = Array.prototype.slice;
+
        var slice = Array.prototype.slice;
var pointerEvents = !!window.PointerEvent;
+
        var pointerEvents = !!window.PointerEvent;
var supportsInputEvent = (function() {
+
        var supportsInputEvent = (function() {
var input = document.createElement('input');
+
                var input = document.createElement('input');
input.setAttribute('oninput', 'return');
+
                input.setAttribute('oninput', 'return');
return typeof input.oninput === 'function';
+
                return typeof input.oninput === 'function';
})();
+
        })();
  
// Regex
+
        // Regex
var rupper = /([A-Z])/g;
+
        var rupper = /([A-Z])/g;
var rsvg = /^http:[\w\.\/]+svg$/;
+
        var rsvg = /^http:[\w\.\/]+svg$/;
var rinline = /^inline/;
+
        var rinline = /^inline/;
  
var floating = '(\\-?[\\d\\.e]+)';
+
        var floating = '(\\-?[\\d\\.e]+)';
var commaSpace = '\\,?\\s*';
+
        var commaSpace = '\\,?\\s*';
var rmatrix = new RegExp(
+
        var rmatrix = new RegExp('^matrix\\(' + floating + commaSpace + floating + commaSpace + floating + commaSpace + floating + commaSpace + floating + commaSpace + floating + '\\)$');
'^matrix\\(' +
+
floating + commaSpace +
+
floating + commaSpace +
+
floating + commaSpace +
+
floating + commaSpace +
+
floating + commaSpace +
+
floating + '\\)$'
+
);
+
  
/**
+
        /**
 
* Utility for determing transform matrix equality
 
* Utility for determing transform matrix equality
 
* Checks backwards to test translation first
 
* Checks backwards to test translation first
Line 101: Line 94:
 
* @param {Array} second
 
* @param {Array} second
 
*/
 
*/
function matrixEquals(first, second) {
+
        function matrixEquals(first, second) {
var i = first.length;
+
                var i = first.length;
while(--i) {
+
                while (--i) {
if (+first[i] !== +second[i]) {
+
                        if ( + first[i] !== +second[i]) {
return false;
+
                                return false;
}
+
                        }
}
+
                }
return true;
+
                return true;
}
+
        }
  
/**
+
        /**
 
* Creates the options object for reset functions
 
* Creates the options object for reset functions
 
* @param {Boolean|Object} opts See reset methods
 
* @param {Boolean|Object} opts See reset methods
 
* @returns {Object} Returns the newly-created options object
 
* @returns {Object} Returns the newly-created options object
 
*/
 
*/
function createResetOptions(opts) {
+
        function createResetOptions(opts) {
var options = { range: true, animate: true };
+
                var options = {
if (typeof opts === 'boolean') {
+
                        range: true,
options.animate = opts;
+
                        animate: true
} else {
+
                };
$.extend(options, opts);
+
                if (typeof opts === 'boolean') {
}
+
                        options.animate = opts;
return options;
+
                } else {
}
+
                        $.extend(options, opts);
 +
                }
 +
                return options;
 +
        }
  
/**
+
        /**
 
* Represent a transformation matrix with a 3x3 matrix for calculations
 
* 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)
 
* 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
 
* @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) {
+
        function Matrix(a, b, c, d, e, f, g, h, i) {
if ($.type(a) === 'array') {
+
                if ($.type(a) === 'array') {
this.elements = [
+
                        this.elements = [ + a[0], +a[2], +a[4], +a[1], +a[3], +a[5], 0, 0, 1];
+a[0], +a[2], +a[4],
+
                } else {
+a[1], +a[3], +a[5],
+
                        this.elements = [a, b, c, d, e, f, g || 0, h || 0, i || 1];
    0,     0,     1
+
                }
];
+
        }
} else {
+
this.elements = [
+
a, b, c,
+
d, e, f,
+
g || 0, h || 0, i || 1
+
];
+
}
+
}
+
  
Matrix.prototype = {
+
        Matrix.prototype = {
/**
+
                /**
 
* Multiply a 3x3 matrix by a similar matrix or a vector
 
* Multiply a 3x3 matrix by a similar matrix or a vector
 
* @param {Matrix|Vector} matrix
 
* @param {Matrix|Vector} matrix
 
* @return {Matrix|Vector} Returns a vector if multiplying by a vector
 
* @return {Matrix|Vector} Returns a vector if multiplying by a vector
 
*/
 
*/
x: function(matrix) {
+
                x: function(matrix) {
var isVector = matrix instanceof Vector;
+
                        var isVector = matrix instanceof Vector;
  
var a = this.elements,
+
                        var a = this.elements,
b = matrix.elements;
+
                        b = matrix.elements;
  
if (isVector && b.length === 3) {
+
                        if (isVector && b.length === 3) {
// b is actually a vector
+
                                // b is actually a vector
return new 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]);
a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
+
                        } else if (b.length === a.length) {
a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
+
                                // b is a 3x3 matrix
a[6] * b[0] + a[7] * b[1] + a[8] * b[2]
+
                                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],
);
+
} 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[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[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[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]);
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
);
+
                },
}
+
                /**
return false; // fail
+
},
+
/**
+
 
* Generates an inverse of the current matrix
 
* Generates an inverse of the current matrix
 
* @returns {Matrix}
 
* @returns {Matrix}
 
*/
 
*/
inverse: function() {
+
                inverse: function() {
var d = 1 / this.determinant(),
+
                        var d = 1 / this.determinant(),
a = this.elements;
+
                        a = this.elements;
return new Matrix(
+
                        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[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[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[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[3] - a[6] * a[4]), d * ( - (a[7] * a[0] - a[6] * a[1])), d * (a[4] * a[0] - a[3] * a[1]));
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
 
* Calculates the determinant of the current matrix
 
* @returns {Number}
 
* @returns {Number}
 
*/
 
*/
determinant: function() {
+
                determinant: function() {
var a = this.elements;
+
                        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]);
+
                        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
 
* Create a vector containing three values
 
*/
 
*/
function Vector(x, y, z) {
+
        function Vector(x, y, z) {
this.elements = [ x, y, z ];
+
                this.elements = [x, y, z];
}
+
        }
  
/**
+
        /**
 
* Get the element at zero-indexed index i
 
* Get the element at zero-indexed index i
 
* @param {Number} i
 
* @param {Number} i
 
*/
 
*/
Vector.prototype.e = Matrix.prototype.e = function(i) {
+
        Vector.prototype.e = Matrix.prototype.e = function(i) {
return this.elements[ i ];
+
                return this.elements[i];
};
+
        };
  
/**
+
        /**
 
* Create a Panzoom object for a given element
 
* Create a Panzoom object for a given element
 
* @constructor
 
* @constructor
Line 243: Line 211:
 
* @param {Function} [options.on[Start|Change|Zoom|Pan|End|Reset] - Optional callbacks for panzoom events
 
* @param {Function} [options.on[Start|Change|Zoom|Pan|End|Reset] - Optional callbacks for panzoom events
 
*/
 
*/
function Panzoom(elem, options) {
+
        function Panzoom(elem, options) {
  
// Allow instantiation without `new` keyword
+
                // Allow instantiation without `new` keyword
if (!(this instanceof Panzoom)) {
+
                if (! (this instanceof Panzoom)) {
return new Panzoom(elem, options);
+
                        return new Panzoom(elem, options);
}
+
                }
  
// Sanity checks
+
                // Sanity checks
if (elem.nodeType !== 1) {
+
                if (elem.nodeType !== 1) {
$.error('Panzoom called on non-Element node');
+
                        $.error('Panzoom called on non-Element node');
}
+
                }
if (!$.contains(document, elem)) {
+
                if (!$.contains(document, elem)) {
$.error('Panzoom element must be attached to the document');
+
                        $.error('Panzoom element must be attached to the document');
}
+
                }
  
// Don't remake
+
                // Don't remake
var d = $.data(elem, datakey);
+
                var d = $.data(elem, datakey);
if (d) {
+
                if (d) {
return d;
+
                        return d;
}
+
                }
  
// Extend default with given object literal
+
                // Extend default with given object literal
// Each instance gets its own options
+
                // Each instance gets its own options
this.options = options = $.extend({}, Panzoom.defaults, options);
+
                this.options = options = $.extend({},
this.elem = elem;
+
                Panzoom.defaults, options);
var $elem = this.$elem = $(elem);
+
                this.elem = elem;
this.$set = options.$set && options.$set.length ? options.$set : $elem;
+
                var $elem = this.$elem = $(elem);
this.$doc = $(elem.ownerDocument || document);
+
                this.$set = options.$set && options.$set.length ? options.$set: $elem;
this.$parent = $elem.parent();
+
                this.$doc = $(elem.ownerDocument || document);
 +
                this.$parent = $elem.parent();
  
// This is SVG if the namespace is SVG
+
                // This is SVG if the namespace is SVG
// However, while <svg> elements are SVG, we want to treat those like other elements
+
                // 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.isSVG = rsvg.test(elem.namespaceURI) && elem.nodeName.toLowerCase() !== 'svg';
  
this.panning = false;
+
                this.panning = false;
  
// Save the original transform value
+
                // Save the original transform value
// Save the prefixed transform style key
+
                // Save the prefixed transform style key
// Set the starting transform
+
                // Set the starting transform
this._buildTransform();
+
                this._buildTransform();
  
// Build the appropriately-prefixed transform style property name
+
                // Build the appropriately-prefixed transform style property name
// De-camelcase
+
                // De-camelcase
this._transform = !this.isSVG && $.cssProps.transform.replace(rupper, '-$1').toLowerCase();
+
                this._transform = !this.isSVG && $.cssProps.transform.replace(rupper, '-$1').toLowerCase();
  
// Build the transition value
+
                // Build the transition value
this._buildTransition();
+
                this._buildTransition();
  
// Build containment dimensions
+
                // Build containment dimensions
this.resetDimensions();
+
                this.resetDimensions();
  
// Add zoom and reset buttons to `this`
+
                // Add zoom and reset buttons to `this`
var $empty = $();
+
                var $empty = $();
var self = this;
+
                var self = this;
$.each([ '$zoomIn', '$zoomOut', '$zoomRange', '$reset' ], function(i, name) {
+
                $.each(['$zoomIn', '$zoomOut', '$zoomRange', '$reset'],
self[ name ] = options[ name ] || $empty;
+
                function(i, name) {
});
+
                        self[name] = options[name] || $empty;
 +
                });
  
this.enable();
+
                this.enable();
  
// Save the instance
+
                // Save the instance
$.data(elem, datakey, this);
+
                $.data(elem, datakey, this);
}
+
        }
  
// Attach regex for possible use (immutable)
+
        // Attach regex for possible use (immutable)
Panzoom.rmatrix = rmatrix;
+
        Panzoom.rmatrix = rmatrix;
  
// Container for event names
+
        // Container for event names
Panzoom.events = $.pointertouch;
+
        Panzoom.events = $.pointertouch;
  
Panzoom.defaults = {
+
        Panzoom.defaults = {
// Should always be non-empty
+
                // Should always be non-empty
// Used to bind jQuery events without collisions
+
                // Used to bind jQuery events without collisions
// A guid is not added here as different instantiations/versions of panzoom
+
                // A guid is not added here as different instantiations/versions of panzoom
// on the same element is not supported, so don't do it.
+
                // on the same element is not supported, so don't do it.
eventNamespace: '.panzoom',
+
                eventNamespace: '.panzoom',
  
// Whether or not to transition the scale
+
                // Whether or not to transition the scale
transition: true,
+
                transition: true,
  
// Default cursor style for the element
+
                // Default cursor style for the element
cursor: 'move',
+
                cursor: 'move',
  
// There may be some use cases for zooming without panning or vice versa
+
                // There may be some use cases for zooming without panning or vice versa
disablePan: false,
+
                disablePan: false,
disableZoom: false,
+
                disableZoom: false,
  
// The increment at which to zoom
+
                // The increment at which to zoom
// adds/subtracts to the scale each time zoomIn/Out is called
+
                // adds/subtracts to the scale each time zoomIn/Out is called
increment: 0.3,
+
                increment: 0.3,
  
minScale: 0.4,
+
                minScale: 0.4,
maxScale: 5,
+
                maxScale: 5,
  
// The default step for the range input
+
                // The default step for the range input
// Precendence: default < HTML attribute < option setting
+
                // Precendence: default < HTML attribute < option setting
rangeStep: 0.05,
+
                rangeStep: 0.05,
  
// Animation duration (ms)
+
                // Animation duration (ms)
duration: 200,
+
                duration: 200,
// CSS easing used for scale transition
+
                // CSS easing used for scale transition
easing: 'ease-in-out',
+
                easing: 'ease-in-out',
  
// Indicate that the element should be contained within it's parent when panning
+
                // Indicate that the element should be contained within it's parent when panning
// Note: this does not affect zooming outside of the parent
+
                // 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)
+
                // 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
+
                // 'invert' is useful for a large panzoom element where you don't want to show anything behind it
contain: false
+
                contain: false
};
+
        };
  
Panzoom.prototype = {
+
        Panzoom.prototype = {
constructor: Panzoom,
+
                constructor: Panzoom,
  
/**
+
                /**
 
* @returns {Panzoom} Returns the instance
 
* @returns {Panzoom} Returns the instance
 
*/
 
*/
instance: function() {
+
                instance: function() {
return this;
+
                        return this;
},
+
                },
  
/**
+
                /**
 
* Enable or re-enable the panzoom instance
 
* Enable or re-enable the panzoom instance
 
*/
 
*/
enable: function() {
+
                enable: function() {
// Unbind first
+
                        // Unbind first
this._initStyle();
+
                        this._initStyle();
this._bind();
+
                        this._bind();
this.disabled = false;
+
                        this.disabled = false;
},
+
                },
  
/**
+
                /**
 
* Disable panzoom
 
* Disable panzoom
 
*/
 
*/
disable: function() {
+
                disable: function() {
this.disabled = true;
+
                        this.disabled = true;
this._resetStyle();
+
                        this._resetStyle();
this._unbind();
+
                        this._unbind();
},
+
                },
  
/**
+
                /**
 
* @returns {Boolean} Returns whether the current panzoom instance is disabled
 
* @returns {Boolean} Returns whether the current panzoom instance is disabled
 
*/
 
*/
isDisabled: function() {
+
                isDisabled: function() {
return this.disabled;
+
                        return this.disabled;
},
+
                },
  
/**
+
                /**
 
* Destroy the panzoom instance
 
* Destroy the panzoom instance
 
*/
 
*/
destroy: function() {
+
                destroy: function() {
this.disable();
+
                        this.disable();
$.removeData(this.elem, datakey);
+
                        $.removeData(this.elem, datakey);
},
+
                },
  
/**
+
                /**
 
* Builds the restricing dimensions from the containment element
 
* Builds the restricing dimensions from the containment element
 
* Also used with focal points
 
* Also used with focal points
 
* Call this method whenever the dimensions of the element or parent are changed
 
* Call this method whenever the dimensions of the element or parent are changed
 
*/
 
*/
resetDimensions: function() {
+
                resetDimensions: function() {
// Reset container properties
+
                        // Reset container properties
var $parent = this.$parent;
+
                        var $parent = this.$parent;
this.container = {
+
                        this.container = {
width: $parent.innerWidth(),
+
                                width: $parent.innerWidth(),
height: $parent.innerHeight()
+
                                height: $parent.innerHeight()
};
+
                        };
var po = $parent.offset();
+
                        var po = $parent.offset();
var elem = this.elem;
+
                        var elem = this.elem;
var $elem = this.$elem;
+
                        var $elem = this.$elem;
var dims;
+
                        var dims;
if (this.isSVG) {
+
                        if (this.isSVG) {
dims = elem.getBoundingClientRect();
+
                                dims = elem.getBoundingClientRect();
dims = {
+
                                dims = {
left: dims.left - po.left,
+
                                        left: dims.left - po.left,
top: dims.top - po.top,
+
                                        top: dims.top - po.top,
width: dims.width,
+
                                        width: dims.width,
height: dims.height,
+
                                        height: dims.height,
margin: { left: 0, top: 0 }
+
                                        margin: {
};
+
                                                left: 0,
} else {
+
                                                top: 0
dims = {
+
                                        }
left: $.css(elem, 'left', true) || 0,
+
                                };
top: $.css(elem, 'top', true) || 0,
+
                        } else {
width: $elem.innerWidth(),
+
                                dims = {
height: $elem.innerHeight(),
+
                                        left: $.css(elem, 'left', true) || 0,
margin: {
+
                                        top: $.css(elem, 'top', true) || 0,
top: $.css(elem, 'marginTop', true) || 0,
+
                                        width: $elem.innerWidth(),
left: $.css(elem, 'marginLeft', true) || 0
+
                                        height: $elem.innerHeight(),
}
+
                                        margin: {
};
+
                                                top: $.css(elem, 'marginTop', true) || 0,
}
+
                                                left: $.css(elem, 'marginLeft', true) || 0
dims.widthBorder = ($.css(elem, 'borderLeftWidth', true) + $.css(elem, 'borderRightWidth', true)) || 0;
+
                                        }
dims.heightBorder = ($.css(elem, 'borderTopWidth', true) + $.css(elem, 'borderBottomWidth', true)) || 0;
+
                                };
this.dimensions = dims;
+
                        }
},
+
                        dims.widthBorder = ($.css(elem, 'borderLeftWidth', true) + $.css(elem, 'borderRightWidth', true)) || 0;
 +
                        dims.heightBorder = ($.css(elem, 'borderTopWidth', true) + $.css(elem, 'borderBottomWidth', true)) || 0;
 +
                        this.dimensions = dims;
 +
                },
  
/**
+
                /**
 
* Return the element to it's original transform matrix
 
* 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] 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
 
* @param {Boolean} [options.silent] Silence the reset event
 
*/
 
*/
reset: function(options) {
+
                reset: function(options) {
options = createResetOptions(options);
+
                        options = createResetOptions(options);
// Reset the transform to its original value
+
                        // Reset the transform to its original value
var matrix = this.setMatrix(this._origTransform, options);
+
                        var matrix = this.setMatrix(this._origTransform, options);
if (!options.silent) {
+
                        if (!options.silent) {
this._trigger('reset', matrix);
+
                                this._trigger('reset', matrix);
}
+
                        }
},
+
                },
  
/**
+
                /**
 
* Only resets zoom level
 
* Only resets zoom level
 
* @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to zoom()
 
* @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to zoom()
 
*/
 
*/
resetZoom: function(options) {
+
                resetZoom: function(options) {
options = createResetOptions(options);
+
                        options = createResetOptions(options);
var origMatrix = this.getMatrix(this._origTransform);
+
                        var origMatrix = this.getMatrix(this._origTransform);
options.dValue = origMatrix[ 3 ];
+
                        options.dValue = origMatrix[3];
this.zoom(origMatrix[0], options);
+
                        this.zoom(origMatrix[0], options);
},
+
                },
  
/**
+
                /**
 
* Only reset panning
 
* Only reset panning
 
* @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to pan()
 
* @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to pan()
 
*/
 
*/
resetPan: function(options) {
+
                resetPan: function(options) {
var origMatrix = this.getMatrix(this._origTransform);
+
                        var origMatrix = this.getMatrix(this._origTransform);
this.pan(origMatrix[4], origMatrix[5], createResetOptions(options));
+
                        this.pan(origMatrix[4], origMatrix[5], createResetOptions(options));
},
+
                },
  
/**
+
                /**
 
* Sets a transform on the $set
 
* Sets a transform on the $set
 
* @param {String} transform
 
* @param {String} transform
 
*/
 
*/
setTransform: function(transform) {
+
                setTransform: function(transform) {
var method = this.isSVG ? 'attr' : 'style';
+
                        var method = this.isSVG ? 'attr': 'style';
var $set = this.$set;
+
                        var $set = this.$set;
var i = $set.length;
+
                        var i = $set.length;
while(i--) {
+
                        while (i--) {
$[method]($set[i], 'transform', transform);
+
                                $[method]($set[i], 'transform', transform);
}
+
                        }
},
+
                },
  
/**
+
                /**
 
* Retrieving the transform is different for SVG
 
* Retrieving the transform is different for SVG
 
*  (unless a style transform is already present)
 
*  (unless a style transform is already present)
Line 494: Line 467:
 
* @returns {String} Returns the current transform value of the element
 
* @returns {String} Returns the current transform value of the element
 
*/
 
*/
getTransform: function(transform) {
+
                getTransform: function(transform) {
var $set = this.$set;
+
                        var $set = this.$set;
var transformElem = $set[0];
+
                        var transformElem = $set[0];
if (transform) {
+
                        if (transform) {
this.setTransform(transform);
+
                                this.setTransform(transform);
} else {
+
                        } else {
// Retrieve the transform
+
                                // Retrieve the transform
transform = $[this.isSVG ? 'attr' : 'style'](transformElem, 'transform');
+
                                transform = $[this.isSVG ? 'attr': 'style'](transformElem, 'transform');
}
+
                        }
  
// Convert any transforms set by the user to matrix format
+
                        // Convert any transforms set by the user to matrix format
// by setting to computed
+
                        // by setting to computed
if (transform !== 'none' && !rmatrix.test(transform)) {
+
                        if (transform !== 'none' && !rmatrix.test(transform)) {
// Get computed and set for next time
+
                                // Get computed and set for next time
this.setTransform(transform = $.css(transformElem, 'transform'));
+
                                this.setTransform(transform = $.css(transformElem, 'transform'));
}
+
                        }
  
return transform || 'none';
+
                        return transform || 'none';
},
+
                },
  
/**
+
                /**
 
* Retrieve the current transform matrix for $elem (or turn a transform into it's array values)
 
* Retrieve the current transform matrix for $elem (or turn a transform into it's array values)
 
* @param {String} [transform] matrix-formatted transform value
 
* @param {String} [transform] matrix-formatted transform value
 
* @returns {Array} Returns the current transform matrix split up into it's parts, or a default matrix
 
* @returns {Array} Returns the current transform matrix split up into it's parts, or a default matrix
 
*/
 
*/
getMatrix: function(transform) {
+
                getMatrix: function(transform) {
var matrix = rmatrix.exec(transform || this.getTransform());
+
                        var matrix = rmatrix.exec(transform || this.getTransform());
if (matrix) {
+
                        if (matrix) {
matrix.shift();
+
                                matrix.shift();
}
+
                        }
return matrix || [ 1, 0, 0, 1, 0, 0 ];
+
                        return matrix || [1, 0, 0, 1, 0, 0];
},
+
                },
  
/**
+
                /**
 
* Given a matrix object, quickly set the current matrix of the element
 
* Given a matrix object, quickly set the current matrix of the element
 
* @param {Array|String} matrix
 
* @param {Array|String} matrix
Line 538: Line 511:
 
* @returns {Array} Returns the newly-set matrix
 
* @returns {Array} Returns the newly-set matrix
 
*/
 
*/
setMatrix: function(matrix, options) {
+
                setMatrix: function(matrix, options) {
if (this.disabled) { return; }
+
                        if (this.disabled) {
if (!options) { options = {}; }
+
                                return;
// Convert to array
+
                        }
if (typeof matrix === 'string') {
+
                        if (!options) {
matrix = this.getMatrix(matrix);
+
                                options = {};
}
+
                        }
var dims, container, marginW, marginH, diffW, diffH, left, top, width, height;
+
                        // Convert to array
var scale = +matrix[0];
+
                        if (typeof matrix === 'string') {
var $parent = this.$parent;
+
                                matrix = this.getMatrix(matrix);
var contain = typeof options.contain !== 'undefined' ? options.contain : this.options.contain;
+
                        }
 +
                        var dims, container, marginW, marginH, diffW, diffH, left, top, width, height;
 +
                        var scale = +matrix[0];
 +
                        var $parent = this.$parent;
 +
                        var contain = typeof options.contain !== 'undefined' ? options.contain: this.options.contain;
  
// Apply containment
+
                        // Apply containment
if (contain) {
+
                        if (contain) {
dims = this._checkDims();
+
                                dims = this._checkDims();
container = this.container;
+
                                container = this.container;
width = dims.width + dims.widthBorder;
+
                                width = dims.width + dims.widthBorder;
height = dims.height + dims.heightBorder;
+
                                height = dims.height + dims.heightBorder;
// Use absolute value of scale here as negative scale doesn't mean even smaller
+
                                // Use absolute value of scale here as negative scale doesn't mean even smaller
marginW = ((width * Math.abs(scale)) - container.width) / 2;
+
                                marginW = ((width * Math.abs(scale)) - container.width) / 2;
marginH = ((height * Math.abs(scale)) - container.height) / 2;
+
                                marginH = ((height * Math.abs(scale)) - container.height) / 2;
left = dims.left + dims.margin.left;
+
                                left = dims.left + dims.margin.left;
top = dims.top + dims.margin.top;
+
                                top = dims.top + dims.margin.top;
if (contain === 'invert') {
+
                                if (contain === 'invert') {
diffW = width > container.width ? width - container.width : 0;
+
                                        diffW = width > container.width ? width - container.width: 0;
diffH = height > container.height ? height - container.height : 0;
+
                                        diffH = height > container.height ? height - container.height: 0;
marginW += (container.width - width) / 2;
+
                                        marginW += (container.width - width) / 2;
marginH += (container.height - height) / 2;
+
                                        marginH += (container.height - height) / 2;
matrix[4] = Math.max(Math.min(matrix[4], marginW - left), -marginW - left - diffW);
+
                                        matrix[4] = Math.max(Math.min(matrix[4], marginW - left), -marginW - left - diffW);
matrix[5] = Math.max(Math.min(matrix[5], marginH - top), -marginH - top - diffH + dims.heightBorder);
+
                                        matrix[5] = Math.max(Math.min(matrix[5], marginH - top), -marginH - top - diffH + dims.heightBorder);
} else {
+
                                } else {
// marginW += dims.widthBorder / 2;
+
                                        // marginW += dims.widthBorder / 2;
marginH += dims.heightBorder / 2;
+
                                        marginH += dims.heightBorder / 2;
diffW = container.width > width ? container.width - width : 0;
+
                                        diffW = container.width > width ? container.width - width: 0;
diffH = container.height > height ? container.height - height : 0;
+
                                        diffH = container.height > height ? container.height - height: 0;
// If the element is not naturally centered, assume full margin right
+
                                        // If the element is not naturally centered, assume full margin right
if ($parent.css('textAlign') !== 'center' || !rinline.test($.css(this.elem, 'display'))) {
+
                                        if ($parent.css('textAlign') !== 'center' || !rinline.test($.css(this.elem, 'display'))) {
marginW = marginH = 0;
+
                                                marginW = marginH = 0;
} else {
+
                                        } else {
diffW = 0;
+
                                                diffW = 0;
}
+
                                        }
matrix[4] = Math.min(
+
                                        matrix[4] = Math.min(Math.max(matrix[4], marginW - left), -marginW - left + diffW);
Math.max(matrix[4], marginW - left),
+
                                        matrix[5] = Math.min(Math.max(matrix[5], marginH - top), -marginH - top + diffH);
-marginW - left + diffW
+
                                }
);
+
                        }
matrix[5] = Math.min(
+
                        if (options.animate !== 'skip') {
Math.max(matrix[5], marginH - top),
+
                                // Set transition
-marginH - top + diffH
+
                                this.transition(!options.animate);
);
+
                        }
}
+
                        // Update range
}
+
                        if (options.range) {
if (options.animate !== 'skip') {
+
                                this.$zoomRange.val(scale);
// Set transition
+
                        }
this.transition(!options.animate);
+
}
+
// Update range
+
if (options.range) {
+
this.$zoomRange.val(scale);
+
}
+
  
// Set the matrix on this.$set
+
                        // Set the matrix on this.$set
this.setTransform('matrix(' + matrix.join(',') + ')');
+
                        this.setTransform('matrix(' + matrix.join(',') + ')');
  
if (!options.silent) {
+
                        if (!options.silent) {
this._trigger('change', matrix);
+
                                this._trigger('change', matrix);
}
+
                        }
  
return matrix;
+
                        return matrix;
},
+
                },
  
/**
+
                /**
 
* @returns {Boolean} Returns whether the panzoom element is currently being dragged
 
* @returns {Boolean} Returns whether the panzoom element is currently being dragged
 
*/
 
*/
isPanning: function() {
+
                isPanning: function() {
return this.panning;
+
                        return this.panning;
},
+
                },
  
/**
+
                /**
 
* Apply the current transition to the element, if allowed
 
* Apply the current transition to the element, if allowed
 
* @param {Boolean} [off] Indicates that the transition should be turned off
 
* @param {Boolean} [off] Indicates that the transition should be turned off
 
*/
 
*/
transition: function(off) {
+
                transition: function(off) {
if (!this._transition) { return; }
+
                        if (!this._transition) {
var transition = off || !this.options.transition ? 'none' : this._transition;
+
                                return;
var $set = this.$set;
+
                        }
var i = $set.length;
+
                        var transition = off || !this.options.transition ? 'none': this._transition;
while(i--) {
+
                        var $set = this.$set;
// Avoid reflows when zooming
+
                        var i = $set.length;
if ($.style($set[i], 'transition') !== transition) {
+
                        while (i--) {
$.style($set[i], 'transition', transition);
+
                                // 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
 
* Pan the element to the specified translation X and Y
 
* Note: this is not the same as setting jQuery#offset() or jQuery#position()
 
* Note: this is not the same as setting jQuery#offset() or jQuery#position()
Line 642: Line 615:
 
* @param {Boolean} [options.relative] Make the x and y values relative to the existing matrix
 
* @param {Boolean} [options.relative] Make the x and y values relative to the existing matrix
 
*/
 
*/
pan: function(x, y, options) {
+
                pan: function(x, y, options) {
if (this.options.disablePan) { return; }
+
                        if (this.options.disablePan) {
if (!options) { options = {}; }
+
                                return;
var matrix = options.matrix;
+
                        }
if (!matrix) {
+
                        if (!options) {
matrix = this.getMatrix();
+
                                options = {};
}
+
                        }
// Cast existing matrix values to numbers
+
                        var matrix = options.matrix;
if (options.relative) {
+
                        if (!matrix) {
x += +matrix[4];
+
                                matrix = this.getMatrix();
y += +matrix[5];
+
                        }
}
+
                        // Cast existing matrix values to numbers
matrix[4] = x;
+
                        if (options.relative) {
matrix[5] = y;
+
                                x += +matrix[4];
this.setMatrix(matrix, options);
+
                                y += +matrix[5];
if (!options.silent) {
+
                        }
this._trigger('pan', matrix[4], 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
 
* 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 {Number|Boolean} [scale] The scale to which to zoom or a boolean indicating to transition a zoom out
Line 679: Line 656:
 
*  For instance, to flip vertically, you could set -1 as the dValue.
 
*  For instance, to flip vertically, you could set -1 as the dValue.
 
*/
 
*/
zoom: function(scale, opts) {
+
                zoom: function(scale, opts) {
// Shuffle arguments
+
                        // Shuffle arguments
if (typeof scale === 'object') {
+
                        if (typeof scale === 'object') {
opts = scale;
+
                                opts = scale;
scale = null;
+
                                scale = null;
} else if (!opts) {
+
                        } else if (!opts) {
opts = {};
+
                                opts = {};
}
+
                        }
var options = $.extend({}, this.options, opts);
+
                        var options = $.extend({},
// Check if disabled
+
                        this.options, opts);
if (options.disableZoom) { return; }
+
                        // Check if disabled
var animate = false;
+
                        if (options.disableZoom) {
var matrix = options.matrix || this.getMatrix();
+
                                return;
 +
                        }
 +
                        var animate = false;
 +
                        var matrix = options.matrix || this.getMatrix();
  
// Calculate zoom based on increment
+
                        // Calculate zoom based on increment
if (typeof scale !== 'number') {
+
                        if (typeof scale !== 'number') {
scale = +matrix[0] + (options.increment * (scale ? -1 : 1));
+
                                scale = +matrix[0] + (options.increment * (scale ? -1 : 1));
animate = true;
+
                                animate = true;
}
+
                        }
  
// Constrain scale
+
                        // Constrain scale
if (scale > options.maxScale) {
+
                        if (scale > options.maxScale) {
scale = options.maxScale;
+
                                scale = options.maxScale;
} else if (scale < options.minScale) {
+
                        } else if (scale < options.minScale) {
scale = options.minScale;
+
                                scale = options.minScale;
}
+
                        }
  
// Calculate focal point based on scale
+
                        // Calculate focal point based on scale
var focal = options.focal;
+
                        var focal = options.focal;
if (focal && !options.disablePan) {
+
                        if (focal && !options.disablePan) {
// Adapted from code by Florian Günther
+
                                // Adapted from code by Florian Günther
// https://github.com/florianguenther/zui53
+
                                // https://github.com/florianguenther/zui53
var dims = this._checkDims();
+
                                var dims = this._checkDims();
var clientX = focal.clientX;
+
                                var clientX = focal.clientX;
var clientY = focal.clientY;
+
                                var clientY = focal.clientY;
// Adjust the focal point for default transform-origin => 50% 50%
+
                                // Adjust the focal point for default transform-origin => 50% 50%
if (!this.isSVG) {
+
                                if (!this.isSVG) {
clientX -= (dims.width + dims.widthBorder) / 2;
+
                                        clientX -= (dims.width + dims.widthBorder) / 2;
clientY -= (dims.height + dims.heightBorder) / 2;
+
                                        clientY -= (dims.height + dims.heightBorder) / 2;
}
+
                                }
var clientV = new Vector(clientX, clientY, 1);
+
                                var clientV = new Vector(clientX, clientY, 1);
var surfaceM = new Matrix(matrix);
+
                                var surfaceM = new Matrix(matrix);
// Supply an offset manually if necessary
+
                                // Supply an offset manually if necessary
var o = this.parentOffset || this.$parent.offset();
+
                                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 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 surfaceV = surfaceM.inverse().x(offsetM.inverse().x(clientV));
var scaleBy = scale / matrix[0];
+
                                var scaleBy = scale / matrix[0];
surfaceM = surfaceM.x(new Matrix([ scaleBy, 0, 0, scaleBy, 0, 0 ]));
+
                                surfaceM = surfaceM.x(new Matrix([scaleBy, 0, 0, scaleBy, 0, 0]));
clientV = offsetM.x(surfaceM.x(surfaceV));
+
                                clientV = offsetM.x(surfaceM.x(surfaceV));
matrix[4] = +matrix[4] + (clientX - clientV.e(0));
+
                                matrix[4] = +matrix[4] + (clientX - clientV.e(0));
matrix[5] = +matrix[5] + (clientY - clientV.e(1));
+
                                matrix[5] = +matrix[5] + (clientY - clientV.e(1));
}
+
                        }
  
// Set the scale
+
                        // Set the scale
matrix[0] = scale;
+
                        matrix[0] = scale;
matrix[3] = typeof options.dValue === 'number' ? options.dValue : scale;
+
                        matrix[3] = typeof options.dValue === 'number' ? options.dValue: scale;
  
// Calling zoom may still pan the element
+
                        // Calling zoom may still pan the element
this.setMatrix(matrix, {
+
                        this.setMatrix(matrix, {
animate: typeof options.animate === 'boolean' ? options.animate : animate,
+
                                animate: typeof options.animate === 'boolean' ? options.animate: animate,
// Set the zoomRange value
+
                                // Set the zoomRange value
range: !options.noSetRange
+
                                range: !options.noSetRange
});
+
                        });
  
// Trigger zoom event
+
                        // Trigger zoom event
if (!options.silent) {
+
                        if (!options.silent) {
this._trigger('zoom', matrix[0], options);
+
                                this._trigger('zoom', matrix[0], options);
}
+
                        }
},
+
                },
  
/**
+
                /**
 
* Get/set option on an existing instance
 
* Get/set option on an existing instance
 
* @returns {Array|undefined} If getting, returns an array of all values
 
* @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.
 
*  on each instance for a given key. If setting, continue chaining by returning undefined.
 
*/
 
*/
option: function(key, value) {
+
                option: function(key, value) {
var options;
+
                        var options;
if (!key) {
+
                        if (!key) {
// Avoids returning direct reference
+
                                // Avoids returning direct reference
return $.extend({}, this.options);
+
                                return $.extend({},
}
+
                                this.options);
 +
                        }
  
if (typeof key === 'string') {
+
                        if (typeof key === 'string') {
if (arguments.length === 1) {
+
                                if (arguments.length === 1) {
return this.options[ key ] !== undefined ?
+
                                        return this.options[key] !== undefined ? this.options[key] : null;
this.options[ key ] :
+
                                }
null;
+
                                options = {};
}
+
                                options[key] = value;
options = {};
+
                        } else {
options[ key ] = value;
+
                                options = key;
} else {
+
                        }
options = key;
+
}
+
  
this._setOptions(options);
+
                        this._setOptions(options);
},
+
                },
  
/**
+
                /**
 
* Internally sets options
 
* Internally sets options
 
* @param {Object} options - An object literal of options to set
 
* @param {Object} options - An object literal of options to set
 
*/
 
*/
_setOptions: function(options) {
+
                _setOptions: function(options) {
$.each(options, $.proxy(function(key, value) {
+
                        $.each(options, $.proxy(function(key, value) {
switch(key) {
+
                                switch (key) {
case 'disablePan':
+
                                case 'disablePan':
this._resetStyle();
+
                                        this._resetStyle();
/* falls through */
+
                                        /* falls through */
case '$zoomIn':
+
                                case '$zoomIn':
case '$zoomOut':
+
                                case '$zoomOut':
case '$zoomRange':
+
                                case '$zoomRange':
case '$reset':
+
                                case '$reset':
case 'disableZoom':
+
                                case 'disableZoom':
case 'onStart':
+
                                case 'onStart':
case 'onChange':
+
                                case 'onChange':
case 'onZoom':
+
                                case 'onZoom':
case 'onPan':
+
                                case 'onPan':
case 'onEnd':
+
                                case 'onEnd':
case 'onReset':
+
                                case 'onReset':
case 'eventNamespace':
+
                                case 'eventNamespace':
this._unbind();
+
                                        this._unbind();
}
+
                                }
this.options[ key ] = value;
+
                                this.options[key] = value;
switch(key) {
+
                                switch (key) {
case 'disablePan':
+
                                case 'disablePan':
this._initStyle();
+
                                        this._initStyle();
/* falls through */
+
                                        /* falls through */
case '$zoomIn':
+
                                case '$zoomIn':
case '$zoomOut':
+
                                case '$zoomOut':
case '$zoomRange':
+
                                case '$zoomRange':
case '$reset':
+
                                case '$reset':
// Set these on the instance
+
                                        // Set these on the instance
this[ key ] = value;
+
                                        this[key] = value;
/* falls through */
+
                                        /* falls through */
case 'disableZoom':
+
                                case 'disableZoom':
case 'onStart':
+
                                case 'onStart':
case 'onChange':
+
                                case 'onChange':
case 'onZoom':
+
                                case 'onZoom':
case 'onPan':
+
                                case 'onPan':
case 'onEnd':
+
                                case 'onEnd':
case 'onReset':
+
                                case 'onReset':
case 'eventNamespace':
+
                                case 'eventNamespace':
this._bind();
+
                                        this._bind();
break;
+
                                        break;
case 'cursor':
+
                                case 'cursor':
$.style(this.elem, 'cursor', value);
+
                                        $.style(this.elem, 'cursor', value);
break;
+
                                        break;
case 'minScale':
+
                                case 'minScale':
this.$zoomRange.attr('min', value);
+
                                        this.$zoomRange.attr('min', value);
break;
+
                                        break;
case 'maxScale':
+
                                case 'maxScale':
this.$zoomRange.attr('max', value);
+
                                        this.$zoomRange.attr('max', value);
break;
+
                                        break;
case 'rangeStep':
+
                                case 'rangeStep':
this.$zoomRange.attr('step', value);
+
                                        this.$zoomRange.attr('step', value);
break;
+
                                        break;
case 'startTransform':
+
                                case 'startTransform':
this._buildTransform();
+
                                        this._buildTransform();
break;
+
                                        break;
case 'duration':
+
                                case 'duration':
case 'easing':
+
                                case 'easing':
this._buildTransition();
+
                                        this._buildTransition();
/* falls through */
+
                                        /* falls through */
case 'transition':
+
                                case 'transition':
this.transition();
+
                                        this.transition();
break;
+
                                        break;
case '$set':
+
                                case '$set':
if (value instanceof $ && value.length) {
+
                                        if (value instanceof $ && value.length) {
this.$set = value;
+
                                                this.$set = value;
// Reset styles
+
                                                // Reset styles
this._initStyle();
+
                                                this._initStyle();
this._buildTransform();
+
                                                this._buildTransform();
}
+
                                        }
}
+
                                }
}, this));
+
                        },
},
+
                        this));
 +
                },
  
/**
+
                /**
 
* Initialize base styles for the element and its parent
 
* Initialize base styles for the element and its parent
 
*/
 
*/
_initStyle: function() {
+
                _initStyle: function() {
var styles = {
+
                        var styles = {
// Promote the element to it's own compositor layer
+
                                // Promote the element to it's own compositor layer
'backface-visibility': 'hidden',
+
                                'backface-visibility': 'hidden',
// Set to defaults for the namespace
+
                                // Set to defaults for the namespace
'transform-origin': this.isSVG ? '0 0' : '50% 50%'
+
                                'transform-origin': this.isSVG ? '0 0': '50% 50%'
};
+
                        };
// Set elem styles
+
                        // Set elem styles
if (!this.options.disablePan) {
+
                        if (!this.options.disablePan) {
styles.cursor = this.options.cursor;
+
                                styles.cursor = this.options.cursor;
}
+
                        }
this.$set.css(styles);
+
                        this.$set.css(styles);
  
// Set parent to relative if set to static
+
                        // Set parent to relative if set to static
var $parent = this.$parent;
+
                        var $parent = this.$parent;
// No need to add styles to the body
+
                        // No need to add styles to the body
if ($parent.length && !$.nodeName($parent[0], 'body')) {
+
                        if ($parent.length && !$.nodeName($parent[0], 'body')) {
styles = {
+
                                styles = {
overflow: 'hidden'
+
                                        overflow: 'hidden'
};
+
                                };
if ($parent.css('position') === 'static') {
+
                                if ($parent.css('position') === 'static') {
styles.position = 'relative';
+
                                        styles.position = 'relative';
}
+
                                }
$parent.css(styles);
+
                                $parent.css(styles);
}
+
                        }
},
+
                },
  
/**
+
                /**
 
* Undo any styles attached in this plugin
 
* Undo any styles attached in this plugin
 
*/
 
*/
_resetStyle: function() {
+
                _resetStyle: function() {
this.$elem.css({
+
                        this.$elem.css({
'cursor': '',
+
                                'cursor': '',
'transition': ''
+
                                'transition': ''
});
+
                        });
this.$parent.css({
+
                        this.$parent.css({
'overflow': '',
+
                                'overflow': '',
'position': ''
+
                                'position': ''
});
+
                        });
},
+
                },
  
/**
+
                /**
 
* Binds all necessary events
 
* Binds all necessary events
 
*/
 
*/
_bind: function() {
+
                _bind: function() {
var self = this;
+
                        var self = this;
var options = this.options;
+
                        var options = this.options;
var ns = options.eventNamespace;
+
                        var ns = options.eventNamespace;
var str_start = pointerEvents ? 'pointerdown' + ns : ('touchstart' + ns + ' mousedown' + ns);
+
                        var str_start = pointerEvents ? 'pointerdown' + ns: ('touchstart' + ns + ' mousedown' + ns);
var str_click = pointerEvents ? 'pointerup' + ns : ('touchend' + ns + ' click' + ns);
+
                        var str_click = pointerEvents ? 'pointerup' + ns: ('touchend' + ns + ' click' + ns);
var events = {};
+
                        var events = {};
var $reset = this.$reset;
+
                        var $reset = this.$reset;
var $zoomRange = this.$zoomRange;
+
                        var $zoomRange = this.$zoomRange;
  
// Bind panzoom events from options
+
                        // Bind panzoom events from options
$.each([ 'Start', 'Change', 'Zoom', 'Pan', 'End', 'Reset' ], function() {
+
                        $.each(['Start', 'Change', 'Zoom', 'Pan', 'End', 'Reset'],
var m = options[ 'on' + this ];
+
                        function() {
if ($.isFunction(m)) {
+
                                var m = options['on' + this];
events[ 'panzoom' + this.toLowerCase() + ns ] = m;
+
                                if ($.isFunction(m)) {
}
+
                                        events['panzoom' + this.toLowerCase() + ns] = m;
});
+
                                }
 +
                        });
  
// Bind $elem drag and click/touchdown events
+
                        // Bind $elem drag and click/touchdown events
// Bind touchstart if either panning or zooming is enabled
+
                        // Bind touchstart if either panning or zooming is enabled
if (!options.disablePan || !options.disableZoom) {
+
                        if (!options.disablePan || !options.disableZoom) {
events[ str_start ] = function(e) {
+
                                events[str_start] = function(e) {
var touches;
+
                                        var touches;
if (e.type === 'touchstart' ?
+
                                        if (e.type === 'touchstart' ?
// Touch
+
                                        // Touch
(touches = e.touches) &&
+
                                        (touches = e.touches) && ((touches.length === 1 && !options.disablePan) || touches.length === 2) :
((touches.length === 1 && !options.disablePan) || touches.length === 2) :
+
                                        // Mouse/Pointer: Ignore right click
// Mouse/Pointer: Ignore right click
+
                                        ! options.disablePan && e.which === 1) {
!options.disablePan && e.which === 1) {
+
  
e.preventDefault();
+
                                                e.preventDefault();
e.stopPropagation();
+
                                                e.stopPropagation();
self._startMove(e, touches);
+
                                                self._startMove(e, touches);
}
+
                                        }
};
+
                                };
}
+
                        }
this.$elem.on(events);
+
                        this.$elem.on(events);
  
// Bind reset
+
                        // Bind reset
if ($reset.length) {
+
                        if ($reset.length) {
$reset.on(str_click, function(e) {
+
                                $reset.on(str_click,
e.preventDefault();
+
                                function(e) {
self.reset();
+
                                        e.preventDefault();
});
+
                                        self.reset();
}
+
                                });
 +
                        }
  
// Set default attributes for the range input
+
                        // Set default attributes for the range input
if ($zoomRange.length) {
+
                        if ($zoomRange.length) {
$zoomRange.attr({
+
                                $zoomRange.attr({
// Only set the range step if explicit or
+
                                        // Only set the range step if explicit or
// set the default if there is no attribute present
+
                                        // set the default if there is no attribute present
step: options.rangeStep === Panzoom.defaults.rangeStep &&
+
                                        step: options.rangeStep === Panzoom.defaults.rangeStep && $zoomRange.attr('step') || options.rangeStep,
$zoomRange.attr('step') ||
+
                                        min: options.minScale,
options.rangeStep,
+
                                        max: options.maxScale
min: options.minScale,
+
                                }).prop({
max: options.maxScale
+
                                        value: this.getMatrix()[0]
}).prop({
+
                                });
value: this.getMatrix()[0]
+
                        }
});
+
}
+
  
// No bindings if zooming is disabled
+
                        // No bindings if zooming is disabled
if (options.disableZoom) {
+
                        if (options.disableZoom) {
return;
+
                                return;
}
+
                        }
  
var $zoomIn = this.$zoomIn;
+
                        var $zoomIn = this.$zoomIn;
var $zoomOut = this.$zoomOut;
+
                        var $zoomOut = this.$zoomOut;
  
// Bind zoom in/out
+
                        // Bind zoom in/out
// Don't bind one without the other
+
                        // Don't bind one without the other
if ($zoomIn.length && $zoomOut.length) {
+
                        if ($zoomIn.length && $zoomOut.length) {
// preventDefault cancels future mouse events on touch events
+
                                // preventDefault cancels future mouse events on touch events
$zoomIn.on(str_click, function(e) {
+
                                $zoomIn.on(str_click,
e.preventDefault();
+
                                function(e) {
self.zoom();
+
                                        e.preventDefault();
});
+
                                        self.zoom();
$zoomOut.on(str_click, function(e) {
+
                                });
e.preventDefault();
+
                                $zoomOut.on(str_click,
self.zoom(true);
+
                                function(e) {
});
+
                                        e.preventDefault();
}
+
                                        self.zoom(true);
 +
                                });
 +
                        }
  
if ($zoomRange.length) {
+
                        if ($zoomRange.length) {
events = {};
+
                                events = {};
// Cannot prevent default action here, just use pointerdown/mousedown
+
                                // Cannot prevent default action here, just use pointerdown/mousedown
events[ (pointerEvents ? 'pointerdown' : 'mousedown') + ns ] = function() {
+
                                events[(pointerEvents ? 'pointerdown': 'mousedown') + ns] = function() {
self.transition(true);
+
                                        self.transition(true);
};
+
                                };
// Zoom on input events if available and change events
+
                                // Zoom on input events if available and change events
// See https://github.com/timmywil/jquery.panzoom/issues/90
+
                                // See https://github.com/timmywil/jquery.panzoom/issues/90
events[ (supportsInputEvent ? 'input' : 'change') + ns ] = function() {
+
                                events[(supportsInputEvent ? 'input': 'change') + ns] = function() {
self.zoom(+this.value, { noSetRange: true });
+
                                        self.zoom( + this.value, {
};
+
                                                noSetRange: true
$zoomRange.on(events);
+
                                        });
}
+
                                };
},
+
                                $zoomRange.on(events);
 +
                        }
 +
                },
  
/**
+
                /**
 
* Unbind all events
 
* Unbind all events
 
*/
 
*/
_unbind: function() {
+
                _unbind: function() {
this.$elem
+
                        this.$elem.add(this.$zoomIn).add(this.$zoomOut).add(this.$reset).off(this.options.eventNamespace);
.add(this.$zoomIn)
+
                },
.add(this.$zoomOut)
+
.add(this.$reset)
+
.off(this.options.eventNamespace);
+
},
+
  
/**
+
                /**
 
* Builds the original transform value
 
* Builds the original transform value
 
*/
 
*/
_buildTransform: function() {
+
                _buildTransform: function() {
// Save the original transform
+
                        // Save the original transform
// Retrieving this also adds the correct prefixed style name
+
                        // Retrieving this also adds the correct prefixed style name
// to jQuery's internal $.cssProps
+
                        // to jQuery's internal $.cssProps
return this._origTransform = this.getTransform(this.options.startTransform);
+
                        return this._origTransform = this.getTransform(this.options.startTransform);
},
+
                },
  
/**
+
                /**
 
* Set transition property for later use when zooming
 
* Set transition property for later use when zooming
 
* If SVG, create necessary animations elements for translations and scaling
 
* If SVG, create necessary animations elements for translations and scaling
 
*/
 
*/
_buildTransition: function() {
+
                _buildTransition: function() {
if (this._transform) {
+
                        if (this._transform) {
var options = this.options;
+
                                var options = this.options;
this._transition = this._transform + ' ' + options.duration + 'ms ' + options.easing;
+
                                this._transition = this._transform + ' ' + options.duration + 'ms ' + options.easing;
}
+
                        }
},
+
                },
  
/**
+
                /**
 
* Checks dimensions to make sure they don't need to be re-calculated
 
* Checks dimensions to make sure they don't need to be re-calculated
 
*/
 
*/
_checkDims: function() {
+
                _checkDims: function() {
var dims = this.dimensions;
+
                        var dims = this.dimensions;
// Rebuild if width or height is still 0
+
                        // Rebuild if width or height is still 0
if (!dims.width || !dims.height) {
+
                        if (!dims.width || !dims.height) {
this.resetDimensions();
+
                                this.resetDimensions();
}
+
                        }
return this.dimensions;
+
                        return this.dimensions;
},
+
                },
  
/**
+
                /**
 
* Calculates the distance between two touch points
 
* Calculates the distance between two touch points
 
* Remember pythagorean?
 
* Remember pythagorean?
Line 1,050: Line 1,029:
 
* @returns {Number} Returns the distance
 
* @returns {Number} Returns the distance
 
*/
 
*/
_getDistance: function(touches) {
+
                _getDistance: function(touches) {
var touch1 = touches[0];
+
                        var touch1 = touches[0];
var touch2 = touches[1];
+
                        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));
+
                        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
 
* Constructs an approximated point in the middle of two touch points
 
* @returns {Object} Returns an object containing pageX and pageY
 
* @returns {Object} Returns an object containing pageX and pageY
 
*/
 
*/
_getMiddle: function(touches) {
+
                _getMiddle: function(touches) {
var touch1 = touches[0];
+
                        var touch1 = touches[0];
var touch2 = touches[1];
+
                        var touch2 = touches[1];
return {
+
                        return {
clientX: ((touch2.clientX - touch1.clientX) / 2) + touch1.clientX,
+
                                clientX: ((touch2.clientX - touch1.clientX) / 2) + touch1.clientX,
clientY: ((touch2.clientY - touch1.clientY) / 2) + touch1.clientY
+
                                clientY: ((touch2.clientY - touch1.clientY) / 2) + touch1.clientY
};
+
                        };
},
+
                },
  
/**
+
                /**
 
* Trigger a panzoom event on our element
 
* Trigger a panzoom event on our element
 
* The event is passed the Panzoom instance
 
* The event is passed the Panzoom instance
Line 1,075: Line 1,054:
 
* @param {Mixed} arg1[, arg2, arg3, ...] Arguments to append to the trigger
 
* @param {Mixed} arg1[, arg2, arg3, ...] Arguments to append to the trigger
 
*/
 
*/
_trigger: function (event) {
+
                _trigger: function(event) {
if (typeof event === 'string') {
+
                        if (typeof event === 'string') {
event = 'panzoom' + event;
+
                                event = 'panzoom' + event;
}
+
                        }
this.$elem.triggerHandler(event, [this].concat(slice.call(arguments, 1)));
+
                        this.$elem.triggerHandler(event, [this].concat(slice.call(arguments, 1)));
},
+
                },
  
/**
+
                /**
 
* Starts the pan
 
* Starts the pan
 
* This is bound to mouse/touchmove on the element
 
* This is bound to mouse/touchmove on the element
Line 1,088: Line 1,067:
 
* @param {TouchList} [touches] The touches list if present
 
* @param {TouchList} [touches] The touches list if present
 
*/
 
*/
_startMove: function(event, touches) {
+
                _startMove: function(event, touches) {
var move, moveEvent, endEvent,
+
                        var move, moveEvent, endEvent, startDistance, startScale, startMiddle, startPageX, startPageY;
startDistance, startScale, startMiddle,
+
                        var self = this;
startPageX, startPageY;
+
                        var options = this.options;
var self = this;
+
                        var ns = options.eventNamespace;
var options = this.options;
+
                        var matrix = this.getMatrix();
var ns = options.eventNamespace;
+
                        var original = matrix.slice(0);
var matrix = this.getMatrix();
+
                        var origPageX = +original[4];
var original = matrix.slice(0);
+
                        var origPageY = +original[5];
var origPageX = +original[4];
+
                        var panOptions = {
var origPageY = +original[5];
+
                                matrix: matrix,
var panOptions = { matrix: matrix, animate: 'skip' };
+
                                animate: 'skip'
 +
                        };
  
// Use proper events
+
                        // Use proper events
if (pointerEvents) {
+
                        if (pointerEvents) {
moveEvent = 'pointermove';
+
                                moveEvent = 'pointermove';
endEvent = 'pointerup';
+
                                endEvent = 'pointerup';
} else if (event.type === 'touchstart') {
+
                        } else if (event.type === 'touchstart') {
moveEvent = 'touchmove';
+
                                moveEvent = 'touchmove';
endEvent = 'touchend';
+
                                endEvent = 'touchend';
} else {
+
                        } else {
moveEvent = 'mousemove';
+
                                moveEvent = 'mousemove';
endEvent = 'mouseup';
+
                                endEvent = 'mouseup';
}
+
                        }
  
// Add namespace
+
                        // Add namespace
moveEvent += ns;
+
                        moveEvent += ns;
endEvent += ns;
+
                        endEvent += ns;
  
// Remove any transitions happening
+
                        // Remove any transitions happening
this.transition(true);
+
                        this.transition(true);
  
// Indicate that we are currently panning
+
                        // Indicate that we are currently panning
this.panning = true;
+
                        this.panning = true;
  
// Trigger start event
+
                        // Trigger start event
this._trigger('start', event, touches);
+
                        this._trigger('start', event, touches);
  
if (touches && touches.length === 2) {
+
                        if (touches && touches.length === 2) {
startDistance = this._getDistance(touches);
+
                                startDistance = this._getDistance(touches);
startScale = +matrix[0];
+
                                startScale = +matrix[0];
startMiddle = this._getMiddle(touches);
+
                                startMiddle = this._getMiddle(touches);
move = function(e) {
+
                                move = function(e) {
e.preventDefault();
+
                                        e.preventDefault();
  
// Calculate move on middle point
+
                                        // Calculate move on middle point
var middle = self._getMiddle(touches = e.touches);
+
                                        var middle = self._getMiddle(touches = e.touches);
var diff = self._getDistance(touches) - startDistance;
+
                                        var diff = self._getDistance(touches) - startDistance;
  
// Set zoom
+
                                        // Set zoom
self.zoom(diff * (options.increment / 100) + startScale, {
+
                                        self.zoom(diff * (options.increment / 100) + startScale, {
focal: middle,
+
                                                focal: middle,
matrix: matrix,
+
                                                matrix: matrix,
animate: false
+
                                                animate: false
});
+
                                        });
  
// Set pan
+
                                        // Set pan
self.pan(
+
                                        self.pan( + matrix[4] + middle.clientX - startMiddle.clientX, +matrix[5] + middle.clientY - startMiddle.clientY, panOptions);
+matrix[4] + middle.clientX - startMiddle.clientX,
+
                                        startMiddle = middle;
+matrix[5] + middle.clientY - startMiddle.clientY,
+
                                };
panOptions
+
                        } else {
);
+
                                startPageX = event.pageX;
startMiddle = middle;
+
                                startPageY = event.pageY;
};
+
} else {
+
startPageX = event.pageX;
+
startPageY = event.pageY;
+
  
/**
+
                                /**
 
* Mousemove/touchmove function to pan the element
 
* Mousemove/touchmove function to pan the element
 
* @param {Object} e Event object
 
* @param {Object} e Event object
 
*/
 
*/
move = function(e) {
+
                                move = function(e) {
e.preventDefault();
+
                                        e.preventDefault();
self.pan(
+
                                        self.pan(origPageX + e.pageX - startPageX, origPageY + e.pageY - startPageY, panOptions);
origPageX + e.pageX - startPageX,
+
                                };
origPageY + e.pageY - startPageY,
+
                        }
panOptions
+
);
+
};
+
}
+
  
// Bind the handlers
+
                        // Bind the handlers
$(document)
+
                        $(document).off(ns).on(moveEvent, move).on(endEvent,
.off(ns)
+
                        function(e) {
.on(moveEvent, move)
+
                                e.preventDefault();
.on(endEvent, function(e) {
+
                                // Unbind all document events
e.preventDefault();
+
                                $(this).off(ns);
// Unbind all document events
+
                                self.panning = false;
$(this).off(ns);
+
                                // Trigger our end event
self.panning = false;
+
                                // Simply set the type to "panzoomend" to pass through all end properties
// Trigger our end event
+
                                // jQuery's `not` is used here to compare Array equality
// Simply set the type to "panzoomend" to pass through all end properties
+
                                e.type = 'panzoomend';
// jQuery's `not` is used here to compare Array equality
+
                                self._trigger(e, matrix, !matrixEquals(matrix, original));
e.type = 'panzoomend';
+
                        });
self._trigger(e, matrix, !matrixEquals(matrix, original));
+
                }
});
+
        };
}
+
};
+
  
// Add Panzoom as a static property
+
        // Add Panzoom as a static property
$.Panzoom = Panzoom;
+
        $.Panzoom = Panzoom;
  
/**
+
        /**
 
* Extend jQuery
 
* Extend jQuery
 
* @param {Object|String} options - The name of a method to call on the prototype
 
* @param {Object|String} options - The name of a method to call on the prototype
Line 1,197: Line 1,167:
 
* @returns {jQuery|Mixed} jQuery instance for regular chaining or the return value(s) of a panzoom method call
 
* @returns {jQuery|Mixed} jQuery instance for regular chaining or the return value(s) of a panzoom method call
 
*/
 
*/
$.fn.panzoom = function(options) {
+
        $.fn.panzoom = function(options) {
var instance, args, m, ret;
+
                var instance, args, m, ret;
  
// Call methods widget-style
+
                // Call methods widget-style
if (typeof options === 'string') {
+
                if (typeof options === 'string') {
ret = [];
+
                        ret = [];
args = slice.call(arguments, 1);
+
                        args = slice.call(arguments, 1);
this.each(function() {
+
                        this.each(function() {
instance = $.data(this, datakey);
+
                                instance = $.data(this, datakey);
  
if (!instance) {
+
                                if (!instance) {
ret.push(undefined);
+
                                        ret.push(undefined);
  
// Ignore methods beginning with `_`
+
                                        // Ignore methods beginning with `_`
} else if (options.charAt(0) !== '_' &&
+
                                } else if (options.charAt(0) !== '_' && typeof(m = instance[options]) === 'function' &&
typeof (m = instance[ options ]) === 'function' &&
+
                                // If nothing is returned, do not add to return values
// If nothing is returned, do not add to return values
+
                                (m = m.apply(instance, args)) !== undefined) {
(m = m.apply(instance, args)) !== undefined) {
+
  
ret.push(m);
+
                                        ret.push(m);
}
+
                                }
});
+
                        });
  
// Return an array of values for the jQuery instances
+
                        // Return an array of values for the jQuery instances
// Or the value itself if there is only one
+
                        // Or the value itself if there is only one
// Or keep chaining
+
                        // Or keep chaining
return ret.length ?
+
                        return ret.length ? (ret.length === 1 ? ret[0] : ret) : this;
(ret.length === 1 ? ret[0] : ret) :
+
                }
this;
+
}
+
  
return this.each(function() { new Panzoom(this, options); });
+
                return this.each(function() {
};
+
                        new Panzoom(this, options);
 +
                });
 +
        };
  
return Panzoom;
+
        return Panzoom;
 
}));
 
}));

Latest revision as of 08:37, 29 August 2017

(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';
       // Common properties to lift for touch or pointer events
       var list = 'over out down up move enter leave cancel'.split(' ');
       var hook = $.extend({},
       $.event.mouseHooks);
       var events = {};
       // Support pointer events in IE11+ if available
       if (window.PointerEvent) {
               $.each(list,
               function(i, name) {
                       // Add event name to events property and add fixHook
                       $.event.fixHooks[(events[name] = 'pointer' + name)] = hook;
               });
       } else {
               var mouseProps = hook.props;
               // Add touch properties for the touch hook
               hook.props = mouseProps.concat(['touches', 'changedTouches', 'targetTouches', 'altKey', 'ctrlKey', 'metaKey', 'shiftKey']);
               /**

* Support: Android * Android sets pageX/Y to 0 for any touch event * Attach first touch's pageX/pageY and clientX/clientY if not set correctly */

               hook.filter = function(event, originalEvent) {
                       var touch;
                       var i = mouseProps.length;
                       if (!originalEvent.pageX && originalEvent.touches && (touch = originalEvent.touches[0])) {
                               // Copy over all mouse properties
                               while (i--) {
                                       event[mouseProps[i]] = touch[mouseProps[i]];
                               }
                       }
                       return event;
               };
               $.each(list,
               function(i, name) {
                       // No equivalent touch events for over and out
                       if (i < 2) {
                               events[name] = 'mouse' + name;
                       } else {
                               var touch = 'touch' + (name === 'down' ? 'start': name === 'up' ? 'end': name);
                               // Add fixHook
                               $.event.fixHooks[touch] = hook;
                               // Add event names to events property
                               events[name] = touch + ' mouse' + name;
                       }
               });
       }
       $.pointertouch = events;
       var document = window.document;
       var datakey = '__pz__';
       var slice = Array.prototype.slice;
       var pointerEvents = !!window.PointerEvent;
       var supportsInputEvent = (function() {
               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 rinline = /^inline/;
       var floating = '(\\-?[\\d\\.e]+)';
       var commaSpace = '\\,?\\s*';
       var rmatrix = new RegExp('^matrix\\(' + floating + commaSpace + floating + commaSpace + floating + commaSpace + floating + commaSpace + floating + commaSpace + floating + '\\)$');
       /**

* Utility for determing 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 ( + first[i] !== +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 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 = !this.isSVG && $.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();
               // Save the instance
               $.data(elem, datakey, this);
       }
       // Attach regex for possible use (immutable)
       Panzoom.rmatrix = rmatrix;
       // Container for event names
       Panzoom.events = $.pointertouch;
       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,
               // The increment at which to zoom
               // adds/subtracts to the scale each time zoomIn/Out is called
               increment: 0.3,
               minScale: 0.4,
               maxScale: 5,
               // 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
                       var $parent = this.$parent;
                       this.container = {
                               width: $parent.innerWidth(),
                               height: $parent.innerHeight()
                       };
                       var po = $parent.offset();
                       var elem = this.elem;
                       var $elem = this.$elem;
                       var dims;
                       if (this.isSVG) {
                               dims = elem.getBoundingClientRect();
                               dims = {
                                       left: dims.left - po.left,
                                       top: dims.top - po.top,
                                       width: dims.width,
                                       height: dims.height,
                                       margin: {
                                               left: 0,
                                               top: 0
                                       }
                               };
                       } else {
                               dims = {
                                       left: $.css(elem, 'left', true) || 0,
                                       top: $.css(elem, 'top', true) || 0,
                                       width: $elem.innerWidth(),
                                       height: $elem.innerHeight(),
                                       margin: {
                                               top: $.css(elem, 'marginTop', true) || 0,
                                               left: $.css(elem, 'marginLeft', true) || 0
                                       }
                               };
                       }
                       dims.widthBorder = ($.css(elem, 'borderLeftWidth', true) + $.css(elem, 'borderRightWidth', true)) || 0;
                       dims.heightBorder = ($.css(elem, 'borderTopWidth', true) + $.css(elem, 'borderBottomWidth', true)) || 0;
                       this.dimensions = dims;
               },
               /**

* 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 * @param {String} transform */

               setTransform: function(transform) {
                       var method = this.isSVG ? 'attr': 'style';
                       var $set = this.$set;
                       var i = $set.length;
                       while (i--) {
                               $[method]($set[i], '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 {
                               // Retrieve the transform
                               transform = $[this.isSVG ? 'attr': 'style'](transformElem, 'transform');
                       }
                       // 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 {Boolean} [animate] Whether to animate the transform change * @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 dims, container, marginW, marginH, diffW, diffH, left, top, width, height;
                       var scale = +matrix[0];
                       var $parent = this.$parent;
                       var contain = typeof options.contain !== 'undefined' ? options.contain: this.options.contain;
                       // Apply containment
                       if (contain) {
                               dims = this._checkDims();
                               container = this.container;
                               width = dims.width + dims.widthBorder;
                               height = dims.height + dims.heightBorder;
                               // Use absolute value of scale here as negative scale doesn't mean even smaller
                               marginW = ((width * Math.abs(scale)) - container.width) / 2;
                               marginH = ((height * Math.abs(scale)) - container.height) / 2;
                               left = dims.left + dims.margin.left;
                               top = dims.top + dims.margin.top;
                               if (contain === 'invert') {
                                       diffW = width > container.width ? width - container.width: 0;
                                       diffH = height > container.height ? height - container.height: 0;
                                       marginW += (container.width - width) / 2;
                                       marginH += (container.height - height) / 2;
                                       matrix[4] = Math.max(Math.min(matrix[4], marginW - left), -marginW - left - diffW);
                                       matrix[5] = Math.max(Math.min(matrix[5], marginH - top), -marginH - top - diffH + dims.heightBorder);
                               } else {
                                       // marginW += dims.widthBorder / 2;
                                       marginH += dims.heightBorder / 2;
                                       diffW = container.width > width ? container.width - width: 0;
                                       diffH = container.height > height ? container.height - height: 0;
                                       // If the element is not naturally centered, assume full margin right
                                       if ($parent.css('textAlign') !== 'center' || !rinline.test($.css(this.elem, 'display'))) {
                                               marginW = marginH = 0;
                                       } else {
                                               diffW = 0;
                                       }
                                       matrix[4] = Math.min(Math.max(matrix[4], marginW - left), -marginW - left + diffW);
                                       matrix[5] = Math.min(Math.max(matrix[5], marginH - top), -marginH - top + diffH);
                               }
                       }
                       if (options.animate !== 'skip') {
                               // Set transition
                               this.transition(!options.animate);
                       }
                       // Update range
                       if (options.range) {
                               this.$zoomRange.val(scale);
                       }
                       // Set the matrix on this.$set
                       this.setTransform('matrix(' + matrix.join(',') + ')');
                       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();
                       // Calculate zoom based on increment
                       if (typeof scale !== 'number') {
                               scale = +matrix[0] + (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
                               var dims = this._checkDims();
                               var clientX = focal.clientX;
                               var clientY = focal.clientY;
                               // Adjust the focal point for default transform-origin => 50% 50%
                               if (!this.isSVG) {
                                       clientX -= (dims.width + dims.widthBorder) / 2;
                                       clientY -= (dims.height + dims.heightBorder) / 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 === 'boolean' ? 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 */

               _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 '$set':
                                       if (value instanceof $ && value.length) {
                                               this.$set = value;
                                               // Reset styles
                                               this._initStyle();
                                               this._buildTransform();
                                       }
                               }
                       },
                       this));
               },
               /**

* Initialize base styles for the element and its parent */

               _initStyle: function() {
                       var styles = {
                               // Promote the element to it's own compositor layer
                               'backface-visibility': 'hidden',
                               // Set to defaults for the namespace
                               '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($parent[0], 'body')) {
                               styles = {
                                       overflow: 'hidden'
                               };
                               if ($parent.css('position') === 'static') {
                                       styles.position = 'relative';
                               }
                               $parent.css(styles);
                       }
               },
               /**

* Undo any styles attached in this plugin */

               _resetStyle: function() {
                       this.$elem.css({
                               'cursor': ,
                               'transition': 
                       });
                       this.$parent.css({
                               'overflow': ,
                               'position': 
                       });
               },
               /**

* Binds all necessary events */

               _bind: function() {
                       var self = this;
                       var options = this.options;
                       var ns = options.eventNamespace;
                       var str_start = pointerEvents ? 'pointerdown' + ns: ('touchstart' + ns + ' mousedown' + ns);
                       var str_click = pointerEvents ? 'pointerup' + ns: ('touchend' + ns + ' click' + 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) && ((touches.length === 1 && !options.disablePan) || touches.length === 2) :
                                       // Mouse/Pointer: Ignore right click
                                       ! options.disablePan && e.which === 1) {
                                               e.preventDefault();
                                               e.stopPropagation();
                                               self._startMove(e, touches);
                                       }
                               };
                       }
                       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, just use pointerdown/mousedown
                               events[(pointerEvents ? 'pointerdown': 'mousedown') + ns] = 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 */

               _unbind: function() {
                       this.$elem.add(this.$zoomIn).add(this.$zoomOut).add(this.$reset).off(this.options.eventNamespace);
               },
               /**

* Builds the original transform value */

               _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 * If SVG, create necessary animations elements for translations and scaling */

               _buildTransition: function() {
                       if (this._transform) {
                               var options = this.options;
                               this._transition = this._transform + ' ' + options.duration + 'ms ' + options.easing;
                       }
               },
               /**

* Checks dimensions to make sure they don't need to be re-calculated */

               _checkDims: function() {
                       var dims = this.dimensions;
                       // Rebuild if width or height is still 0
                       if (!dims.width || !dims.height) {
                               this.resetDimensions();
                       }
                       return this.dimensions;
               },
               /**

* Calculates the distance between two touch points * Remember pythagorean? * @param {Array} touches * @returns {Number} Returns the distance */

               _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 */

               _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 */

               _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 */

               _startMove: function(event, touches) {
                       var move, moveEvent, endEvent, startDistance, startScale, startMiddle, startPageX, startPageY;
                       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'
                       };
                       // Use proper events
                       if (pointerEvents) {
                               moveEvent = 'pointermove';
                               endEvent = 'pointerup';
                       } else if (event.type === 'touchstart') {
                               moveEvent = 'touchmove';
                               endEvent = 'touchend';
                       } 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);
                       if (touches && touches.length === 2) {
                               startDistance = this._getDistance(touches);
                               startScale = +matrix[0];
                               startMiddle = this._getMiddle(touches);
                               move = function(e) {
                                       e.preventDefault();
                                       // Calculate move on middle point
                                       var middle = self._getMiddle(touches = e.touches);
                                       var diff = self._getDistance(touches) - startDistance;
                                       // Set zoom
                                       self.zoom(diff * (options.increment / 100) + startScale, {
                                               focal: middle,
                                               matrix: matrix,
                                               animate: false
                                       });
                                       // Set pan
                                       self.pan( + matrix[4] + middle.clientX - startMiddle.clientX, +matrix[5] + middle.clientY - startMiddle.clientY, panOptions);
                                       startMiddle = middle;
                               };
                       } else {
                               startPageX = event.pageX;
                               startPageY = event.pageY;
                               /**

* Mousemove/touchmove function to pan the element * @param {Object} e Event object */

                               move = function(e) {
                                       e.preventDefault();
                                       self.pan(origPageX + e.pageX - startPageX, origPageY + e.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;

}));