blob: 0a3682ffab643512b47a1467e826f441f78c2123 [file] [log] [blame]
/**
* SVGPan library 1.2.2
* ======================
*
* Given an unique existing element with a given id (or by default, the first
* g-element), including the library into any SVG adds the following
* capabilities:
*
* - Mouse panning
* - Mouse zooming (using the wheel)
* - Object dragging
*
* You can configure the behaviour of the pan/zoom/drag via setOptions().
*
* Known issues:
*
* - Zooming (while panning) on Safari has still some issues
*
* Releases:
*
* 1.2.2, Tue Aug 30 17:21:56 CEST 2011, Andrea Leofreddi
* - Fixed viewBox on root tag (#7)
* - Improved zoom speed (#2)
*
* 1.2.1, Mon Jul 4 00:33:18 CEST 2011, Andrea Leofreddi
* - Fixed a regression with mouse wheel (now working on Firefox 5)
* - Working with viewBox attribute (#4)
* - Added "use strict;" and fixed resulting warnings (#5)
* - Added configuration variables, dragging is disabled by default (#3)
*
* 1.2, Sat Mar 20 08:42:50 GMT 2010, Zeng Xiaohui
* Fixed a bug with browser mouse handler interaction
*
* 1.1, Wed Feb 3 17:39:33 GMT 2010, Zeng Xiaohui
* Updated the zoom code to support the mouse wheel on Safari/Chrome
*
* 1.0, Andrea Leofreddi
* First release
*/
/**
* @license
* This code is licensed under the following BSD license:
* Copyright 2009-2010 Andrea Leofreddi <a.leofreddi@itcharm.com>. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY Andrea Leofreddi ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL Andrea Leofreddi OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are
* those of the authors and should not be interpreted as representing official
* policies, either expressed or implied, of Andrea Leofreddi.
*
*/
goog.provide('svgpan.SvgPan');
goog.require('goog.Disposable');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.MouseWheelHandler');
/**
* Instantiates an SvgPan object.
* @param {string=} opt_graphElementId The id of the graph element.
* @param {Element=} opt_root An optional document root.
* @constructor
* @extends {goog.Disposable}
*/
svgpan.SvgPan = function(opt_graphElementId, opt_root) {
svgpan.SvgPan.base(this, 'constructor');
/** @private {Element} */
this.root_ = opt_root || document.documentElement;
/** @private {?string} */
this.graphElementId_ = opt_graphElementId || null;
/** @private {boolean} */
this.cancelNextClick_ = false;
/** @private {boolean} */
this.enablePan_ = true;
/** @private {boolean} */
this.enableZoom_ = true;
/** @private {boolean} */
this.enableDrag_ = false;
/** @private {number} */
this.zoomScale_ = 0.4;
/** @private {svgpan.SvgPan.State} */
this.state_ = svgpan.SvgPan.State.NONE;
/** @private {Element} */
this.svgRoot_ = null;
/** @private {Element} */
this.stateTarget_ = null;
/** @private {SVGPoint} */
this.stateOrigin_ = null;
/** @private {SVGMatrix} */
this.stateTf_ = null;
/** @private {goog.events.MouseWheelHandler} */
this.mouseWheelHandler_ = null;
this.setupHandlers_();
};
goog.inherits(svgpan.SvgPan, goog.Disposable);
/** @override */
svgpan.SvgPan.prototype.disposeInternal = function() {
svgpan.SvgPan.base(this, 'disposeInternal');
goog.events.removeAll(this.root_);
this.mouseWheelHandler_.dispose();
};
/**
* @enum {string}
*/
svgpan.SvgPan.State = {
NONE: 'none',
PAN: 'pan',
DRAG: 'drag'
};
/**
* Enables/disables panning the entire SVG (default = true).
* @param {boolean} enabled Whether or not to allow panning.
*/
svgpan.SvgPan.prototype.setPanEnabled = function(enabled) {
this.enablePan_ = enabled;
};
/**
* Enables/disables zooming (default = true).
* @param {boolean} enabled Whether or not to allow zooming (default = true).
*/
svgpan.SvgPan.prototype.setZoomEnabled = function(enabled) {
this.enableZoom_ = enabled;
};
/**
* Enables/disables dragging individual SVG objects (default = false).
* @param {boolean} enabled Whether or not to allow dragging of objects.
*/
svgpan.SvgPan.prototype.setDragEnabled = function(enabled) {
this.enableDrag_ = enabled;
};
/**
* Sets the sensitivity of mousewheel zooming (default = 0.4).
* @param {number} scale The new zoom scale.
*/
svgpan.SvgPan.prototype.setZoomScale = function(scale) {
this.zoomScale_ = scale;
};
/**
* Registers mouse event handlers.
* @private
*/
svgpan.SvgPan.prototype.setupHandlers_ = function() {
goog.events.listen(this.root_, goog.events.EventType.CLICK,
goog.bind(this.handleMouseClick_, this));
goog.events.listen(this.root_, goog.events.EventType.MOUSEUP,
goog.bind(this.handleMouseUp_, this));
goog.events.listen(this.root_, goog.events.EventType.MOUSEDOWN,
goog.bind(this.handleMouseDown_, this));
goog.events.listen(this.root_, goog.events.EventType.MOUSEMOVE,
goog.bind(this.handleMouseMove_, this));
this.mouseWheelHandler_ = new goog.events.MouseWheelHandler(this.root_);
goog.events.listen(this.mouseWheelHandler_,
goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
goog.bind(this.handleMouseWheel_, this));
};
/**
* Retrieves the root element for SVG manipulation. The element is then cached.
* @param {Document} svgDoc The document.
* @return {Element} The svg root.
* @private
*/
svgpan.SvgPan.prototype.getRoot_ = function(svgDoc) {
if (!this.svgRoot_) {
var r = this.graphElementId_ ?
svgDoc.getElementById(this.graphElementId_) : svgDoc.documentElement;
var t = r;
while (t != svgDoc) {
if (t.getAttribute('viewBox')) {
this.setCtm_(r, r.getCTM());
t.removeAttribute('viewBox');
}
t = t.parentNode;
}
this.svgRoot_ = r;
}
return this.svgRoot_;
};
/**
* Instantiates an SVGPoint object with given event coordinates.
* @param {!goog.events.Event} evt The event with coordinates.
* @return {SVGPoint} The created point.
* @private
*/
svgpan.SvgPan.prototype.getEventPoint_ = function(evt) {
return this.newPoint_(evt.clientX, evt.clientY);
};
/**
* Instantiates an SVGPoint object with given coordinates.
* @param {number} x The x coordinate.
* @param {number} y The y coordinate.
* @return {SVGPoint} The created point.
* @private
*/
svgpan.SvgPan.prototype.newPoint_ = function(x, y) {
var p = this.root_.createSVGPoint();
p.x = x;
p.y = y;
return p;
};
/**
* Sets the current transform matrix of an element.
* @param {Element} element The element.
* @param {SVGMatrix} matrix The transform matrix.
* @private
*/
svgpan.SvgPan.prototype.setCtm_ = function(element, matrix) {
var s = 'matrix(' + matrix.a + ',' + matrix.b + ',' + matrix.c + ',' +
matrix.d + ',' + matrix.e + ',' + matrix.f + ')';
element.setAttribute('transform', s);
};
/**
* Handle mouse wheel event.
* @param {!goog.events.Event} evt The event.
* @private
*/
svgpan.SvgPan.prototype.handleMouseWheel_ = function(evt) {
if (!this.enableZoom_)
return;
// Prevents scrolling.
evt.preventDefault();
var svgDoc = evt.target.ownerDocument;
var delta = evt.deltaY / -9;
var z = Math.pow(1 + this.zoomScale_, delta);
var g = this.getRoot_(svgDoc);
var p = this.getEventPoint_(evt);
p = p.matrixTransform(g.getCTM().inverse());
// Compute new scale matrix in current mouse position
var k = this.root_.createSVGMatrix().translate(
p.x, p.y).scale(z).translate(-p.x, -p.y);
this.setCtm_(g, g.getCTM().multiply(k));
if (typeof(this.stateTf_) == 'undefined') {
this.stateTf_ = g.getCTM().inverse();
}
this.stateTf_ =
this.stateTf_ ? this.stateTf_.multiply(k.inverse()) : this.stateTf_;
};
/**
* Handle mouse move event.
* @param {!goog.events.Event} evt The event.
* @private
*/
svgpan.SvgPan.prototype.handleMouseMove_ = function(evt) {
if (evt.button != 0) {
return;
}
this.handleMove(evt.clientX, evt.clientY, evt.target.ownerDocument);
};
/**
* Handles mouse motion for the given coordinates.
* @param {number} x The x coordinate.
* @param {number} y The y coordinate.
* @param {Document} svgDoc The svg document.
*/
svgpan.SvgPan.prototype.handleMove = function(x, y, svgDoc) {
var g = this.getRoot_(svgDoc);
if (this.state_ == svgpan.SvgPan.State.PAN && this.enablePan_) {
// Pan mode
var p = this.newPoint_(x, y).matrixTransform(
/** @type {!SVGMatrix} */ (this.stateTf_));
this.setCtm_(g, this.stateTf_.inverse().translate(
p.x - this.stateOrigin_.x, p.y - this.stateOrigin_.y));
this.cancelNextClick_ = true;
} else if (this.state_ == svgpan.SvgPan.State.DRAG && this.enableDrag_) {
// Drag mode
var p = this.newPoint_(x, y).matrixTransform(g.getCTM().inverse());
this.setCtm_(this.stateTarget_, this.root_.createSVGMatrix().translate(
p.x - this.stateOrigin_.x, p.y - this.stateOrigin_.y).multiply(
g.getCTM().inverse()).multiply(this.stateTarget_.getCTM()));
this.stateOrigin_ = p;
}
};
/**
* Handle click event.
* @param {!goog.events.Event} evt The event.
* @private
*/
svgpan.SvgPan.prototype.handleMouseDown_ = function(evt) {
if (evt.button != 0) {
return;
}
// Prevent selection while dragging.
evt.preventDefault();
var svgDoc = evt.target.ownerDocument;
var g = this.getRoot_(svgDoc);
if (evt.target.tagName == 'svg' || !this.enableDrag_) {
// Pan mode
this.state_ = svgpan.SvgPan.State.PAN;
this.stateTf_ = g.getCTM().inverse();
this.stateOrigin_ = this.getEventPoint_(evt).matrixTransform(this.stateTf_);
} else {
// Drag mode
this.state_ = svgpan.SvgPan.State.DRAG;
this.stateTarget_ = /** @type {Element} */ (evt.target);
this.stateTf_ = g.getCTM().inverse();
this.stateOrigin_ = this.getEventPoint_(evt).matrixTransform(this.stateTf_);
}
};
/**
* Handle mouse button release event.
* @param {!goog.events.Event} evt The event.
* @private
*/
svgpan.SvgPan.prototype.handleMouseUp_ = function(evt) {
if (this.state_ != svgpan.SvgPan.State.NONE) {
this.endPanOrDrag();
}
};
/**
* Ends pan/drag mode.
*/
svgpan.SvgPan.prototype.endPanOrDrag = function() {
if (this.state_ != svgpan.SvgPan.State.NONE) {
this.state_ = svgpan.SvgPan.State.NONE;
}
};
/**
* Handle mouse clicks.
* @param {!goog.events.Event} evt The event.
* @private
*/
svgpan.SvgPan.prototype.handleMouseClick_ = function(evt) {
// We only set cancelNextClick_ after panning occurred, and use it to prevent
// the default action that would otherwise take place when clicking on the
// element (for instance, navigation on clickable links, but also any click
// handler that may be set on an SVG element, in the case of active SVG
// content)
if (this.cancelNextClick_) {
// Cancel potential click handler on active SVG content.
evt.stopPropagation();
// Cancel navigation when panning on clickable links.
evt.preventDefault();
}
this.cancelNextClick_ = false;
};
/**
* Returns the current state.
* @return {!svgpan.SvgPan.State}
*/
svgpan.SvgPan.prototype.getState = function() {
return this.state_;
};