PageSourceSearch

https://www.laminex.co.nz/_ui/responsive/common/js/jquery.panzoom.js

js laminex.co.nz collected 2026-10-06 15:23:12 UTC 38,598 bytes, 1,315 lines download raw bytes

vendor: 6,895 bytes, lines 1-239
1/**
2 * @license jquery.panzoom.js v3.2.2
3 * Updated: Wed Jun 21 2017
4 * Add pan and zoom functionality to any element
5 * Copyright (c) timmy willison
6 * Released under the MIT license
7 * https://github.com/timmywil/jquery.panzoom/blob/master/MIT-License.txt
8 */
9
10(function(global, factory) {
11	// AMD
12	if (typeof define === 'function' && define.amd) {
13		define([ 'jquery' ], function(jQuery) {
14			return factory(global, jQuery);
15		});
16	// CommonJS/Browserify
17	} else if (typeof exports === 'object') {
18		factory(global, require('jquery'));
19	// Global
20	} else {
21		factory(global, global.jQuery);
22	}
23}(typeof window !== 'undefined' ? window : this, function(window, $) {
24	'use strict';
25
26	var document = window.document;
27	var datakey = '__pz__';
28	var slice = Array.prototype.slice;
29	var rIE11 = /trident\/7./i;
30	var supportsInputEvent = (function() {
31		// IE11 returns a false positive
32		if (rIE11.test(navigator.userAgent)) {
33			return false;
34		}
35		var input = document.createElement('input');
36		input.setAttribute('oninput', 'return');
37		return typeof input.oninput === 'function';
38	})();
39
40	// Regex
41	var rupper = /([A-Z])/g;
42	var rsvg = /^http:[\w\.\/]+svg$/;
43
44	var floating = '(\\-?\\d[\\d\\.e-]*)';
45	var commaSpace = '\\,?\\s*';
46	var rmatrix = new RegExp(
47		'^matrix\\(' +
48		floating + commaSpace +
49		floating + commaSpace +
50		floating + commaSpace +
51		floating + commaSpace +
52		floating + commaSpace +
53		floating + '\\)$'
54	);
55
56	/**
57	 * Utility for determining transform matrix equality
58	 * Checks backwards to test translation first
59	 * @param {Array} first
60	 * @param {Array} second
61	 */
62	function matrixEquals(first, second) {
63		var i = first.length;
64		while(--i) {
65			if (Math.round(+first[i]) !== Math.round(+second[i])) {
66				return false;
67			}
68		}
69		return true;
70	}
71
72	/**
73	 * Creates the options object for reset functions
74	 * @param {Boolean|Object} opts See reset methods
75	 * @returns {Object} Returns the newly-created options object
76	 */
77	function createResetOptions(opts) {
78		var options = { range: true, animate: true };
79		if (typeof opts === 'boolean') {
80			options.animate = opts;
81		} else {
82			$.extend(options, opts);
83		}
84		return options;
85	}
86
87	/**
88	 * Represent a transformation matrix with a 3x3 matrix for calculations
89	 * Matrix functions adapted from Louis Remi's jQuery.transform (https://github.com/louisremi/jquery.transform.js)
90	 * @param {Array|Number} a An array of six values representing a 2d transformation matrix
91	 */
92	function Matrix(a, b, c, d, e, f, g, h, i) {
93		if ($.type(a) === 'array') {
94			this.elements = [
95				+a[0], +a[2], +a[4],
96				+a[1], +a[3], +a[5],
97				    0,     0,     1
98			];
99		} else {
100			this.elements = [
101				a, b, c,
102				d, e, f,
103				g || 0, h || 0, i || 1
104			];
105		}
106	}
107
108	Matrix.prototype = {
109		/**
110		 * Multiply a 3x3 matrix by a similar matrix or a vector
111		 * @param {Matrix|Vector} matrix
112		 * @return {Matrix|Vector} Returns a vector if multiplying by a vector
113		 */
114		x: function(matrix) {
115			var isVector = matrix instanceof Vector;
116
117			var a = this.elements,
118				b = matrix.elements;
119
120			if (isVector && b.length === 3) {
121				// b is actually a vector
122				return new Vector(
123					a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
124					a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
125					a[6] * b[0] + a[7] * b[1] + a[8] * b[2]
126				);
127			} else if (b.length === a.length) {
128				// b is a 3x3 matrix
129				return new Matrix(
130					a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
131					a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
132					a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
133
134					a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
135					a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
136					a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
137
138					a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
139					a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
140					a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
141				);
142			}
143			return false; // fail
144		},
145		/**
146		 * Generates an inverse of the current matrix
147		 * @returns {Matrix}
148		 */
149		inverse: function() {
150			var d = 1 / this.determinant(),
151				a = this.elements;
152			return new Matrix(
153				d * ( a[8] * a[4] - a[7] * a[5]),
154				d * (-(a[8] * a[1] - a[7] * a[2])),
155				d * ( a[5] * a[1] - a[4] * a[2]),
156
157				d * (-(a[8] * a[3] - a[6] * a[5])),
158				d * ( a[8] * a[0] - a[6] * a[2]),
159				d * (-(a[5] * a[0] - a[3] * a[2])),
160
161				d * ( a[7] * a[3] - a[6] * a[4]),
162				d * (-(a[7] * a[0] - a[6] * a[1])),
163				d * ( a[4] * a[0] - a[3] * a[1])
164			);
165		},
166		/**
167		 * Calculates the determinant of the current matrix
168		 * @returns {Number}
169		 */
170		determinant: function() {
171			var a = this.elements;
172			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]);
173		}
174	};
175
176	/**
177	 * Create a vector containing three values
178	 */
179	function Vector(x, y, z) {
180		this.elements = [ x, y, z ];
181	}
182
183	/**
184	 * Get the element at zero-indexed index i
185	 * @param {Number} i
186	 */
187	Vector.prototype.e = Matrix.prototype.e = function(i) {
188		return this.elements[ i ];
189	};
190
191	/**
192	 * Create a Panzoom object for a given element
193	 * @constructor
194	 * @param {Element} elem - Element to use pan and zoom
195	 * @param {Object} [options] - An object literal containing options to override default options
196	 *  (See Panzoom.defaults for ones not listed below)
197	 * @param {jQuery} [options.$zoomIn] - zoom in buttons/links collection (you can also bind these yourself
198	 *  e.g. $button.on('click', function(e) { e.preventDefault(); $elem.panzoom('zoomIn'); });)
199	 * @param {jQuery} [options.$zoomOut] - zoom out buttons/links collection on which to bind zoomOut
200	 * @param {jQuery} [options.$zoomRange] - zoom in/out with this range control
201	 * @param {jQuery} [options.$reset] - Reset buttons/links collection on which to bind the reset method
202	 * @param {Function} [options.on[Start|Change|Zoom|Pan|End|Reset] - Optional callbacks for panzoom events
203	 */
204	function Panzoom(elem, options) {
205
206		// Allow instantiation without `new` keyword
207		if (!(this instanceof Panzoom)) {
208			return new Panzoom(elem, options);
209		}
210
211		// Sanity checks
212		if (elem.nodeType !== 1) {
213			$.error('Panzoom called on non-Element node');
214		}
215		if (!$.contains(document, elem)) {
216			$.error('Panzoom element must be attached to the document');
217		}
218
219		// Don't remake
220		var d = $.data(elem, datakey);
221		if (d) {
222			return d;
223		}
224
225		// Extend default with given object literal
226		// Each instance gets its own options
227		this.options = options = $.extend({}, Panzoom.defaults, options);
228		this.elem = elem;
229		var $elem = this.$elem = $(elem);
230		this.$set = options.$set && options.$set.length ? options.$set : $elem;
231		this.$doc = $(elem.ownerDocument || document);
232		this.$parent = $elem.parent();
233		this.parent = this.$parent[0];
234
235		// This is SVG if the namespace is SVG
236		// However, while <svg> elements are SVG, we want to treat those like other elements
237		this.isSVG = rsvg.test(elem.namespaceURI) && elem.nodeName.toLowerCase() !== 'svg';
238
239		this.panning = false;
240
241		// Save the original transform value
242		// Save the prefixed transform style key
243		// Set the starting transform
244		this._buildTransform();
245
246		// Build the appropriately-prefixed transform style property name
247		// De-camelcase
248		this._transform = $.cssProps.transform.replace(rupper, '-$1').toLowerCase()
vendor: 31,398 bytes, lines 248-1315
248;
249
250		// Build the transition value
251		this._buildTransition();
252
253		// Build containment dimensions
254		this.resetDimensions();
255
256		// Add zoom and reset buttons to `this`
257		var $empty = $();
258		var self = this;
259		$.each([ '$zoomIn', '$zoomOut', '$zoomRange', '$reset' ], function(i, name) {
260			self[ name ] = options[ name ] || $empty;
261		});
262
263		this.enable();
264
265		this.scale = this.getMatrix()[0];
266		this._checkPanWhenZoomed();
267
268		// Save the instance
269		$.data(elem, datakey, this);
270	}
271
272	// Attach regex for possible use (immutable)
273	Panzoom.rmatrix = rmatrix;
274
275	Panzoom.defaults = {
276		// Should always be non-empty
277		// Used to bind jQuery events without collisions
278		// A guid is not added here as different instantiations/versions of panzoom
279		// on the same element is not supported, so don't do it.
280		eventNamespace: '.panzoom',
281
282		// Whether or not to transition the scale
283		transition: true,
284
285		// Default cursor style for the element
286		cursor: 'move',
287
288		// There may be some use cases for zooming without panning or vice versa
289		disablePan: false,
290		disableZoom: false,
291
292		// Pan only on the X or Y axes
293		disableXAxis: false,
294		disableYAxis: false,
295
296		// Set whether you'd like to pan on left (1), middle (2), or right click (3)
297		which: 1,
298
299		// The increment at which to zoom
300		// Should be a number greater than 0
301		increment: 0.3,
302
303		// When no scale is passed, this option tells
304		// the `zoom` method to increment
305		// the scale *linearly* based on the increment option.
306		// This often ends up looking like very little happened at larger zoom levels.
307		// The default is to multiply/divide the scale based on the increment.
308		linearZoom: false,
309
310		// Pan only when the scale is greater than minScale
311		panOnlyWhenZoomed: false,
312
313		// min and max zoom scales
314		minScale: 0.3,
315		maxScale: 6,
316
317		// The default step for the range input
318		// Precendence: default < HTML attribute < option setting
319		rangeStep: 0.05,
320
321		// Animation duration (ms)
322		duration: 200,
323		// CSS easing used for scale transition
324		easing: 'ease-in-out',
325
326		// Indicate that the element should be contained within it's parent when panning
327		// Note: this does not affect zooming outside of the parent
328		// Set this value to 'invert' to only allow panning outside of the parent element (basically the opposite of the normal use of contain)
329		// 'invert' is useful for a large panzoom element where you don't want to show anything behind it
330		contain: false
331	};
332
333	Panzoom.prototype = {
334		constructor: Panzoom,
335
336		/**
337		 * @returns {Panzoom} Returns the instance
338		 */
339		instance: function() {
340			return this;
341		},
342
343		/**
344		 * Enable or re-enable the panzoom instance
345		 */
346		enable: function() {
347			// Unbind first
348			this._initStyle();
349			this._bind();
350			this.disabled = false;
351		},
352
353		/**
354		 * Disable panzoom
355		 */
356		disable: function() {
357			this.disabled = true;
358			this._resetStyle();
359			this._unbind();
360		},
361
362		/**
363		 * @returns {Boolean} Returns whether the current panzoom instance is disabled
364		 */
365		isDisabled: function() {
366			return this.disabled;
367		},
368
369		/**
370		 * Destroy the panzoom instance
371		 */
372		destroy: function() {
373			this.disable();
374			$.removeData(this.elem, datakey);
375		},
376
377		/**
378		 * Builds the restricing dimensions from the containment element
379		 * Also used with focal points
380		 * Call this method whenever the dimensions of the element or parent are changed
381		 */
382		resetDimensions: function() {
383			// Reset container properties
384			this.container = this.parent.getBoundingClientRect();
385
386			// Set element properties
387			var elem = this.elem;
388			// getBoundingClientRect() works with SVG, offsetWidth does not
389			var dims = elem.getBoundingClientRect();
390			var absScale = Math.abs(this.scale);
391			this.dimensions = {
392				width: dims.width,
393				height: dims.height,
394				left: $.css(elem, 'left', true) || 0,
395				top: $.css(elem, 'top', true) || 0,
396				// Borders and margins are scaled
397				border: {
398					top: $.css(elem, 'borderTopWidth', true) * absScale || 0,
399					bottom: $.css(elem, 'borderBottomWidth', true) * absScale || 0,
400					left: $.css(elem, 'borderLeftWidth', true) * absScale || 0,
401					right: $.css(elem, 'borderRightWidth', true) * absScale || 0
402				},
403				margin: {
404					top: $.css(elem, 'marginTop', true) * absScale || 0,
405					left: $.css(elem, 'marginLeft', true) * absScale || 0
406				}
407			};
408		},
409
410		/**
411		 * Return the element to it's original transform matrix
412		 * @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.
413		 * @param {Boolean} [options.silent] Silence the reset event
414		 */
415		reset: function(options) {
416			options = createResetOptions(options);
417			// Reset the transform to its original value
418			var matrix = this.setMatrix(this._origTransform, options);
419			if (!options.silent) {
420				this._trigger('reset', matrix);
421			}
422		},
423
424		/**
425		 * Only resets zoom level
426		 * @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to zoom()
427		 */
428		resetZoom: function(options) {
429			options = createResetOptions(options);
430			var origMatrix = this.getMatrix(this._origTransform);
431			options.dValue = origMatrix[ 3 ];
432			this.zoom(origMatrix[0], options);
433		},
434
435		/**
436		 * Only reset panning
437		 * @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to pan()
438		 */
439		resetPan: function(options) {
440			var origMatrix = this.getMatrix(this._origTransform);
441			this.pan(origMatrix[4], origMatrix[5], createResetOptions(options));
442		},
443
444		/**
445		 * Sets a transform on the $set
446		 * For SVG, the style attribute takes precedence
447		 * and allows us to animate
448		 * @param {String} transform
449		 */
450		setTransform: function(transform) {
451			var $set = this.$set;
452			var i = $set.length;
453			while(i--) {
454				$.style($set[i], 'transform', transform);
455
456				// Support IE9-11, Edge 13-14+
457				// Set attribute alongside style attribute
458				// since IE and Edge do not respect style settings on SVG
459				// See https://css-tricks.com/transforms-on-svg-elements/
460				if (this.isSVG) {
461					$set[i].setAttribute('transform', transform);
462				}
463			}
464		},
465
466		/**
467		 * Retrieving the transform is different for SVG
468		 *  (unless a style transform is already present)
469		 * Uses the $set collection for retrieving the transform
470		 * @param {String} [transform] Pass in an transform value (like 'scale(1.1)')
471		 *  to have it formatted into matrix format for use by Panzoom
472		 * @returns {String} Returns the current transform value of the element
473		 */
474		getTransform: function(transform) {
475			var $set = this.$set;
476			var transformElem = $set[0];
477			if (transform) {
478				this.setTransform(transform);
479			} else {
480
481				// IE and Edge still set the transform style properly
482				// They just don't render it on SVG
483				// So we get a correct value here
484				transform = $.style(transformElem, 'transform');
485
486				if (this.isSVG && (!transform || transform === 'none')) {
487					transform = $.attr(transformElem, 'transform') || 'none';
488				}
489			}
490
491			// Convert any transforms set by the user to matrix format
492			// by setting to computed
493			if (transform !== 'none' && !rmatrix.test(transform)) {
494
495				// Get computed and set for next time
496				this.setTransform(transform = $.css(transformElem, 'transform'));
497			}
498
499			return transform || 'none';
500		},
501
502		/**
503		 * Retrieve the current transform matrix for $elem (or turn a transform into it's array values)
504		 * @param {String} [transform] matrix-formatted transform value
505		 * @returns {Array} Returns the current transform matrix split up into it's parts, or a default matrix
506		 */
507		getMatrix: function(transform) {
508			var matrix = rmatrix.exec(transform || this.getTransform());
509			if (matrix) {
510				matrix.shift();
511			}
512			return matrix || [ 1, 0, 0, 1, 0, 0 ];
513		},
514
515		/**
516		 * Get the current scale.
517		 * @param {String} [transform] matrix-formatted transform value
518		 * @returns {Number} Current scale relative to the initial scale (height / width = 1)
519		 */
520		getScale: function(matrix) {
521			return Math.sqrt(Math.pow(matrix[0], 2) + Math.pow(matrix[1], 2));
522		},
523
524		/**
525		 * Given a matrix object, quickly set the current matrix of the element
526		 * @param {Array|String} matrix
527		 * @param {Object} [options]
528		 * @param {Boolean|String} [options.animate] Whether to animate the transform change, or 'skip' indicating that it is unnecessary to set
529		 * @param {Boolean} [options.contain] Override the global contain option
530		 * @param {Boolean} [options.range] If true, $zoomRange's value will be updated.
531		 * @param {Boolean} [options.silent] If true, the change event will not be triggered
532		 * @returns {Array} Returns the newly-set matrix
533		 */
534		setMatrix: function(matrix, options) {
535			if (this.disabled) { return; }
536			if (!options) { options = {}; }
537			// Convert to array
538			if (typeof matrix === 'string') {
539				matrix = this.getMatrix(matrix);
540			}
541			var scale = this.getScale(matrix);
542			var contain = typeof options.contain !== 'undefined' ? options.contain : this.options.contain;
543
544			// Apply containment
545			if (contain) {
546				var dims = options.dims;
547				if (!dims) {
548					this.resetDimensions();
549					dims = this.dimensions;
550				}
551				var spaceWLeft, spaceWRight, scaleDiff;
552				var container = this.container;
553				var width = dims.width;
554				var height = dims.height;
555				var conWidth = container.width;
556				var conHeight = container.height;
557				var zoomAspectW = conWidth / width;
558				var zoomAspectH = conHeight / height;
559
560				// If the element is not naturally centered,
561				// assume full space right
562				if (this.$parent.css('textAlign') !== 'center' || $.css(this.elem, 'display') !== 'inline') {
563					// offsetWidth gets us the width without the transform
564					scaleDiff = (width - this.elem.offsetWidth) / 2;
565					spaceWLeft = scaleDiff - dims.border.left;
566					spaceWRight = width - conWidth - scaleDiff + dims.border.right;
567				} else {
568					spaceWLeft = spaceWRight = ((width - conWidth) / 2);
569				}
570				var spaceHTop = ((height - conHeight) / 2) + dims.border.top;
571				var spaceHBottom = ((height - conHeight) / 2) - dims.border.top - dims.border.bottom;
572
573				if (contain === 'invert' || contain === 'automatic' && zoomAspectW < 1.01) {
574					matrix[4] = Math.max(Math.min(matrix[4], spaceWLeft - dims.border.left), -spaceWRight);
575				} else {
576					matrix[4] = Math.min(Math.max(matrix[4], spaceWLeft), -spaceWRight);
577				}
578
579				if (contain === 'invert' || (contain === 'automatic' && zoomAspectH < 1.01)) {
580					matrix[5] = Math.max(Math.min(matrix[5], spaceHTop - dims.border.top), -spaceHBottom);
581				} else {
582					matrix[5] = Math.min(Math.max(matrix[5], spaceHTop), -spaceHBottom);
583				}
584			}
585
586			// Animate
587			if (options.animate !== 'skip') {
588				// Set transition
589				this.transition(!options.animate);
590			}
591
592			// Update range element
593			if (options.range) {
594				this.$zoomRange.val(scale);
595			}
596
597			// Set the matrix on this.$set
598			if (this.options.disableXAxis || this.options.disableYAxis) {
599				var originalMatrix = this.getMatrix();
600				if (this.options.disableXAxis) {
601					matrix[4] = originalMatrix[4];
602				}
603				if (this.options.disableYAxis) {
604					matrix[5] = originalMatrix[5];
605				}
606			}
607			this.setTransform('matrix(' + matrix.join(',') + ')');
608
609			this.scale = scale;
610
611			// Disable/enable panning if zooming is at minimum and panOnlyWhenZoomed is true
612			this._checkPanWhenZoomed(scale);
613
614			if (!options.silent) {
615				this._trigger('change', matrix);
616			}
617
618			return matrix;
619		},
620
621		/**
622		 * @returns {Boolean} Returns whether the panzoom element is currently being dragged
623		 */
624		isPanning: function() {
625			return this.panning;
626		},
627
628		/**
629		 * Apply the current transition to the element, if allowed
630		 * @param {Boolean} [off] Indicates that the transition should be turned off
631		 */
632		transition: function(off) {
633			if (!this._transition) { return; }
634			var transition = off || !this.options.transition ? 'none' : this._transition;
635			var $set = this.$set;
636			var i = $set.length;
637			while(i--) {
638				// Avoid reflows when zooming
639				if ($.style($set[i], 'transition') !== transition) {
640					$.style($set[i], 'transition', transition);
641				}
642			}
643		},
644
645		/**
646		 * Pan the element to the specified translation X and Y
647		 * Note: this is not the same as setting jQuery#offset() or jQuery#position()
648		 * @param {Number} x
649		 * @param {Number} y
650		 * @param {Object} [options] These options are passed along to setMatrix
651		 * @param {Array} [options.matrix] The matrix being manipulated (if already known so it doesn't have to be retrieved again)
652		 * @param {Boolean} [options.silent] Silence the pan event. Note that this will also silence the setMatrix change event.
653		 * @param {Boolean} [options.relative] Make the x and y values relative to the existing matrix
654		 */
655		pan: function(x, y, options) {
656			if (this.options.disablePan) { return; }
657			if (!options) { options = {}; }
658			var matrix = options.matrix;
659			if (!matrix) {
660				matrix = this.getMatrix();
661			}
662			// Cast existing matrix values to numbers
663			if (options.relative) {
664				x += +matrix[4];
665				y += +matrix[5];
666			}
667			matrix[4] = x;
668			matrix[5] = y;
669			this.setMatrix(matrix, options);
670			if (!options.silent) {
671				this._trigger('pan', matrix[4], matrix[5]);
672			}
673		},
674
675		/**
676		 * Zoom in/out the element using the scale properties of a transform matrix
677		 * @param {Number|Boolean} [scale] The scale to which to zoom or a boolean indicating to transition a zoom out
678		 * @param {Object} [opts] All global options can be overwritten by this options object. For example, override the default increment.
679		 * @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)
680		 * @param {jQuery.Event|Object} [opts.focal] A focal point on the panzoom element on which to zoom.
681		 *  If an object, set the clientX and clientY properties to the position relative to the parent
682		 * @param {Boolean} [opts.animate] Whether to animate the zoom (defaults to true if scale is not a number, false otherwise)
683		 * @param {Boolean} [opts.silent] Silence the zoom event
684		 * @param {Array} [opts.matrix] Optionally pass the current matrix so it doesn't need to be retrieved
685		 * @param {Number} [opts.dValue] Think of a transform matrix as four values a, b, c, d
686		 *  where a/d are the horizontal/vertical scale values and b/c are the skew values
687		 *  (5 and 6 of matrix array are the tx/ty transform values).
688		 *  Normally, the scale is set to both the a and d values of the matrix.
689		 *  This option allows you to specify a different d value for the zoom.
690		 *  For instance, to flip vertically, you could set -1 as the dValue.
691		 */
692		zoom: function(scale, opts) {
693			// Shuffle arguments
694			if (typeof scale === 'object') {
695				opts = scale;
696				scale = null;
697			} else if (!opts) {
698				opts = {};
699			}
700			var options = $.extend({}, this.options, opts);
701			// Check if disabled
702			if (options.disableZoom) { return; }
703			var animate = false;
704			var matrix = options.matrix || this.getMatrix();
705			var surfaceM = new Matrix(matrix);
706			var startScale = this.getScale(matrix);
707
708			// Calculate zoom based on increment
709			if (typeof scale !== 'number') {
710				if (options.linearZoom) {
711					scale = 1 + (options.increment * (scale ? -1 : 1)) / startScale;
712				} else {
713					scale = scale ? (1 / (1 + options.increment)) : (1 + options.increment);
714				}
715				animate = true;
716			} else {
717				scale = 1 / startScale;
718			}
719
720			// Constrain scale
721			scale = Math.max(Math.min(scale, options.maxScale / startScale), options.minScale / startScale);
722			var m = surfaceM.x(new Matrix(scale, 0, 0, 0, (typeof options.dValue === 'number' ? options.dValue / startScale : scale), 0));
723
724			// Calculate focal point based on scale
725			var focal = options.focal;
726			if (focal && !options.disablePan) {
727				// Adapted from code by Florian Günther
728				// https://github.com/florianguenther/zui53
729				this.resetDimensions();
730				var dims = options.dims = this.dimensions;
731				var clientX = focal.clientX;
732				var clientY = focal.clientY;
733
734				// Adjust the focal point for transform-origin 50% 50%
735				// SVG elements have a transform origin of 0 0
736				if (!this.isSVG) {
737					clientX -= (dims.width / startScale) / 2;
738					clientY -= (dims.height / startScale) / 2;
739				}
740
741				var clientV = new Vector(clientX, clientY, 1);
742				// Supply an offset manually if necessary
743				var o = this.parentOffset || this.$parent.offset();
744				var offsetM = new Matrix(1, 0, o.left - this.$doc.scrollLeft(), 0, 1, o.top - this.$doc.scrollTop());
745				var surfaceV = surfaceM.inverse().x(offsetM.inverse().x(clientV));
746				surfaceM = surfaceM.x(new Matrix([scale, 0, 0, scale, 0, 0]));
747				clientV = offsetM.x(surfaceM.x(surfaceV));
748				matrix[4] = +matrix[4] + (clientX - clientV.e(0));
749				matrix[5] = +matrix[5] + (clientY - clientV.e(1));
750			}
751
752			// Set the scale
753			matrix[0] = m.e(0);
754			matrix[1] = m.e(3);
755			matrix[2] = m.e(1);
756			matrix[3] = m.e(4);
757
758			// Calling zoom may still pan the element
759			this.setMatrix(matrix, {
760				animate: typeof options.animate !== 'undefined' ? options.animate : animate,
761				// Set the zoomRange value
762				range: !options.noSetRange
763			});
764
765			// Trigger zoom event
766			if (!options.silent) {
767				this._trigger('zoom', scale, options);
768			}
769		},
770
771		/**
772		 * Get/set option on an existing instance
773		 * @returns {Array|undefined} If getting, returns an array of all values
774		 *   on each instance for a given key. If setting, continue chaining by returning undefined.
775		 */
776		option: function(key, value) {
777			var options;
778			if (!key) {
779				// Avoids returning direct reference
780				return $.extend({}, this.options);
781			}
782
783			if (typeof key === 'string') {
784				if (arguments.length === 1) {
785					return this.options[ key ] !== undefined ?
786						this.options[ key ] :
787						null;
788				}
789				options = {};
790				options[ key ] = value;
791			} else {
792				options = key;
793			}
794
795			this._setOptions(options);
796		},
797
798		/**
799		 * Internally sets options
800		 * @param {Object} options - An object literal of options to set
801		 * @private
802		 */
803		_setOptions: function(options) {
804			$.each(options, $.proxy(function(key, value) {
805				switch(key) {
806					case 'disablePan':
807						this._resetStyle();
808						/* falls through */
809					case '$zoomIn':
810					case '$zoomOut':
811					case '$zoomRange':
812					case '$reset':
813					case 'disableZoom':
814					case 'onStart':
815					case 'onChange':
816					case 'onZoom':
817					case 'onPan':
818					case 'onEnd':
819					case 'onReset':
820					case 'eventNamespace':
821						this._unbind();
822				}
823				this.options[ key ] = value;
824				switch(key) {
825					case 'disablePan':
826						this._initStyle();
827						/* falls through */
828					case '$zoomIn':
829					case '$zoomOut':
830					case '$zoomRange':
831					case '$reset':
832						// Set these on the instance
833						this[ key ] = value;
834						/* falls through */
835					case 'disableZoom':
836					case 'onStart':
837					case 'onChange':
838					case 'onZoom':
839					case 'onPan':
840					case 'onEnd':
841					case 'onReset':
842					case 'eventNamespace':
843						this._bind();
844						break;
845					case 'cursor':
846						$.style(this.elem, 'cursor', value);
847						break;
848					case 'minScale':
849						this.$zoomRange.attr('min', value);
850						break;
851					case 'maxScale':
852						this.$zoomRange.attr('max', value);
853						break;
854					case 'rangeStep':
855						this.$zoomRange.attr('step', value);
856						break;
857					case 'startTransform':
858						this._buildTransform();
859						break;
860					case 'duration':
861					case 'easing':
862						this._buildTransition();
863						/* falls through */
864					case 'transition':
865						this.transition();
866						break;
867					case 'panOnlyWhenZoomed':
868						this._checkPanWhenZoomed();
869						break;
870					case '$set':
871						if (value instanceof $ && value.length) {
872							this.$set = value;
873							// Reset styles
874							this._initStyle();
875							this._buildTransform();
876						}
877				}
878			}, this));
879		},
880
881		/**
882		 * Disable/enable panning depending on whether the current scale
883		 * matches the minimum
884		 * @param {Number} [scale]
885		 * @private
886		 */
887		_checkPanWhenZoomed: function(scale) {
888			var options = this.options;
889			if (options.panOnlyWhenZoomed) {
890				if (!scale) {
891					scale = this.getMatrix()[0];
892				}
893				var toDisable = scale <= options.minScale;
894				if (options.disablePan !== toDisable) {
895					this.option('disablePan', toDisable);
896				}
897			}
898		},
899
900		/**
901		 * Initialize base styles for the element and its parent
902		 * @private
903		 */
904		_initStyle: function() {
905			var styles = {
906				// Set the same default whether SVG or HTML
907				// transform-origin cannot be changed to 50% 50% in IE9-11 or Edge 13-14+
908				'transform-origin': this.isSVG ? '0 0' : '50% 50%'
909			};
910			// Set elem styles
911			if (!this.options.disablePan) {
912				styles.cursor = this.options.cursor;
913			}
914			this.$set.css(styles);
915
916			// Set parent to relative if set to static
917			var $parent = this.$parent;
918			// No need to add styles to the body
919			if ($parent.length && !$.nodeName(this.parent, 'body')) {
920				styles = {
921					overflow: 'hidden'
922				};
923				if ($parent.css('position') === 'static') {
924					styles.position = 'relative';
925				}
926				$parent.css(styles);
927			}
928		},
929
930		/**
931		 * Undo any styles attached in this plugin
932		 * @private
933		 */
934		_resetStyle: function() {
935			this.$elem.css({
936				'cursor': '',
937				'transition': ''
938			});
939			this.$parent.css({
940				'overflow': '',
941				'position': ''
942			});
943		},
944
945		/**
946		 * Binds all necessary events
947		 * @private
948		 */
949		_bind: function() {
950			var self = this;
951			var options = this.options;
952			var ns = options.eventNamespace;
953			var str_down = 'mousedown' + ns + ' pointerdown' + ns + ' MSPointerDown' + ns;
954			var str_start = 'touchstart' + ns + ' ' + str_down;
955			var str_click = 'touchend' + ns + ' click' + ns + ' pointerup' + ns + ' MSPointerUp' + ns;
956			var events = {};
957			var $reset = this.$reset;
958			var $zoomRange = this.$zoomRange;
959
960			// Bind panzoom events from options
961			$.each([ 'Start', 'Change', 'Zoom', 'Pan', 'End', 'Reset' ], function() {
962				var m = options[ 'on' + this ];
963				if ($.isFunction(m)) {
964					events[ 'panzoom' + this.toLowerCase() + ns ] = m;
965				}
966			});
967
968			// Bind $elem drag and click/touchdown events
969			// Bind touchstart if either panning or zooming is enabled
970			if (!options.disablePan || !options.disableZoom) {
971				events[ str_start ] = function(e) {
972					var touches;
973					if (e.type === 'touchstart' ?
974						// Touch
975						(touches = e.touches || e.originalEvent.touches) &&
976							((touches.length === 1 && !options.disablePan) || touches.length === 2) :
977						// Mouse/Pointer: Ignore unexpected click types
978						// Support: IE10 only
979						// IE10 does not support e.button for MSPointerDown, but does have e.which
980						!options.disablePan && (e.which || e.originalEvent.which) === options.which) {
981
982						e.preventDefault();
983						e.stopPropagation();
984						self._startMove(e, touches);
985					}
986				};
987				// Prevent the contextmenu event
988				// if we're binding to right-click
989				if (options.which === 3) {
990					events.contextmenu = false;
991				}
992			}
993			this.$elem.on(events);
994
995			// Bind reset
996			if ($reset.length) {
997				$reset.on(str_click, function(e) {
998					e.preventDefault();
999					self.reset();
1000				});
1001			}
1002
1003			// Set default attributes for the range input
1004			if ($zoomRange.length) {
1005				$zoomRange.attr({
1006					// Only set the range step if explicit or
1007					// set the default if there is no attribute present
1008					step: options.rangeStep === Panzoom.defaults.rangeStep &&
1009						$zoomRange.attr('step') ||
1010						options.rangeStep,
1011					min: options.minScale,
1012					max: options.maxScale
1013				}).prop({
1014					value: this.getMatrix()[0]
1015				});
1016			}
1017
1018			// No bindings if zooming is disabled
1019			if (options.disableZoom) {
1020				return;
1021			}
1022
1023			var $zoomIn = this.$zoomIn;
1024			var $zoomOut = this.$zoomOut;
1025
1026			// Bind zoom in/out
1027			// Don't bind one without the other
1028			if ($zoomIn.length && $zoomOut.length) {
1029				// preventDefault cancels future mouse events on touch events
1030				$zoomIn.on(str_click, function(e) {
1031					e.preventDefault();
1032					self.zoom();
1033				});
1034				$zoomOut.on(str_click, function(e) {
1035					e.preventDefault();
1036					self.zoom(true);
1037				});
1038			}
1039
1040			if ($zoomRange.length) {
1041				events = {};
1042				// Cannot prevent default action here
1043				events[ str_down ] = function() {
1044					self.transition(true);
1045				};
1046				// Zoom on input events if available and change events
1047				// See https://github.com/timmywil/jquery.panzoom/issues/90
1048				events[ (supportsInputEvent ? 'input' : 'change') + ns ] = function() {
1049					self.zoom(+this.value, { noSetRange: true });
1050				};
1051				$zoomRange.on(events);
1052			}
1053		},
1054
1055		/**
1056		 * Unbind all events
1057		 * @private
1058		 */
1059		_unbind: function() {
1060			this.$elem
1061				.add(this.$zoomIn)
1062				.add(this.$zoomOut)
1063				.add(this.$reset)
1064				.off(this.options.eventNamespace);
1065		},
1066
1067		/**
1068		 * Builds the original transform value
1069		 * @private
1070		 */
1071		_buildTransform: function() {
1072			// Save the original transform
1073			// Retrieving this also adds the correct prefixed style name
1074			// to jQuery's internal $.cssProps
1075			return this._origTransform = this.getTransform(this.options.startTransform);
1076		},
1077
1078		/**
1079		 * Set transition property for later use when zooming
1080		 * @private
1081		 */
1082		_buildTransition: function() {
1083			if (this._transform) {
1084				var options = this.options;
1085				this._transition = this._transform + ' ' + options.duration + 'ms ' + options.easing;
1086			}
1087		},
1088
1089		/**
1090		 * Calculates the distance between two touch points
1091		 * Remember pythagorean?
1092		 * @param {Array} touches
1093		 * @returns {Number} Returns the distance
1094		 * @private
1095		 */
1096		_getDistance: function(touches) {
1097			var touch1 = touches[0];
1098			var touch2 = touches[1];
1099			return Math.sqrt(Math.pow(Math.abs(touch2.clientX - touch1.clientX), 2) + Math.pow(Math.abs(touch2.clientY - touch1.clientY), 2));
1100		},
1101
1102		/**
1103		 * Constructs an approximated point in the middle of two touch points
1104		 * @returns {Object} Returns an object containing pageX and pageY
1105		 * @private
1106		 */
1107		_getMiddle: function(touches) {
1108			var touch1 = touches[0];
1109			var touch2 = touches[1];
1110			return {
1111				clientX: ((touch2.clientX - touch1.clientX) / 2) + touch1.clientX,
1112				clientY: ((touch2.clientY - touch1.clientY) / 2) + touch1.clientY
1113			};
1114		},
1115
1116		/**
1117		 * Trigger a panzoom event on our element
1118		 * The event is passed the Panzoom instance
1119		 * @param {String|jQuery.Event} event
1120		 * @param {Mixed} arg1[, arg2, arg3, ...] Arguments to append to the trigger
1121		 * @private
1122		 */
1123		_trigger: function (event) {
1124			if (typeof event === 'string') {
1125				event = 'panzoom' + event;
1126			}
1127			this.$elem.triggerHandler(event, [this].concat(slice.call(arguments, 1)));
1128		},
1129
1130		/**
1131		 * Starts the pan
1132		 * This is bound to mouse/touchmove on the element
1133		 * @param {jQuery.Event} event An event with pageX, pageY, and possibly the touches list
1134		 * @param {TouchList} [touches] The touches list if present
1135		 * @private
1136		 */
1137		_startMove: function(event, touches) {
1138			if (this.panning) {
1139				return;
1140			}
1141			var moveEvent, endEvent,
1142				startDistance, startScale, startMiddle,
1143				startPageX, startPageY, touch;
1144			var self = this;
1145			var options = this.options;
1146			var ns = options.eventNamespace;
1147			var matrix = this.getMatrix();
1148			var original = matrix.slice(0);
1149			var origPageX = +original[4];
1150			var origPageY = +original[5];
1151			var panOptions = { matrix: matrix, animate: 'skip' };
1152			var type = event.type;
1153
1154			// Use proper events
1155			if (type === 'pointerdown') {
1156				moveEvent = 'pointermove';
1157				endEvent = 'pointerup';
1158			} else if (type === 'touchstart') {
1159				moveEvent = 'touchmove';
1160				endEvent = 'touchend';
1161			} else if (type === 'MSPointerDown') {
1162				moveEvent = 'MSPointerMove';
1163				endEvent = 'MSPointerUp';
1164			} else {
1165				moveEvent = 'mousemove';
1166				endEvent = 'mouseup';
1167			}
1168
1169			// Add namespace
1170			moveEvent += ns;
1171			endEvent += ns;
1172
1173			// Remove any transitions happening
1174			this.transition(true);
1175
1176			// Indicate that we are currently panning
1177			this.panning = true;
1178
1179			// Trigger start event
1180			this._trigger('start', event, touches);
1181
1182			var setStart = function(event, touches) {
1183				if (touches) {
1184					if (touches.length === 2) {
1185						if (startDistance != null) {
1186							return;
1187						}
1188						startDistance = self._getDistance(touches);
1189						startScale = self.getScale(matrix);
1190						startMiddle = self._getMiddle(touches);
1191						return;
1192					}
1193					if (startPageX != null) {
1194						return;
1195					}
1196					if ((touch = touches[0])) {
1197						startPageX = touch.pageX;
1198						startPageY = touch.pageY;
1199					}
1200				}
1201				if (startPageX != null) {
1202					return;
1203				}
1204				startPageX = event.pageX;
1205				startPageY = event.pageY;
1206			};
1207
1208			setStart(event, touches);
1209
1210			var move = function(e) {
1211				var coords;
1212				e.preventDefault();
1213				touches = e.touches || e.originalEvent.touches;
1214				setStart(e, touches);
1215
1216				if (touches) {
1217					if (touches.length === 2) {
1218
1219						// Calculate move on middle point
1220						var middle = self._getMiddle(touches);
1221						var diff = self._getDistance(touches) - startDistance;
1222
1223						// Set zoom
1224						self.zoom(diff * (options.increment / 100) + startScale, {
1225							focal: middle,
1226							matrix: matrix,
1227							animate: 'skip'
1228						});
1229
1230						// Set pan
1231						self.pan(
1232							+matrix[4] + middle.clientX - startMiddle.clientX,
1233							+matrix[5] + middle.clientY - startMiddle.clientY,
1234							panOptions
1235						);
1236						startMiddle = middle;
1237						return;
1238					}
1239					coords = touches[0] || { pageX: 0, pageY: 0 };
1240				}
1241
1242				if (!coords) {
1243					coords = e;
1244				}
1245
1246				self.pan(
1247					origPageX + coords.pageX - startPageX,
1248					origPageY + coords.pageY - startPageY,
1249					panOptions
1250				);
1251			};
1252
1253			// Bind the handlers
1254			$(document)
1255				.off(ns)
1256				.on(moveEvent, move)
1257				.on(endEvent, function(e) {
1258					e.preventDefault();
1259					// Unbind all document events
1260					$(this).off(ns);
1261					self.panning = false;
1262					// Trigger our end event
1263					// Simply set the type to "panzoomend" to pass through all end properties
1264					// jQuery's `not` is used here to compare Array equality
1265					e.type = 'panzoomend';
1266					self._trigger(e, matrix, !matrixEquals(matrix, original));
1267				});
1268		}
1269	};
1270
1271	// Add Panzoom as a static property
1272	$.Panzoom = Panzoom;
1273
1274	/**
1275	 * Extend jQuery
1276	 * @param {Object|String} options - The name of a method to call on the prototype
1277	 *  or an object literal of options
1278	 * @returns {jQuery|Mixed} jQuery instance for regular chaining or the return value(s) of a panzoom method call
1279	 */
1280	$.fn.panzoom = function(options) {
1281		var instance, args, m, ret;
1282
1283		// Call methods widget-style
1284		if (typeof options === 'string') {
1285			ret = [];
1286			args = slice.call(arguments, 1);
1287			this.each(function() {
1288				instance = $.data(this, datakey);
1289
1290				if (!instance) {
1291					ret.push(undefined);
1292
1293				// Ignore methods beginning with `_`
1294				} else if (options.charAt(0) !== '_' &&
1295					typeof (m = instance[ options ]) === 'function' &&
1296					// If nothing is returned, do not add to return values
1297					(m = m.apply(instance, args)) !== undefined) {
1298
1299					ret.push(m);
1300				}
1301			});
1302
1303			// Return an array of values for the jQuery instances
1304			// Or the value itself if there is only one
1305			// Or keep chaining
1306			return ret.length ?
1307				(ret.length === 1 ? ret[0] : ret) :
1308				this;
1309		}
1310
1311		return this.each(function() { new Panzoom(this, options); });
1312	};
1313
1314	return Panzoom;
1315}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.