dragscrollsupport.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Class to support scrollable containers for drag and drop.
  16. *
  17. * @author dgajda@google.com (Damian Gajda)
  18. */
  19. goog.provide('goog.fx.DragScrollSupport');
  20. goog.require('goog.Disposable');
  21. goog.require('goog.Timer');
  22. goog.require('goog.dom');
  23. goog.require('goog.events.EventHandler');
  24. goog.require('goog.events.EventType');
  25. goog.require('goog.math.Coordinate');
  26. goog.require('goog.style');
  27. /**
  28. * A scroll support class. Currently this class will automatically scroll
  29. * a scrollable container node and scroll it by a fixed amount at a timed
  30. * interval when the mouse is moved above or below the container or in vertical
  31. * margin areas. Intended for use in drag and drop. This could potentially be
  32. * made more general and could support horizontal scrolling.
  33. *
  34. * @param {Element} containerNode A container that can be scrolled.
  35. * @param {number=} opt_margin Optional margin to use while scrolling.
  36. * @param {boolean=} opt_externalMouseMoveTracking Whether mouse move events
  37. * are tracked externally by the client object which calls the mouse move
  38. * event handler, useful when events are generated for more than one source
  39. * element and/or are not real mousemove events.
  40. * @constructor
  41. * @struct
  42. * @extends {goog.Disposable}
  43. * @see ../demos/dragscrollsupport.html
  44. */
  45. goog.fx.DragScrollSupport = function(
  46. containerNode, opt_margin, opt_externalMouseMoveTracking) {
  47. goog.fx.DragScrollSupport.base(this, 'constructor');
  48. /**
  49. * Whether scrolling should be constrained to happen only when the cursor is
  50. * inside the container node.
  51. * @private {boolean}
  52. */
  53. this.constrainScroll_ = false;
  54. /**
  55. * Whether horizontal scrolling is allowed.
  56. * @private {boolean}
  57. */
  58. this.horizontalScrolling_ = true;
  59. /**
  60. * The container to be scrolled.
  61. * @type {Element}
  62. * @private
  63. */
  64. this.containerNode_ = containerNode;
  65. /**
  66. * Scroll timer that will scroll the container until it is stopped.
  67. * It will scroll when the mouse is outside the scrolling area of the
  68. * container.
  69. *
  70. * @type {goog.Timer}
  71. * @private
  72. */
  73. this.scrollTimer_ = new goog.Timer(goog.fx.DragScrollSupport.TIMER_STEP_);
  74. /**
  75. * EventHandler used to set up and tear down listeners.
  76. * @type {goog.events.EventHandler<!goog.fx.DragScrollSupport>}
  77. * @private
  78. */
  79. this.eventHandler_ = new goog.events.EventHandler(this);
  80. /**
  81. * The current scroll delta.
  82. * @type {goog.math.Coordinate}
  83. * @private
  84. */
  85. this.scrollDelta_ = new goog.math.Coordinate();
  86. /**
  87. * The container bounds.
  88. * @type {goog.math.Rect}
  89. * @private
  90. */
  91. this.containerBounds_ = goog.style.getBounds(containerNode);
  92. if (containerNode.tagName === 'BODY') {
  93. var size = goog.dom.getViewportSize();
  94. this.containerBounds_.height = size.height;
  95. this.containerBounds_.width = size.width;
  96. }
  97. /**
  98. * The margin for triggering a scroll.
  99. * @type {number}
  100. * @private
  101. */
  102. this.margin_ = opt_margin || 0;
  103. /**
  104. * The bounding rectangle which if left triggers scrolling.
  105. * @type {goog.math.Rect}
  106. * @private
  107. */
  108. this.scrollBounds_ = opt_margin ?
  109. this.constrainBounds_(this.containerBounds_.clone()) :
  110. this.containerBounds_;
  111. this.setupListeners_(!!opt_externalMouseMoveTracking);
  112. };
  113. goog.inherits(goog.fx.DragScrollSupport, goog.Disposable);
  114. /**
  115. * The scroll timer step in ms.
  116. * @type {number}
  117. * @private
  118. */
  119. goog.fx.DragScrollSupport.TIMER_STEP_ = 50;
  120. /**
  121. * The scroll step in pixels.
  122. * @type {number}
  123. * @private
  124. */
  125. goog.fx.DragScrollSupport.SCROLL_STEP_ = 8;
  126. /**
  127. * The suggested scrolling margin.
  128. * @type {number}
  129. */
  130. goog.fx.DragScrollSupport.MARGIN = 32;
  131. /**
  132. * Sets whether scrolling should be constrained to happen only when the cursor
  133. * is inside the container node.
  134. * NOTE: If a margin is not set, then it does not make sense to
  135. * contain the scroll, because in that case scroll will never be triggered.
  136. * @param {boolean} constrain Whether scrolling should be constrained to happen
  137. * only when the cursor is inside the container node.
  138. */
  139. goog.fx.DragScrollSupport.prototype.setConstrainScroll = function(constrain) {
  140. this.constrainScroll_ = !!this.margin_ && constrain;
  141. };
  142. /**
  143. * Sets whether horizontal scrolling is allowed.
  144. * @param {boolean} scrolling Whether horizontal scrolling is allowed.
  145. */
  146. goog.fx.DragScrollSupport.prototype.setHorizontalScrolling = function(
  147. scrolling) {
  148. this.horizontalScrolling_ = scrolling;
  149. };
  150. /**
  151. * Constrains the container bounds with respect to the margin.
  152. *
  153. * @param {goog.math.Rect} bounds The container element.
  154. * @return {goog.math.Rect} The bounding rectangle used to calculate scrolling
  155. * direction.
  156. * @private
  157. */
  158. goog.fx.DragScrollSupport.prototype.constrainBounds_ = function(bounds) {
  159. var margin = this.margin_;
  160. if (margin) {
  161. var quarterHeight = bounds.height * 0.25;
  162. var yMargin = Math.min(margin, quarterHeight);
  163. bounds.top += yMargin;
  164. bounds.height -= 2 * yMargin;
  165. var quarterWidth = bounds.width * 0.25;
  166. var xMargin = Math.min(margin, quarterWidth);
  167. bounds.left += xMargin;
  168. bounds.width -= 2 * xMargin;
  169. }
  170. return bounds;
  171. };
  172. /**
  173. * Attaches listeners and activates automatic scrolling.
  174. * @param {boolean} externalMouseMoveTracking Whether to enable internal
  175. * mouse move event handling.
  176. * @private
  177. */
  178. goog.fx.DragScrollSupport.prototype.setupListeners_ = function(
  179. externalMouseMoveTracking) {
  180. if (!externalMouseMoveTracking) {
  181. // Track mouse pointer position to determine scroll direction.
  182. this.eventHandler_.listen(
  183. goog.dom.getOwnerDocument(this.containerNode_),
  184. goog.events.EventType.MOUSEMOVE, this.onMouseMove);
  185. }
  186. // Scroll with a constant speed.
  187. this.eventHandler_.listen(this.scrollTimer_, goog.Timer.TICK, this.onTick_);
  188. };
  189. /**
  190. * Handler for timer tick event, scrolls the container by one scroll step if
  191. * needed.
  192. * @param {goog.events.Event} event Timer tick event.
  193. * @private
  194. */
  195. goog.fx.DragScrollSupport.prototype.onTick_ = function(event) {
  196. this.containerNode_.scrollTop += this.scrollDelta_.y;
  197. this.containerNode_.scrollLeft += this.scrollDelta_.x;
  198. };
  199. /**
  200. * Handler for mouse moves events.
  201. * @param {goog.events.Event} event Mouse move event.
  202. */
  203. goog.fx.DragScrollSupport.prototype.onMouseMove = function(event) {
  204. var deltaX = this.horizontalScrolling_ ?
  205. this.calculateScrollDelta(
  206. event.clientX, this.scrollBounds_.left, this.scrollBounds_.width) :
  207. 0;
  208. var deltaY = this.calculateScrollDelta(
  209. event.clientY, this.scrollBounds_.top, this.scrollBounds_.height);
  210. this.scrollDelta_.x = deltaX;
  211. this.scrollDelta_.y = deltaY;
  212. // If the scroll data is 0 or the event fired outside of the
  213. // bounds of the container node.
  214. if ((!deltaX && !deltaY) ||
  215. (this.constrainScroll_ &&
  216. !this.isInContainerBounds_(event.clientX, event.clientY))) {
  217. this.scrollTimer_.stop();
  218. } else if (!this.scrollTimer_.enabled) {
  219. this.scrollTimer_.start();
  220. }
  221. };
  222. /**
  223. * Gets whether the input coordinate is in the container bounds.
  224. * @param {number} x The x coordinate.
  225. * @param {number} y The y coordinate.
  226. * @return {boolean} Whether the input coordinate is in the container bounds.
  227. * @private
  228. */
  229. goog.fx.DragScrollSupport.prototype.isInContainerBounds_ = function(x, y) {
  230. var containerBounds = this.containerBounds_;
  231. return containerBounds.left <= x &&
  232. containerBounds.left + containerBounds.width >= x &&
  233. containerBounds.top <= y &&
  234. containerBounds.top + containerBounds.height >= y;
  235. };
  236. /**
  237. * Calculates scroll delta.
  238. *
  239. * @param {number} coordinate Current mouse pointer coordinate.
  240. * @param {number} min The coordinate value below which scrolling up should be
  241. * started.
  242. * @param {number} rangeLength The length of the range in which scrolling should
  243. * be disabled and above which scrolling down should be started.
  244. * @return {number} The calculated scroll delta.
  245. * @protected
  246. */
  247. goog.fx.DragScrollSupport.prototype.calculateScrollDelta = function(
  248. coordinate, min, rangeLength) {
  249. var delta = 0;
  250. if (coordinate < min) {
  251. delta = -goog.fx.DragScrollSupport.SCROLL_STEP_;
  252. } else if (coordinate > min + rangeLength) {
  253. delta = goog.fx.DragScrollSupport.SCROLL_STEP_;
  254. }
  255. return delta;
  256. };
  257. /** @override */
  258. goog.fx.DragScrollSupport.prototype.disposeInternal = function() {
  259. goog.fx.DragScrollSupport.superClass_.disposeInternal.call(this);
  260. this.eventHandler_.dispose();
  261. this.scrollTimer_.dispose();
  262. };