blob: 66072e8d236ee6b1d789895e72bf36b3a81bbf41 [file] [log] [blame]
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Class to support scrollable containers for drag and drop.
*
* @author dgajda@google.com (Damian Gajda)
*/
goog.provide('goog.fx.DragScrollSupport');
goog.require('goog.Disposable');
goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
/**
* A scroll support class. Currently this class will automatically scroll
* a scrollable container node and scroll it by a fixed amount at a timed
* interval when the mouse is moved above or below the container or in vertical
* margin areas. Intended for use in drag and drop. This could potentially be
* made more general and could support horizontal scrolling.
*
* @param {Element} containerNode A container that can be scrolled.
* @param {number=} opt_margin Optional margin to use while scrolling.
* @param {boolean=} opt_externalMouseMoveTracking Whether mouse move events
* are tracked externally by the client object which calls the mouse move
* event handler, useful when events are generated for more than one source
* element and/or are not real mousemove events.
* @constructor
* @extends {goog.Disposable}
* @see ../demos/dragscrollsupport.html
*/
goog.fx.DragScrollSupport = function(containerNode, opt_margin,
opt_externalMouseMoveTracking) {
goog.Disposable.call(this);
/**
* The container to be scrolled.
* @type {Element}
* @private
*/
this.containerNode_ = containerNode;
/**
* Scroll timer that will scroll the container until it is stopped.
* It will scroll when the mouse is outside the scrolling area of the
* container.
*
* @type {goog.Timer}
* @private
*/
this.scrollTimer_ = new goog.Timer(goog.fx.DragScrollSupport.TIMER_STEP_);
/**
* EventHandler used to set up and tear down listeners.
* @type {goog.events.EventHandler<!goog.fx.DragScrollSupport>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* The current scroll delta.
* @type {goog.math.Coordinate}
* @private
*/
this.scrollDelta_ = new goog.math.Coordinate();
/**
* The container bounds.
* @type {goog.math.Rect}
* @private
*/
this.containerBounds_ = goog.style.getBounds(containerNode);
/**
* The margin for triggering a scroll.
* @type {number}
* @private
*/
this.margin_ = opt_margin || 0;
/**
* The bounding rectangle which if left triggers scrolling.
* @type {goog.math.Rect}
* @private
*/
this.scrollBounds_ = opt_margin ?
this.constrainBounds_(this.containerBounds_.clone()) :
this.containerBounds_;
this.setupListeners_(!!opt_externalMouseMoveTracking);
};
goog.inherits(goog.fx.DragScrollSupport, goog.Disposable);
/**
* The scroll timer step in ms.
* @type {number}
* @private
*/
goog.fx.DragScrollSupport.TIMER_STEP_ = 50;
/**
* The scroll step in pixels.
* @type {number}
* @private
*/
goog.fx.DragScrollSupport.SCROLL_STEP_ = 8;
/**
* The suggested scrolling margin.
* @type {number}
*/
goog.fx.DragScrollSupport.MARGIN = 32;
/**
* Whether scrolling should be constrained to happen only when the cursor is
* inside the container node.
* @type {boolean}
* @private
*/
goog.fx.DragScrollSupport.prototype.constrainScroll_ = false;
/**
* Whether horizontal scrolling is allowed.
* @type {boolean}
* @private
*/
goog.fx.DragScrollSupport.prototype.horizontalScrolling_ = true;
/**
* Sets whether scrolling should be constrained to happen only when the cursor
* is inside the container node.
* NOTE: If a margin is not set, then it does not make sense to
* contain the scroll, because in that case scroll will never be triggered.
* @param {boolean} constrain Whether scrolling should be constrained to happen
* only when the cursor is inside the container node.
*/
goog.fx.DragScrollSupport.prototype.setConstrainScroll = function(constrain) {
this.constrainScroll_ = !!this.margin_ && constrain;
};
/**
* Sets whether horizontal scrolling is allowed.
* @param {boolean} scrolling Whether horizontal scrolling is allowed.
*/
goog.fx.DragScrollSupport.prototype.setHorizontalScrolling =
function(scrolling) {
this.horizontalScrolling_ = scrolling;
};
/**
* Constrains the container bounds with respect to the margin.
*
* @param {goog.math.Rect} bounds The container element.
* @return {goog.math.Rect} The bounding rectangle used to calculate scrolling
* direction.
* @private
*/
goog.fx.DragScrollSupport.prototype.constrainBounds_ = function(bounds) {
var margin = this.margin_;
if (margin) {
var quarterHeight = bounds.height * 0.25;
var yMargin = Math.min(margin, quarterHeight);
bounds.top += yMargin;
bounds.height -= 2 * yMargin;
var quarterWidth = bounds.width * 0.25;
var xMargin = Math.min(margin, quarterWidth);
bounds.top += xMargin;
bounds.height -= 2 * xMargin;
}
return bounds;
};
/**
* Attaches listeners and activates automatic scrolling.
* @param {boolean} externalMouseMoveTracking Whether to enable internal
* mouse move event handling.
* @private
*/
goog.fx.DragScrollSupport.prototype.setupListeners_ = function(
externalMouseMoveTracking) {
if (!externalMouseMoveTracking) {
// Track mouse pointer position to determine scroll direction.
this.eventHandler_.listen(goog.dom.getOwnerDocument(this.containerNode_),
goog.events.EventType.MOUSEMOVE, this.onMouseMove);
}
// Scroll with a constant speed.
this.eventHandler_.listen(this.scrollTimer_, goog.Timer.TICK, this.onTick_);
};
/**
* Handler for timer tick event, scrolls the container by one scroll step if
* needed.
* @param {goog.events.Event} event Timer tick event.
* @private
*/
goog.fx.DragScrollSupport.prototype.onTick_ = function(event) {
this.containerNode_.scrollTop += this.scrollDelta_.y;
this.containerNode_.scrollLeft += this.scrollDelta_.x;
};
/**
* Handler for mouse moves events.
* @param {goog.events.Event} event Mouse move event.
*/
goog.fx.DragScrollSupport.prototype.onMouseMove = function(event) {
var deltaX = this.horizontalScrolling_ ? this.calculateScrollDelta(
event.clientX, this.scrollBounds_.left, this.scrollBounds_.width) : 0;
var deltaY = this.calculateScrollDelta(event.clientY,
this.scrollBounds_.top, this.scrollBounds_.height);
this.scrollDelta_.x = deltaX;
this.scrollDelta_.y = deltaY;
// If the scroll data is 0 or the event fired outside of the
// bounds of the container node.
if ((!deltaX && !deltaY) ||
(this.constrainScroll_ &&
!this.isInContainerBounds_(event.clientX, event.clientY))) {
this.scrollTimer_.stop();
} else if (!this.scrollTimer_.enabled) {
this.scrollTimer_.start();
}
};
/**
* Gets whether the input coordinate is in the container bounds.
* @param {number} x The x coordinate.
* @param {number} y The y coordinate.
* @return {boolean} Whether the input coordinate is in the container bounds.
* @private
*/
goog.fx.DragScrollSupport.prototype.isInContainerBounds_ = function(x, y) {
var containerBounds = this.containerBounds_;
return containerBounds.left <= x &&
containerBounds.left + containerBounds.width >= x &&
containerBounds.top <= y &&
containerBounds.top + containerBounds.height >= y;
};
/**
* Calculates scroll delta.
*
* @param {number} coordinate Current mouse pointer coordinate.
* @param {number} min The coordinate value below which scrolling up should be
* started.
* @param {number} rangeLength The length of the range in which scrolling should
* be disabled and above which scrolling down should be started.
* @return {number} The calculated scroll delta.
* @protected
*/
goog.fx.DragScrollSupport.prototype.calculateScrollDelta = function(
coordinate, min, rangeLength) {
var delta = 0;
if (coordinate < min) {
delta = -goog.fx.DragScrollSupport.SCROLL_STEP_;
} else if (coordinate > min + rangeLength) {
delta = goog.fx.DragScrollSupport.SCROLL_STEP_;
}
return delta;
};
/** @override */
goog.fx.DragScrollSupport.prototype.disposeInternal = function() {
goog.fx.DragScrollSupport.superClass_.disposeInternal.call(this);
this.eventHandler_.dispose();
this.scrollTimer_.dispose();
};