jquery.panzoom.js 36 KB

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