svgpan.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /**
  2. * SVGPan library 1.2.2
  3. * ======================
  4. *
  5. * Given an unique existing element with a given id (or by default, the first
  6. * g-element), including the library into any SVG adds the following
  7. * capabilities:
  8. *
  9. * - Mouse panning
  10. * - Mouse zooming (using the wheel)
  11. * - Object dragging
  12. *
  13. * You can configure the behaviour of the pan/zoom/drag via setOptions().
  14. *
  15. * Known issues:
  16. *
  17. * - Zooming (while panning) on Safari has still some issues
  18. *
  19. * Releases:
  20. *
  21. * 1.2.2, Tue Aug 30 17:21:56 CEST 2011, Andrea Leofreddi
  22. * - Fixed viewBox on root tag (#7)
  23. * - Improved zoom speed (#2)
  24. *
  25. * 1.2.1, Mon Jul 4 00:33:18 CEST 2011, Andrea Leofreddi
  26. * - Fixed a regression with mouse wheel (now working on Firefox 5)
  27. * - Working with viewBox attribute (#4)
  28. * - Added "use strict;" and fixed resulting warnings (#5)
  29. * - Added configuration variables, dragging is disabled by default (#3)
  30. *
  31. * 1.2, Sat Mar 20 08:42:50 GMT 2010, Zeng Xiaohui
  32. * Fixed a bug with browser mouse handler interaction
  33. *
  34. * 1.1, Wed Feb 3 17:39:33 GMT 2010, Zeng Xiaohui
  35. * Updated the zoom code to support the mouse wheel on Safari/Chrome
  36. *
  37. * 1.0, Andrea Leofreddi
  38. * First release
  39. */
  40. /**
  41. * @license
  42. * This code is licensed under the following BSD license:
  43. * Copyright 2009-2010 Andrea Leofreddi <a.leofreddi@itcharm.com>. All rights
  44. * reserved.
  45. *
  46. * Redistribution and use in source and binary forms, with or without
  47. * modification, are permitted provided that the following conditions are met:
  48. *
  49. * 1. Redistributions of source code must retain the above copyright notice,
  50. * this list of conditions and the following disclaimer.
  51. *
  52. * 2. Redistributions in binary form must reproduce the above copyright
  53. * notice, this list of conditions and the following disclaimer in the
  54. * documentation and/or other materials provided with the distribution.
  55. *
  56. * THIS SOFTWARE IS PROVIDED BY Andrea Leofreddi ``AS IS'' AND ANY EXPRESS OR
  57. * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  58. * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
  59. * EVENT SHALL Andrea Leofreddi OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
  60. * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  61. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  62. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  63. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  64. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  65. * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  66. *
  67. * The views and conclusions contained in the software and documentation are
  68. * those of the authors and should not be interpreted as representing official
  69. * policies, either expressed or implied, of Andrea Leofreddi.
  70. *
  71. */
  72. goog.provide('svgpan.SvgPan');
  73. goog.require('goog.Disposable');
  74. goog.require('goog.events');
  75. goog.require('goog.events.EventType');
  76. goog.require('goog.events.MouseWheelHandler');
  77. /**
  78. * Instantiates an SvgPan object.
  79. * @param {string=} opt_graphElementId The id of the graph element.
  80. * @param {Element=} opt_root An optional document root.
  81. * @constructor
  82. * @extends {goog.Disposable}
  83. */
  84. svgpan.SvgPan = function(opt_graphElementId, opt_root) {
  85. svgpan.SvgPan.base(this, 'constructor');
  86. /** @private {Element} */
  87. this.root_ = opt_root || document.documentElement;
  88. /** @private {?string} */
  89. this.graphElementId_ = opt_graphElementId || null;
  90. /** @private {boolean} */
  91. this.cancelNextClick_ = false;
  92. /** @private {boolean} */
  93. this.enablePan_ = true;
  94. /** @private {boolean} */
  95. this.enableZoom_ = true;
  96. /** @private {boolean} */
  97. this.enableDrag_ = false;
  98. /** @private {number} */
  99. this.zoomScale_ = 0.4;
  100. /** @private {svgpan.SvgPan.State} */
  101. this.state_ = svgpan.SvgPan.State.NONE;
  102. /** @private {Element} */
  103. this.svgRoot_ = null;
  104. /** @private {Element} */
  105. this.stateTarget_ = null;
  106. /** @private {SVGPoint} */
  107. this.stateOrigin_ = null;
  108. /** @private {SVGMatrix} */
  109. this.stateTf_ = null;
  110. /** @private {goog.events.MouseWheelHandler} */
  111. this.mouseWheelHandler_ = null;
  112. this.setupHandlers_();
  113. };
  114. goog.inherits(svgpan.SvgPan, goog.Disposable);
  115. /** @override */
  116. svgpan.SvgPan.prototype.disposeInternal = function() {
  117. svgpan.SvgPan.base(this, 'disposeInternal');
  118. goog.events.removeAll(this.root_);
  119. this.mouseWheelHandler_.dispose();
  120. };
  121. /**
  122. * @enum {string}
  123. */
  124. svgpan.SvgPan.State = {
  125. NONE: 'none',
  126. PAN: 'pan',
  127. DRAG: 'drag'
  128. };
  129. /**
  130. * Enables/disables panning the entire SVG (default = true).
  131. * @param {boolean} enabled Whether or not to allow panning.
  132. */
  133. svgpan.SvgPan.prototype.setPanEnabled = function(enabled) {
  134. this.enablePan_ = enabled;
  135. };
  136. /**
  137. * Enables/disables zooming (default = true).
  138. * @param {boolean} enabled Whether or not to allow zooming (default = true).
  139. */
  140. svgpan.SvgPan.prototype.setZoomEnabled = function(enabled) {
  141. this.enableZoom_ = enabled;
  142. };
  143. /**
  144. * Enables/disables dragging individual SVG objects (default = false).
  145. * @param {boolean} enabled Whether or not to allow dragging of objects.
  146. */
  147. svgpan.SvgPan.prototype.setDragEnabled = function(enabled) {
  148. this.enableDrag_ = enabled;
  149. };
  150. /**
  151. * Sets the sensitivity of mousewheel zooming (default = 0.4).
  152. * @param {number} scale The new zoom scale.
  153. */
  154. svgpan.SvgPan.prototype.setZoomScale = function(scale) {
  155. this.zoomScale_ = scale;
  156. };
  157. /**
  158. * Registers mouse event handlers.
  159. * @private
  160. */
  161. svgpan.SvgPan.prototype.setupHandlers_ = function() {
  162. goog.events.listen(this.root_, goog.events.EventType.CLICK,
  163. goog.bind(this.handleMouseClick_, this));
  164. goog.events.listen(this.root_, goog.events.EventType.MOUSEUP,
  165. goog.bind(this.handleMouseUp_, this));
  166. goog.events.listen(this.root_, goog.events.EventType.MOUSEDOWN,
  167. goog.bind(this.handleMouseDown_, this));
  168. goog.events.listen(this.root_, goog.events.EventType.MOUSEMOVE,
  169. goog.bind(this.handleMouseMove_, this));
  170. this.mouseWheelHandler_ = new goog.events.MouseWheelHandler(this.root_);
  171. goog.events.listen(this.mouseWheelHandler_,
  172. goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
  173. goog.bind(this.handleMouseWheel_, this));
  174. };
  175. /**
  176. * Retrieves the root element for SVG manipulation. The element is then cached.
  177. * @param {Document} svgDoc The document.
  178. * @return {Element} The svg root.
  179. * @private
  180. */
  181. svgpan.SvgPan.prototype.getRoot_ = function(svgDoc) {
  182. if (!this.svgRoot_) {
  183. var r = this.graphElementId_ ?
  184. svgDoc.getElementById(this.graphElementId_) : svgDoc.documentElement;
  185. var t = r;
  186. while (t != svgDoc) {
  187. if (t.getAttribute('viewBox')) {
  188. this.setCtm_(r, r.getCTM());
  189. t.removeAttribute('viewBox');
  190. }
  191. t = t.parentNode;
  192. }
  193. this.svgRoot_ = r;
  194. }
  195. return this.svgRoot_;
  196. };
  197. /**
  198. * Instantiates an SVGPoint object with given event coordinates.
  199. * @param {!goog.events.Event} evt The event with coordinates.
  200. * @return {SVGPoint} The created point.
  201. * @private
  202. */
  203. svgpan.SvgPan.prototype.getEventPoint_ = function(evt) {
  204. return this.newPoint_(evt.clientX, evt.clientY);
  205. };
  206. /**
  207. * Instantiates an SVGPoint object with given coordinates.
  208. * @param {number} x The x coordinate.
  209. * @param {number} y The y coordinate.
  210. * @return {SVGPoint} The created point.
  211. * @private
  212. */
  213. svgpan.SvgPan.prototype.newPoint_ = function(x, y) {
  214. var p = this.root_.createSVGPoint();
  215. p.x = x;
  216. p.y = y;
  217. return p;
  218. };
  219. /**
  220. * Sets the current transform matrix of an element.
  221. * @param {Element} element The element.
  222. * @param {SVGMatrix} matrix The transform matrix.
  223. * @private
  224. */
  225. svgpan.SvgPan.prototype.setCtm_ = function(element, matrix) {
  226. var s = 'matrix(' + matrix.a + ',' + matrix.b + ',' + matrix.c + ',' +
  227. matrix.d + ',' + matrix.e + ',' + matrix.f + ')';
  228. element.setAttribute('transform', s);
  229. };
  230. /**
  231. * Handle mouse wheel event.
  232. * @param {!goog.events.Event} evt The event.
  233. * @private
  234. */
  235. svgpan.SvgPan.prototype.handleMouseWheel_ = function(evt) {
  236. if (!this.enableZoom_)
  237. return;
  238. // Prevents scrolling.
  239. evt.preventDefault();
  240. var svgDoc = evt.target.ownerDocument;
  241. var delta = evt.deltaY / -9;
  242. var z = Math.pow(1 + this.zoomScale_, delta);
  243. var g = this.getRoot_(svgDoc);
  244. var p = this.getEventPoint_(evt);
  245. p = p.matrixTransform(g.getCTM().inverse());
  246. // Compute new scale matrix in current mouse position
  247. var k = this.root_.createSVGMatrix().translate(
  248. p.x, p.y).scale(z).translate(-p.x, -p.y);
  249. this.setCtm_(g, g.getCTM().multiply(k));
  250. if (typeof(this.stateTf_) == 'undefined') {
  251. this.stateTf_ = g.getCTM().inverse();
  252. }
  253. this.stateTf_ =
  254. this.stateTf_ ? this.stateTf_.multiply(k.inverse()) : this.stateTf_;
  255. };
  256. /**
  257. * Handle mouse move event.
  258. * @param {!goog.events.Event} evt The event.
  259. * @private
  260. */
  261. svgpan.SvgPan.prototype.handleMouseMove_ = function(evt) {
  262. if (evt.button != 0) {
  263. return;
  264. }
  265. this.handleMove(evt.clientX, evt.clientY, evt.target.ownerDocument);
  266. };
  267. /**
  268. * Handles mouse motion for the given coordinates.
  269. * @param {number} x The x coordinate.
  270. * @param {number} y The y coordinate.
  271. * @param {Document} svgDoc The svg document.
  272. */
  273. svgpan.SvgPan.prototype.handleMove = function(x, y, svgDoc) {
  274. var g = this.getRoot_(svgDoc);
  275. if (this.state_ == svgpan.SvgPan.State.PAN && this.enablePan_) {
  276. // Pan mode
  277. var p = this.newPoint_(x, y).matrixTransform(
  278. /** @type {!SVGMatrix} */ (this.stateTf_));
  279. this.setCtm_(g, this.stateTf_.inverse().translate(
  280. p.x - this.stateOrigin_.x, p.y - this.stateOrigin_.y));
  281. this.cancelNextClick_ = true;
  282. } else if (this.state_ == svgpan.SvgPan.State.DRAG && this.enableDrag_) {
  283. // Drag mode
  284. var p = this.newPoint_(x, y).matrixTransform(g.getCTM().inverse());
  285. this.setCtm_(this.stateTarget_, this.root_.createSVGMatrix().translate(
  286. p.x - this.stateOrigin_.x, p.y - this.stateOrigin_.y).multiply(
  287. g.getCTM().inverse()).multiply(this.stateTarget_.getCTM()));
  288. this.stateOrigin_ = p;
  289. }
  290. };
  291. /**
  292. * Handle click event.
  293. * @param {!goog.events.Event} evt The event.
  294. * @private
  295. */
  296. svgpan.SvgPan.prototype.handleMouseDown_ = function(evt) {
  297. if (evt.button != 0) {
  298. return;
  299. }
  300. // Prevent selection while dragging.
  301. evt.preventDefault();
  302. var svgDoc = evt.target.ownerDocument;
  303. var g = this.getRoot_(svgDoc);
  304. if (evt.target.tagName == 'svg' || !this.enableDrag_) {
  305. // Pan mode
  306. this.state_ = svgpan.SvgPan.State.PAN;
  307. this.stateTf_ = g.getCTM().inverse();
  308. this.stateOrigin_ = this.getEventPoint_(evt).matrixTransform(this.stateTf_);
  309. } else {
  310. // Drag mode
  311. this.state_ = svgpan.SvgPan.State.DRAG;
  312. this.stateTarget_ = /** @type {Element} */ (evt.target);
  313. this.stateTf_ = g.getCTM().inverse();
  314. this.stateOrigin_ = this.getEventPoint_(evt).matrixTransform(this.stateTf_);
  315. }
  316. };
  317. /**
  318. * Handle mouse button release event.
  319. * @param {!goog.events.Event} evt The event.
  320. * @private
  321. */
  322. svgpan.SvgPan.prototype.handleMouseUp_ = function(evt) {
  323. if (this.state_ != svgpan.SvgPan.State.NONE) {
  324. this.endPanOrDrag();
  325. }
  326. };
  327. /**
  328. * Ends pan/drag mode.
  329. */
  330. svgpan.SvgPan.prototype.endPanOrDrag = function() {
  331. if (this.state_ != svgpan.SvgPan.State.NONE) {
  332. this.state_ = svgpan.SvgPan.State.NONE;
  333. }
  334. };
  335. /**
  336. * Handle mouse clicks.
  337. * @param {!goog.events.Event} evt The event.
  338. * @private
  339. */
  340. svgpan.SvgPan.prototype.handleMouseClick_ = function(evt) {
  341. // We only set cancelNextClick_ after panning occurred, and use it to prevent
  342. // the default action that would otherwise take place when clicking on the
  343. // element (for instance, navigation on clickable links, but also any click
  344. // handler that may be set on an SVG element, in the case of active SVG
  345. // content)
  346. if (this.cancelNextClick_) {
  347. // Cancel potential click handler on active SVG content.
  348. evt.stopPropagation();
  349. // Cancel navigation when panning on clickable links.
  350. evt.preventDefault();
  351. }
  352. this.cancelNextClick_ = false;
  353. };
  354. /**
  355. * Returns the current state.
  356. * @return {!svgpan.SvgPan.State}
  357. */
  358. svgpan.SvgPan.prototype.getState = function() {
  359. return this.state_;
  360. };