1 /**
  2  * Returns a new pan behavior to be registered on mousedown events.
  3  *
  4  * @class Implements interactive panning starting with mousedown events.
  5  * Register this behavior on panels to allow panning. This behavior can be used
  6  * in tandem with {@link pv.Behavior.zoom} to allow both panning and zooming:
  7  *
  8  * <pre>    .event("mousedown", pv.Behavior.pan())
  9  *     .event("mousewheel", pv.Behavior.zoom())</pre>
 10  *
 11  * The pan behavior currently supports only mouse events; support for keyboard
 12  * shortcuts to improve accessibility may be added in the future.
 13  *
 14  * <p>After the initial mousedown event is triggered, this behavior listens for
 15  * mousemove and mouseup events on the window. This allows panning to continue
 16  * even if the mouse temporarily leaves the panel that is being panned, or even
 17  * the root panel.
 18  *
 19  * <p>The implementation of this behavior relies on the panel's
 20  * <tt>transform</tt> property, which specifies a matrix transformation that is
 21  * applied to child marks. Note that the transform property only affects the
 22  * panel's children, but not the panel itself; therefore the panel's fill and
 23  * stroke will not change when the contents are panned.
 24  *
 25  * <p>Panels have transparent fill styles by default; this means that panels may
 26  * not receive the initial mousedown event to start panning. To fix this
 27  * problem, either given the panel a visible fill style (such as "white"), or
 28  * set the <tt>events</tt> property to "all" such that the panel receives events
 29  * despite its transparent fill.
 30  *
 31  * <p>The pan behavior has optional support for bounding. If enabled, the user
 32  * will not be able to pan the panel outside of the initial bounds. This feature
 33  * is designed to work in conjunction with the zoom behavior; otherwise,
 34  * bounding the panel effectively disables all panning.
 35  *
 36  * @extends pv.Behavior
 37  * @see pv.Behavior.zoom
 38  * @see pv.Panel#transform
 39  */
 40 pv.Behavior.pan = function() {
 41   var scene, // scene context
 42       index, // scene context
 43       m1, // transformation matrix at the start of panning
 44       v1, // mouse location at the start of panning
 45       k, // inverse scale
 46       bound; // whether to bound to the panel
 47 
 48   /** @private */
 49   function mousedown() {
 50     index = this.index;
 51     scene = this.scene;
 52     v1 = pv.vector(pv.event.pageX, pv.event.pageY);
 53     m1 = this.transform();
 54     k = 1 / (m1.k * this.scale);
 55     if (bound) {
 56       bound = {
 57         x: (1 - m1.k) * this.width(),
 58         y: (1 - m1.k) * this.height()
 59       };
 60     }
 61   }
 62 
 63   /** @private */
 64   function mousemove() {
 65     if (!scene) return;
 66     scene.mark.context(scene, index, function() {
 67         var x = (pv.event.pageX - v1.x) * k,
 68             y = (pv.event.pageY - v1.y) * k,
 69             m = m1.translate(x, y);
 70         if (bound) {
 71           m.x = Math.max(bound.x, Math.min(0, m.x));
 72           m.y = Math.max(bound.y, Math.min(0, m.y));
 73         }
 74         this.transform(m).render();
 75       });
 76     pv.Mark.dispatch("pan", scene, index);
 77   }
 78 
 79   /** @private */
 80   function mouseup() {
 81     scene = null;
 82   }
 83 
 84   /**
 85    * Sets or gets the bound parameter. If bounding is enabled, the user will not
 86    * be able to pan outside the initial panel bounds; this typically applies
 87    * only when the pan behavior is used in tandem with the zoom behavior.
 88    * Bounding is not enabled by default.
 89    *
 90    * <p>Note: enabling bounding after panning has already occurred will not
 91    * immediately reset the transform. Bounding should be enabled before the
 92    * panning behavior is applied.
 93    *
 94    * @function
 95    * @returns {pv.Behavior.pan} this, or the current bound parameter.
 96    * @name pv.Behavior.pan.prototype.bound
 97    * @param {boolean} [x] the new bound parameter.
 98    */
 99   mousedown.bound = function(x) {
100     if (arguments.length) {
101       bound = Boolean(x);
102       return this;
103     }
104     return Boolean(bound);
105   };
106 
107   pv.listen(window, "mousemove", mousemove);
108   pv.listen(window, "mouseup", mouseup);
109   return mousedown;
110 };
111