Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinching is not working in iPad and mobile devices #373

Closed
ghost opened this issue Mar 15, 2018 · 5 comments
Closed

Pinching is not working in iPad and mobile devices #373

ghost opened this issue Mar 15, 2018 · 5 comments

Comments

@ghost
Copy link

ghost commented Mar 15, 2018

Pinching is not working in iPad and mobile devices

Version:
Panzoom js - jquery.panzoom.js v3.2.2
jQuery - 3.3.1

Check the link:
https://prem-thinkdesign.github.io/medical-mdm/medical_codes_er.html

In iPad (ios devices) Dragging and pinch to zoom in and zoom out is not working.
In mobile devices (android devices) dragging is working but pinch to zoom in and zoom out is not working.

@ANSEUNGCHAN
Copy link

now new version me too not working but demo page js working... try it good luck

change panzoom version

// panzoom.js this is work (demo for js : http://timmywil.github.io/jquery.panzoom/demo/)
/**

(function(global, factory) {
// AMD
if (typeof define === 'function' && define.amd) {
define([ 'jquery' ], function(jQuery) {
return factory(global, jQuery);
});
// CommonJS/Browserify
} else if (typeof exports === 'object') {
factory(global, require('jquery'));
// Global
} else {
factory(global, global.jQuery);
}
}(typeof window !== 'undefined' ? window : this, function(window, $) {
'use strict';

var document = window.document;
var datakey = '__pz__';
var slice = Array.prototype.slice;
var rIE11 = /trident\/7./i;
var supportsInputEvent = (function() {
	// IE11 returns a false positive
	if (rIE11.test(navigator.userAgent)) {
		return false;
	}
	var input = document.createElement('input');
	input.setAttribute('oninput', 'return');
	return typeof input.oninput === 'function';
})();

// Regex
var rupper = /([A-Z])/g;
var rsvg = /^http:[\w\.\/]+svg$/;

var floating = '(\\-?\\d[\\d\\.e-]*)';
var commaSpace = '\\,?\\s*';
var rmatrix = new RegExp(
	'^matrix\\(' +
	floating + commaSpace +
	floating + commaSpace +
	floating + commaSpace +
	floating + commaSpace +
	floating + commaSpace +
	floating + '\\)$'
);

/**
 * Utility for determining transform matrix equality
 * Checks backwards to test translation first
 * @param {Array} first
 * @param {Array} second
 */
function matrixEquals(first, second) {
	var i = first.length;
	while(--i) {
		if (Math.round(+first[i]) !== Math.round(+second[i])) {
			return false;
		}
	}
	return true;
}

/**
 * Creates the options object for reset functions
 * @param {Boolean|Object} opts See reset methods
 * @returns {Object} Returns the newly-created options object
 */
function createResetOptions(opts) {
	var options = { range: true, animate: true };
	if (typeof opts === 'boolean') {
		options.animate = opts;
	} else {
		$.extend(options, opts);
	}
	return options;
}

/**
 * Represent a transformation matrix with a 3x3 matrix for calculations
 * Matrix functions adapted from Louis Remi's jQuery.transform (https://github.com/louisremi/jquery.transform.js)
 * @param {Array|Number} a An array of six values representing a 2d transformation matrix
 */
function Matrix(a, b, c, d, e, f, g, h, i) {
	if ($.type(a) === 'array') {
		this.elements = [
			+a[0], +a[2], +a[4],
			+a[1], +a[3], +a[5],
			    0,     0,     1
		];
	} else {
		this.elements = [
			a, b, c,
			d, e, f,
			g || 0, h || 0, i || 1
		];
	}
}

Matrix.prototype = {
	/**
	 * Multiply a 3x3 matrix by a similar matrix or a vector
	 * @param {Matrix|Vector} matrix
	 * @return {Matrix|Vector} Returns a vector if multiplying by a vector
	 */
	x: function(matrix) {
		var isVector = matrix instanceof Vector;

		var a = this.elements,
			b = matrix.elements;

		if (isVector && b.length === 3) {
			// b is actually a vector
			return new Vector(
				a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
				a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
				a[6] * b[0] + a[7] * b[1] + a[8] * b[2]
			);
		} else if (b.length === a.length) {
			// b is a 3x3 matrix
			return new Matrix(
				a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
				a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
				a[0] * b[2] + a[1] * b[5] + a[2] * b[8],

				a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
				a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
				a[3] * b[2] + a[4] * b[5] + a[5] * b[8],

				a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
				a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
				a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
			);
		}
		return false; // fail
	},
	/**
	 * Generates an inverse of the current matrix
	 * @returns {Matrix}
	 */
	inverse: function() {
		var d = 1 / this.determinant(),
			a = this.elements;
		return new Matrix(
			d * ( a[8] * a[4] - a[7] * a[5]),
			d * (-(a[8] * a[1] - a[7] * a[2])),
			d * ( a[5] * a[1] - a[4] * a[2]),

			d * (-(a[8] * a[3] - a[6] * a[5])),
			d * ( a[8] * a[0] - a[6] * a[2]),
			d * (-(a[5] * a[0] - a[3] * a[2])),

			d * ( a[7] * a[3] - a[6] * a[4]),
			d * (-(a[7] * a[0] - a[6] * a[1])),
			d * ( a[4] * a[0] - a[3] * a[1])
		);
	},
	/**
	 * Calculates the determinant of the current matrix
	 * @returns {Number}
	 */
	determinant: function() {
		var a = this.elements;
		return a[0] * (a[8] * a[4] - a[7] * a[5]) - a[3] * (a[8] * a[1] - a[7] * a[2]) + a[6] * (a[5] * a[1] - a[4] * a[2]);
	}
};

/**
 * Create a vector containing three values
 */
function Vector(x, y, z) {
	this.elements = [ x, y, z ];
}

/**
 * Get the element at zero-indexed index i
 * @param {Number} i
 */
Vector.prototype.e = Matrix.prototype.e = function(i) {
	return this.elements[ i ];
};

/**
 * Create a Panzoom object for a given element
 * @constructor
 * @param {Element} elem - Element to use pan and zoom
 * @param {Object} [options] - An object literal containing options to override default options
 *  (See Panzoom.defaults for ones not listed below)
 * @param {jQuery} [options.$zoomIn] - zoom in buttons/links collection (you can also bind these yourself
 *  e.g. $button.on('click', function(e) { e.preventDefault(); $elem.panzoom('zoomIn'); });)
 * @param {jQuery} [options.$zoomOut] - zoom out buttons/links collection on which to bind zoomOut
 * @param {jQuery} [options.$zoomRange] - zoom in/out with this range control
 * @param {jQuery} [options.$reset] - Reset buttons/links collection on which to bind the reset method
 * @param {Function} [options.on[Start|Change|Zoom|Pan|End|Reset] - Optional callbacks for panzoom events
 */
function Panzoom(elem, options) {

	// Allow instantiation without `new` keyword
	if (!(this instanceof Panzoom)) {
		return new Panzoom(elem, options);
	}

	// Sanity checks
	if (elem.nodeType !== 1) {
		$.error('Panzoom called on non-Element node');
	}
	if (!$.contains(document, elem)) {
		$.error('Panzoom element must be attached to the document');
	}

	// Don't remake
	var d = $.data(elem, datakey);
	if (d) {
		return d;
	}

	// Extend default with given object literal
	// Each instance gets its own options
	this.options = options = $.extend({}, Panzoom.defaults, options);
	this.elem = elem;
	var $elem = this.$elem = $(elem);
	this.$set = options.$set && options.$set.length ? options.$set : $elem;
	this.$doc = $(elem.ownerDocument || document);
	this.$parent = $elem.parent();
	this.parent = this.$parent[0];

	// This is SVG if the namespace is SVG
	// However, while <svg> elements are SVG, we want to treat those like other elements
	this.isSVG = rsvg.test(elem.namespaceURI) && elem.nodeName.toLowerCase() !== 'svg';

	this.panning = false;

	// Save the original transform value
	// Save the prefixed transform style key
	// Set the starting transform
	this._buildTransform();

	// Build the appropriately-prefixed transform style property name
	// De-camelcase
	this._transform = $.cssProps.transform.replace(rupper, '-$1').toLowerCase();

	// Build the transition value
	this._buildTransition();

	// Build containment dimensions
	this.resetDimensions();

	// Add zoom and reset buttons to `this`
	var $empty = $();
	var self = this;
	$.each([ '$zoomIn', '$zoomOut', '$zoomRange', '$reset' ], function(i, name) {
		self[ name ] = options[ name ] || $empty;
	});

	this.enable();

	this.scale = this.getMatrix()[0];
	this._checkPanWhenZoomed();

	// Save the instance
	$.data(elem, datakey, this);
}

// Attach regex for possible use (immutable)
Panzoom.rmatrix = rmatrix;

Panzoom.defaults = {
	// Should always be non-empty
	// Used to bind jQuery events without collisions
	// A guid is not added here as different instantiations/versions of panzoom
	// on the same element is not supported, so don't do it.
	eventNamespace: '.panzoom',

	// Whether or not to transition the scale
	transition: true,

	// Default cursor style for the element
	cursor: 'move',

	// There may be some use cases for zooming without panning or vice versa
	disablePan: false,
	disableZoom: false,

	// Pan only on the X or Y axes
	disableXAxis: false,
	disableYAxis: false,

	// Set whether you'd like to pan on left (1), middle (2), or right click (3)
	which: 1,

	// The increment at which to zoom
	// adds/subtracts to the scale each time zoomIn/Out is called
	increment: 0.3,

	// Turns on exponential zooming
	// If false, zooming will be incremented linearly
	exponential: true,

	// Pan only when the scale is greater than minScale
	panOnlyWhenZoomed: false,

	// min and max zoom scales
	minScale: 0.3,
	maxScale: 6,

	// The default step for the range input
	// Precendence: default < HTML attribute < option setting
	rangeStep: 0.05,

	// Animation duration (ms)
	duration: 200,
	// CSS easing used for scale transition
	easing: 'ease-in-out',

	// Indicate that the element should be contained within it's parent when panning
	// Note: this does not affect zooming outside of the parent
	// Set this value to 'invert' to only allow panning outside of the parent element (basically the opposite of the normal use of contain)
	// 'invert' is useful for a large panzoom element where you don't want to show anything behind it
	contain: false
};

Panzoom.prototype = {
	constructor: Panzoom,

	/**
	 * @returns {Panzoom} Returns the instance
	 */
	instance: function() {
		return this;
	},

	/**
	 * Enable or re-enable the panzoom instance
	 */
	enable: function() {
		// Unbind first
		this._initStyle();
		this._bind();
		this.disabled = false;
	},

	/**
	 * Disable panzoom
	 */
	disable: function() {
		this.disabled = true;
		this._resetStyle();
		this._unbind();
	},

	/**
	 * @returns {Boolean} Returns whether the current panzoom instance is disabled
	 */
	isDisabled: function() {
		return this.disabled;
	},

	/**
	 * Destroy the panzoom instance
	 */
	destroy: function() {
		this.disable();
		$.removeData(this.elem, datakey);
	},

	/**
	 * Builds the restricing dimensions from the containment element
	 * Also used with focal points
	 * Call this method whenever the dimensions of the element or parent are changed
	 */
	resetDimensions: function() {
		// Reset container properties
		this.container = this.parent.getBoundingClientRect();

		// Set element properties
		var elem = this.elem;
		// getBoundingClientRect() works with SVG, offsetWidth does not
		var dims = elem.getBoundingClientRect();
		var absScale = Math.abs(this.scale);
		this.dimensions = {
			width: dims.width,
			height: dims.height,
			left: $.css(elem, 'left', true) || 0,
			top: $.css(elem, 'top', true) || 0,
			// Borders and margins are scaled
			border: {
				top: $.css(elem, 'borderTopWidth', true) * absScale || 0,
				bottom: $.css(elem, 'borderBottomWidth', true) * absScale || 0,
				left: $.css(elem, 'borderLeftWidth', true) * absScale || 0,
				right: $.css(elem, 'borderRightWidth', true) * absScale || 0
			},
			margin: {
				top: $.css(elem, 'marginTop', true) * absScale || 0,
				left: $.css(elem, 'marginLeft', true) * absScale || 0
			}
		};
	},

	/**
	 * Return the element to it's original transform matrix
	 * @param {Boolean} [options] If a boolean is passed, animate the reset (default: true). If an options object is passed, simply pass that along to setMatrix.
	 * @param {Boolean} [options.silent] Silence the reset event
	 */
	reset: function(options) {
		options = createResetOptions(options);
		// Reset the transform to its original value
		var matrix = this.setMatrix(this._origTransform, options);
		if (!options.silent) {
			this._trigger('reset', matrix);
		}
	},

	/**
	 * Only resets zoom level
	 * @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to zoom()
	 */
	resetZoom: function(options) {
		options = createResetOptions(options);
		var origMatrix = this.getMatrix(this._origTransform);
		options.dValue = origMatrix[ 3 ];
		this.zoom(origMatrix[0], options);
	},

	/**
	 * Only reset panning
	 * @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to pan()
	 */
	resetPan: function(options) {
		var origMatrix = this.getMatrix(this._origTransform);
		this.pan(origMatrix[4], origMatrix[5], createResetOptions(options));
	},

	/**
	 * Sets a transform on the $set
	 * For SVG, the style attribute takes precedence
	 * and allows us to animate
	 * @param {String} transform
	 */
	setTransform: function(transform) {
		var $set = this.$set;
		var i = $set.length;
		while(i--) {
			$.style($set[i], 'transform', transform);

			// Support IE9-11, Edge 13-14+
			// Set attribute alongside style attribute
			// since IE and Edge do not respect style settings on SVG
			// See https://css-tricks.com/transforms-on-svg-elements/
			if (this.isSVG) {
				$set[i].setAttribute('transform', transform);
			}
		}
	},

	/**
	 * Retrieving the transform is different for SVG
	 *  (unless a style transform is already present)
	 * Uses the $set collection for retrieving the transform
	 * @param {String} [transform] Pass in an transform value (like 'scale(1.1)')
	 *  to have it formatted into matrix format for use by Panzoom
	 * @returns {String} Returns the current transform value of the element
	 */
	getTransform: function(transform) {
		var $set = this.$set;
		var transformElem = $set[0];
		if (transform) {
			this.setTransform(transform);
		} else {

			// IE and Edge still set the transform style properly
			// They just don't render it on SVG
			// So we get a correct value here
			transform = $.style(transformElem, 'transform');

			if (this.isSVG && (!transform || transform === 'none')) {
				transform = $.attr(transformElem, 'transform') || 'none';
			}
		}

		// Convert any transforms set by the user to matrix format
		// by setting to computed
		if (transform !== 'none' && !rmatrix.test(transform)) {

			// Get computed and set for next time
			this.setTransform(transform = $.css(transformElem, 'transform'));
		}

		return transform || 'none';
	},

	/**
	 * Retrieve the current transform matrix for $elem (or turn a transform into it's array values)
	 * @param {String} [transform] matrix-formatted transform value
	 * @returns {Array} Returns the current transform matrix split up into it's parts, or a default matrix
	 */
	getMatrix: function(transform) {
		var matrix = rmatrix.exec(transform || this.getTransform());
		if (matrix) {
			matrix.shift();
		}
		return matrix || [ 1, 0, 0, 1, 0, 0 ];
	},

	/**
	 * Given a matrix object, quickly set the current matrix of the element
	 * @param {Array|String} matrix
	 * @param {Object} [options]
	 * @param {Boolean|String} [options.animate] Whether to animate the transform change, or 'skip' indicating that it is unnecessary to set
	 * @param {Boolean} [options.contain] Override the global contain option
	 * @param {Boolean} [options.range] If true, $zoomRange's value will be updated.
	 * @param {Boolean} [options.silent] If true, the change event will not be triggered
	 * @returns {Array} Returns the newly-set matrix
	 */
	setMatrix: function(matrix, options) {
		if (this.disabled) { return; }
		if (!options) { options = {}; }
		// Convert to array
		if (typeof matrix === 'string') {
			matrix = this.getMatrix(matrix);
		}
		var scale = +matrix[0];
		var contain = typeof options.contain !== 'undefined' ? options.contain : this.options.contain;

		// Apply containment
		if (contain) {
			var dims = options.dims;
			if (!dims) {
				this.resetDimensions();
				dims = this.dimensions;
			}
			var spaceWLeft, spaceWRight, scaleDiff;
			var container = this.container;
			var width = dims.width;
			var height = dims.height;
			var conWidth = container.width;
			var conHeight = container.height;
			var zoomAspectW = conWidth / width;
			var zoomAspectH = conHeight / height;

			// If the element is not naturally centered,
			// assume full space right
			if (this.$parent.css('textAlign') !== 'center' || $.css(this.elem, 'display') !== 'inline') {
				// offsetWidth gets us the width without the transform
				scaleDiff = (width - this.elem.offsetWidth) / 2;
				spaceWLeft = scaleDiff - dims.border.left;
				spaceWRight = width - conWidth - scaleDiff + dims.border.right;
			} else {
				spaceWLeft = spaceWRight = ((width - conWidth) / 2);
			}
			var spaceHTop = ((height - conHeight) / 2) + dims.border.top;
			var spaceHBottom = ((height - conHeight) / 2) - dims.border.top - dims.border.bottom;

			if (contain === 'invert' || contain === 'automatic' && zoomAspectW < 1.01) {
				matrix[4] = Math.max(Math.min(matrix[4], spaceWLeft - dims.border.left), -spaceWRight);
			} else {
				matrix[4] = Math.min(Math.max(matrix[4], spaceWLeft), -spaceWRight);
			}

			if (contain === 'invert' || (contain === 'automatic' && zoomAspectH < 1.01)) {
				matrix[5] = Math.max(Math.min(matrix[5], spaceHTop - dims.border.top), -spaceHBottom);
			} else {
				matrix[5] = Math.min(Math.max(matrix[5], spaceHTop), -spaceHBottom);
			}
		}

		// Animate
		if (options.animate !== 'skip') {
			// Set transition
			this.transition(!options.animate);
		}

		// Update range element
		if (options.range) {
			this.$zoomRange.val(scale);
		}

		// Set the matrix on this.$set
		if (this.options.disableXAxis || this.options.disableYAxis) {
			var originalMatrix = this.getMatrix();
			if (this.options.disableXAxis) {
				matrix[4] = originalMatrix[4];
			}
			if (this.options.disableYAxis) {
				matrix[5] = originalMatrix[5];
			}
		}
		this.setTransform('matrix(' + matrix.join(',') + ')');

		this.scale = scale;

		// Disable/enable panning if zooming is at minimum and panOnlyWhenZoomed is true
		this._checkPanWhenZoomed(scale);

		if (!options.silent) {
			this._trigger('change', matrix);
		}

		return matrix;
	},

	/**
	 * @returns {Boolean} Returns whether the panzoom element is currently being dragged
	 */
	isPanning: function() {
		return this.panning;
	},

	/**
	 * Apply the current transition to the element, if allowed
	 * @param {Boolean} [off] Indicates that the transition should be turned off
	 */
	transition: function(off) {
		if (!this._transition) { return; }
		var transition = off || !this.options.transition ? 'none' : this._transition;
		var $set = this.$set;
		var i = $set.length;
		while(i--) {
			// Avoid reflows when zooming
			if ($.style($set[i], 'transition') !== transition) {
				$.style($set[i], 'transition', transition);
			}
		}
	},

	/**
	 * Pan the element to the specified translation X and Y
	 * Note: this is not the same as setting jQuery#offset() or jQuery#position()
	 * @param {Number} x
	 * @param {Number} y
	 * @param {Object} [options] These options are passed along to setMatrix
	 * @param {Array} [options.matrix] The matrix being manipulated (if already known so it doesn't have to be retrieved again)
	 * @param {Boolean} [options.silent] Silence the pan event. Note that this will also silence the setMatrix change event.
	 * @param {Boolean} [options.relative] Make the x and y values relative to the existing matrix
	 */
	pan: function(x, y, options) {
		if (this.options.disablePan) { return; }
		if (!options) { options = {}; }
		var matrix = options.matrix;
		if (!matrix) {
			matrix = this.getMatrix();
		}
		// Cast existing matrix values to numbers
		if (options.relative) {
			x += +matrix[4];
			y += +matrix[5];
		}
		matrix[4] = x;
		matrix[5] = y;
		this.setMatrix(matrix, options);
		if (!options.silent) {
			this._trigger('pan', matrix[4], matrix[5]);
		}
	},

	/**
	 * Zoom in/out the element using the scale properties of a transform matrix
	 * @param {Number|Boolean} [scale] The scale to which to zoom or a boolean indicating to transition a zoom out
	 * @param {Object} [opts] All global options can be overwritten by this options object. For example, override the default increment.
	 * @param {Boolean} [opts.noSetRange] Specify that the method should not set the $zoomRange value (as is the case when $zoomRange is calling zoom on change)
	 * @param {jQuery.Event|Object} [opts.focal] A focal point on the panzoom element on which to zoom.
	 *  If an object, set the clientX and clientY properties to the position relative to the parent
	 * @param {Boolean} [opts.animate] Whether to animate the zoom (defaults to true if scale is not a number, false otherwise)
	 * @param {Boolean} [opts.silent] Silence the zoom event
	 * @param {Array} [opts.matrix] Optionally pass the current matrix so it doesn't need to be retrieved
	 * @param {Number} [opts.dValue] Think of a transform matrix as four values a, b, c, d
	 *  where a/d are the horizontal/vertical scale values and b/c are the skew values
	 *  (5 and 6 of matrix array are the tx/ty transform values).
	 *  Normally, the scale is set to both the a and d values of the matrix.
	 *  This option allows you to specify a different d value for the zoom.
	 *  For instance, to flip vertically, you could set -1 as the dValue.
	 */
	zoom: function(scale, opts) {
		// Shuffle arguments
		if (typeof scale === 'object') {
			opts = scale;
			scale = null;
		} else if (!opts) {
			opts = {};
		}
		var options = $.extend({}, this.options, opts);
		// Check if disabled
		if (options.disableZoom) { return; }
		var animate = false;
		var matrix = options.matrix || this.getMatrix();
		var startScale = +matrix[0];

		// Calculate zoom based on increment
		if (typeof scale !== 'number') {
			// Just use a number a little greater than 1
			// Below 1 can use normal increments
			if (options.exponential && startScale - options.increment >= 1) {
				scale = Math[scale ? 'sqrt' : 'pow'](startScale, 2);
			} else {
				scale = startScale + (options.increment * (scale ? -1 : 1));
			}
			animate = true;
		}

		// Constrain scale
		if (scale > options.maxScale) {
			scale = options.maxScale;
		} else if (scale < options.minScale) {
			scale = options.minScale;
		}

		// Calculate focal point based on scale
		var focal = options.focal;
		if (focal && !options.disablePan) {
			// Adapted from code by Florian Günther
			// https://github.com/florianguenther/zui53
			this.resetDimensions();
			var dims = options.dims = this.dimensions;
			var clientX = focal.clientX;
			var clientY = focal.clientY;

			// Adjust the focal point for transform-origin 50% 50%
			// SVG elements have a transform origin of 0 0
			if (!this.isSVG) {
				clientX -= (dims.width / startScale) / 2;
				clientY -= (dims.height / startScale) / 2;
			}

			var clientV = new Vector(clientX, clientY, 1);
			var surfaceM = new Matrix(matrix);
			// Supply an offset manually if necessary
			var o = this.parentOffset || this.$parent.offset();
			var offsetM = new Matrix(1, 0, o.left - this.$doc.scrollLeft(), 0, 1, o.top - this.$doc.scrollTop());
			var surfaceV = surfaceM.inverse().x(offsetM.inverse().x(clientV));
			var scaleBy = scale / matrix[0];
			surfaceM = surfaceM.x(new Matrix([scaleBy, 0, 0, scaleBy, 0, 0]));
			clientV = offsetM.x(surfaceM.x(surfaceV));
			matrix[4] = +matrix[4] + (clientX - clientV.e(0));
			matrix[5] = +matrix[5] + (clientY - clientV.e(1));
		}

		// Set the scale
		matrix[0] = scale;
		matrix[3] = typeof options.dValue === 'number' ? options.dValue : scale;

		// Calling zoom may still pan the element
		this.setMatrix(matrix, {
			animate: typeof options.animate !== 'undefined' ? options.animate : animate,
			// Set the zoomRange value
			range: !options.noSetRange
		});

		// Trigger zoom event
		if (!options.silent) {
			this._trigger('zoom', matrix[0], options);
		}
	},

	/**
	 * Get/set option on an existing instance
	 * @returns {Array|undefined} If getting, returns an array of all values
	 *   on each instance for a given key. If setting, continue chaining by returning undefined.
	 */
	option: function(key, value) {
		var options;
		if (!key) {
			// Avoids returning direct reference
			return $.extend({}, this.options);
		}

		if (typeof key === 'string') {
			if (arguments.length === 1) {
				return this.options[ key ] !== undefined ?
					this.options[ key ] :
					null;
			}
			options = {};
			options[ key ] = value;
		} else {
			options = key;
		}

		this._setOptions(options);
	},

	/**
	 * Internally sets options
	 * @param {Object} options - An object literal of options to set
	 * @private
	 */
	_setOptions: function(options) {
		$.each(options, $.proxy(function(key, value) {
			switch(key) {
				case 'disablePan':
					this._resetStyle();
					/* falls through */
				case '$zoomIn':
				case '$zoomOut':
				case '$zoomRange':
				case '$reset':
				case 'disableZoom':
				case 'onStart':
				case 'onChange':
				case 'onZoom':
				case 'onPan':
				case 'onEnd':
				case 'onReset':
				case 'eventNamespace':
					this._unbind();
			}
			this.options[ key ] = value;
			switch(key) {
				case 'disablePan':
					this._initStyle();
					/* falls through */
				case '$zoomIn':
				case '$zoomOut':
				case '$zoomRange':
				case '$reset':
					// Set these on the instance
					this[ key ] = value;
					/* falls through */
				case 'disableZoom':
				case 'onStart':
				case 'onChange':
				case 'onZoom':
				case 'onPan':
				case 'onEnd':
				case 'onReset':
				case 'eventNamespace':
					this._bind();
					break;
				case 'cursor':
					$.style(this.elem, 'cursor', value);
					break;
				case 'minScale':
					this.$zoomRange.attr('min', value);
					break;
				case 'maxScale':
					this.$zoomRange.attr('max', value);
					break;
				case 'rangeStep':
					this.$zoomRange.attr('step', value);
					break;
				case 'startTransform':
					this._buildTransform();
					break;
				case 'duration':
				case 'easing':
					this._buildTransition();
					/* falls through */
				case 'transition':
					this.transition();
					break;
				case 'panOnlyWhenZoomed':
					this._checkPanWhenZoomed();
					break;
				case '$set':
					if (value instanceof $ && value.length) {
						this.$set = value;
						// Reset styles
						this._initStyle();
						this._buildTransform();
					}
			}
		}, this));
	},

	/**
	 * Disable/enable panning depending on whether the current scale
	 * matches the minimum
	 * @param {Number} [scale]
	 * @private
	 */
	_checkPanWhenZoomed: function(scale) {
		var options = this.options;
		if (options.panOnlyWhenZoomed) {
			if (!scale) {
				scale = this.getMatrix()[0];
			}
			var toDisable = scale <= options.minScale;
			if (options.disablePan !== toDisable) {
				this.option('disablePan', toDisable);
			}
		}
	},

	/**
	 * Initialize base styles for the element and its parent
	 * @private
	 */
	_initStyle: function() {
		var styles = {
			// Set the same default whether SVG or HTML
			// transform-origin cannot be changed to 50% 50% in IE9-11 or Edge 13-14+
			'transform-origin': this.isSVG ? '0 0' : '50% 50%'
		};
		// Set elem styles
		if (!this.options.disablePan) {
			styles.cursor = this.options.cursor;
		}
		this.$set.css(styles);

		// Set parent to relative if set to static
		var $parent = this.$parent;
		// No need to add styles to the body
		if ($parent.length && !$.nodeName(this.parent, 'body')) {
			styles = {
				overflow: 'hidden'
			};
			if ($parent.css('position') === 'static') {
				styles.position = 'relative';
			}
			$parent.css(styles);
		}
	},

	/**
	 * Undo any styles attached in this plugin
	 * @private
	 */
	_resetStyle: function() {
		this.$elem.css({
			'cursor': '',
			'transition': ''
		});
		this.$parent.css({
			'overflow': '',
			'position': ''
		});
	},

	/**
	 * Binds all necessary events
	 * @private
	 */
	_bind: function() {
		var self = this;
		var options = this.options;
		var ns = options.eventNamespace;
		var str_down = 'mousedown' + ns + ' pointerdown' + ns + ' MSPointerDown' + ns;
		var str_start = 'touchstart' + ns + ' ' + str_down;
		var str_click = 'touchend' + ns + ' click' + ns + ' pointerup' + ns + ' MSPointerUp' + ns;
		var events = {};
		var $reset = this.$reset;
		var $zoomRange = this.$zoomRange;

		// Bind panzoom events from options
		$.each([ 'Start', 'Change', 'Zoom', 'Pan', 'End', 'Reset' ], function() {
			var m = options[ 'on' + this ];
			if ($.isFunction(m)) {
				events[ 'panzoom' + this.toLowerCase() + ns ] = m;
			}
		});

		// Bind $elem drag and click/touchdown events
		// Bind touchstart if either panning or zooming is enabled
		if (!options.disablePan || !options.disableZoom) {
			events[ str_start ] = function(e) {
				var touches;
				if (e.type === 'touchstart' ?
					// Touch
					(touches = e.touches || e.originalEvent.touches) &&
						((touches.length === 1 && !options.disablePan) || touches.length === 2) :
					// Mouse/Pointer: Ignore unexpected click types
					// Support: IE10 only
					// IE10 does not support e.button for MSPointerDown, but does have e.which
					!options.disablePan && (e.which || e.originalEvent.which) === options.which) {

					e.preventDefault();
					e.stopPropagation();
					self._startMove(e, touches);
				}
			};
			// Prevent the contextmenu event
			// if we're binding to right-click
			if (options.which === 3) {
				events.contextmenu = false;
			}
		}
		this.$elem.on(events);

		// Bind reset
		if ($reset.length) {
			$reset.on(str_click, function(e) {
				e.preventDefault();
				self.reset();
			});
		}

		// Set default attributes for the range input
		if ($zoomRange.length) {
			$zoomRange.attr({
				// Only set the range step if explicit or
				// set the default if there is no attribute present
				step: options.rangeStep === Panzoom.defaults.rangeStep &&
					$zoomRange.attr('step') ||
					options.rangeStep,
				min: options.minScale,
				max: options.maxScale
			}).prop({
				value: this.getMatrix()[0]
			});
		}

		// No bindings if zooming is disabled
		if (options.disableZoom) {
			return;
		}

		var $zoomIn = this.$zoomIn;
		var $zoomOut = this.$zoomOut;

		// Bind zoom in/out
		// Don't bind one without the other
		if ($zoomIn.length && $zoomOut.length) {
			// preventDefault cancels future mouse events on touch events
			$zoomIn.on(str_click, function(e) {
				e.preventDefault();
				self.zoom();
			});
			$zoomOut.on(str_click, function(e) {
				e.preventDefault();
				self.zoom(true);
			});
		}

		if ($zoomRange.length) {
			events = {};
			// Cannot prevent default action here
			events[ str_down ] = function() {
				self.transition(true);
			};
			// Zoom on input events if available and change events
			// See https://github.com/timmywil/jquery.panzoom/issues/90
			events[ (supportsInputEvent ? 'input' : 'change') + ns ] = function() {
				self.zoom(+this.value, { noSetRange: true });
			};
			$zoomRange.on(events);
		}
	},

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

	/**
	 * Builds the original transform value
	 * @private
	 */
	_buildTransform: function() {
		// Save the original transform
		// Retrieving this also adds the correct prefixed style name
		// to jQuery's internal $.cssProps
		return this._origTransform = this.getTransform(this.options.startTransform);
	},

	/**
	 * Set transition property for later use when zooming
	 * @private
	 */
	_buildTransition: function() {
		if (this._transform) {
			var options = this.options;
			this._transition = this._transform + ' ' + options.duration + 'ms ' + options.easing;
		}
	},

	/**
	 * Calculates the distance between two touch points
	 * Remember pythagorean?
	 * @param {Array} touches
	 * @returns {Number} Returns the distance
	 * @private
	 */
	_getDistance: function(touches) {
		var touch1 = touches[0];
		var touch2 = touches[1];
		return Math.sqrt(Math.pow(Math.abs(touch2.clientX - touch1.clientX), 2) + Math.pow(Math.abs(touch2.clientY - touch1.clientY), 2));
	},

	/**
	 * Constructs an approximated point in the middle of two touch points
	 * @returns {Object} Returns an object containing pageX and pageY
	 * @private
	 */
	_getMiddle: function(touches) {
		var touch1 = touches[0];
		var touch2 = touches[1];
		return {
			clientX: ((touch2.clientX - touch1.clientX) / 2) + touch1.clientX,
			clientY: ((touch2.clientY - touch1.clientY) / 2) + touch1.clientY
		};
	},

	/**
	 * Trigger a panzoom event on our element
	 * The event is passed the Panzoom instance
	 * @param {String|jQuery.Event} event
	 * @param {Mixed} arg1[, arg2, arg3, ...] Arguments to append to the trigger
	 * @private
	 */
	_trigger: function (event) {
		if (typeof event === 'string') {
			event = 'panzoom' + event;
		}
		this.$elem.triggerHandler(event, [this].concat(slice.call(arguments, 1)));
	},

	/**
	 * Starts the pan
	 * This is bound to mouse/touchmove on the element
	 * @param {jQuery.Event} event An event with pageX, pageY, and possibly the touches list
	 * @param {TouchList} [touches] The touches list if present
	 * @private
	 */
	_startMove: function(event, touches) {
		if (this.panning) {
			return;
		}
		var moveEvent, endEvent,
			startDistance, startScale, startMiddle,
			startPageX, startPageY, touch;
		var self = this;
		var options = this.options;
		var ns = options.eventNamespace;
		var matrix = this.getMatrix();
		var original = matrix.slice(0);
		var origPageX = +original[4];
		var origPageY = +original[5];
		var panOptions = { matrix: matrix, animate: 'skip' };
		var type = event.type;

		// Use proper events
		if (type === 'pointerdown') {
			moveEvent = 'pointermove';
			endEvent = 'pointerup';
		} else if (type === 'touchstart') {
			moveEvent = 'touchmove';
			endEvent = 'touchend';
		} else if (type === 'MSPointerDown') {
			moveEvent = 'MSPointerMove';
			endEvent = 'MSPointerUp';
		} else {
			moveEvent = 'mousemove';
			endEvent = 'mouseup';
		}

		// Add namespace
		moveEvent += ns;
		endEvent += ns;

		// Remove any transitions happening
		this.transition(true);

		// Indicate that we are currently panning
		this.panning = true;

		// Trigger start event
		this._trigger('start', event, touches);

		var setStart = function(event, touches) {
			if (touches) {
				if (touches.length === 2) {
					if (startDistance != null) {
						return;
					}
					startDistance = self._getDistance(touches);
					startScale = +matrix[0];
					startMiddle = self._getMiddle(touches);
					return;
				}
				if (startPageX != null) {
					return;
				}
				if ((touch = touches[0])) {
					startPageX = touch.pageX;
					startPageY = touch.pageY;
				}
			}
			if (startPageX != null) {
				return;
			}
			startPageX = event.pageX;
			startPageY = event.pageY;
		};

		setStart(event, touches);

		var move = function(e) {
			var coords;
			e.preventDefault();
			touches = e.touches || e.originalEvent.touches;
			setStart(e, touches);

			if (touches) {
				if (touches.length === 2) {

					// Calculate move on middle point
					var middle = self._getMiddle(touches);
					var diff = self._getDistance(touches) - startDistance;

					// Set zoom
					self.zoom(diff * (options.increment / 100) + startScale, {
						focal: middle,
						matrix: matrix,
						animate: 'skip'
					});

					// Set pan
					self.pan(
						+matrix[4] + middle.clientX - startMiddle.clientX,
						+matrix[5] + middle.clientY - startMiddle.clientY,
						panOptions
					);
					startMiddle = middle;
					return;
				}
				coords = touches[0] || { pageX: 0, pageY: 0 };
			}

			if (!coords) {
				coords = e;
			}

			self.pan(
				origPageX + coords.pageX - startPageX,
				origPageY + coords.pageY - startPageY,
				panOptions
			);
		};

		// Bind the handlers
		$(document)
			.off(ns)
			.on(moveEvent, move)
			.on(endEvent, function(e) {
				e.preventDefault();
				// Unbind all document events
				$(this).off(ns);
				self.panning = false;
				// Trigger our end event
				// Simply set the type to "panzoomend" to pass through all end properties
				// jQuery's `not` is used here to compare Array equality
				e.type = 'panzoomend';
				self._trigger(e, matrix, !matrixEquals(matrix, original));
			});
	}
};

// Add Panzoom as a static property
$.Panzoom = Panzoom;

/**
 * Extend jQuery
 * @param {Object|String} options - The name of a method to call on the prototype
 *  or an object literal of options
 * @returns {jQuery|Mixed} jQuery instance for regular chaining or the return value(s) of a panzoom method call
 */
$.fn.panzoom = function(options) {
	var instance, args, m, ret;

	// Call methods widget-style
	if (typeof options === 'string') {
		ret = [];
		args = slice.call(arguments, 1);
		this.each(function() {
			instance = $.data(this, datakey);

			if (!instance) {
				ret.push(undefined);

			// Ignore methods beginning with `_`
			} else if (options.charAt(0) !== '_' &&
				typeof (m = instance[ options ]) === 'function' &&
				// If nothing is returned, do not add to return values
				(m = m.apply(instance, args)) !== undefined) {

				ret.push(m);
			}
		});

		// Return an array of values for the jQuery instances
		// Or the value itself if there is only one
		// Or keep chaining
		return ret.length ?
			(ret.length === 1 ? ret[0] : ret) :
			this;
	}

	return this.each(function() { new Panzoom(this, options); });
};

return Panzoom;

}));

@ghost
Copy link
Author

ghost commented Apr 9, 2018

I tried with new version already but the problem is same, its not working in ipad and mobile devices.

@ramoncarreras
Copy link

Same problem here with android devices.

@marinoboscolo
Copy link

Same issue here with v3.2.2 on android 7 and 8.
First time you pinch it works. After release, it doesn't.
No console errors shown.

@tzeumer
Copy link

tzeumer commented Jul 6, 2018

There is an older ticket with this solution

The problem is, one only wants to disable "which" for mobile devices, not for "mouse pushers". So I think (hope) the way below works. Happy if anyone could confirm it. So far I think it works well with Chrome/Firefox mobile and with desktops. I'll test it with a Win 10 infoscreen (Chrome) on monday.

        var $panzoom = $("#element").panzoom({
            which: -1
           // other settings
        });
        // Add onetime event listener, that just changes "which" if a mouse is detected
        window.addEventListener('mousemove', function() {
            $("#element").panzoom("option", "which", 1);
        }, {once: true});
        /* 
        // Other way around; but this requires on simple touch before doing any pinching 
        // This "might" be the better solution a problem with Win8/10 and touchscreens?
        window.addEventListener('touchstart', function() {
            $("#element").panzoom("option", "which", -1);
        }, {once: true});
        */
```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants