3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('node-scroll-info', function (Y, NAME) {
10 Provides the ScrollInfo Node plugin, which exposes convenient events and methods
13 @module node-scroll-info
18 Provides convenient events and methods related to scrolling. This could be used,
19 for example, to implement infinite scrolling, or to lazy-load content based on
20 the current scroll position.
24 var body = Y.one('body');
26 body.plug(Y.Plugin.ScrollInfo);
28 body.scrollInfo.on('scrollToBottom', function (e) {
29 // Load more content when the user scrolls to the bottom of the page.
32 @class Plugin.ScrollInfo
38 Fired when the user scrolls within the host node.
40 This event (like all scroll events exposed by ScrollInfo) is throttled and fired
41 only after the number of milliseconds specified by the `scrollDelay` attribute
42 have passed in order to prevent thrashing.
44 This event passes along the event facade for the standard DOM `scroll` event and
45 mixes in the following additional properties.
48 @param {Boolean} atBottom Whether the current scroll position is at the bottom
49 of the scrollable region.
50 @param {Boolean} atLeft Whether the current scroll position is at the extreme
51 left of the scrollable region.
52 @param {Boolean} atRight Whether the current scroll position is at the extreme
53 right of the scrollable region.
54 @param {Boolean} atTop Whether the current scroll position is at the top of the
56 @param {Boolean} isScrollDown `true` if the user scrolled down.
57 @param {Boolean} isScrollLeft `true` if the user scrolled left.
58 @param {Boolean} isScrollRight `true` if the user scrolled right.
59 @param {Boolean} isScrollUp `true` if the user scrolled up.
60 @param {Number} scrollBottom Y value of the bottom-most onscreen pixel of the
62 @param {Number} scrollHeight Total height in pixels of the scrollable region,
63 including offscreen pixels.
64 @param {Number} scrollLeft X value of the left-most onscreen pixel of the
66 @param {Number} scrollRight X value of the right-most onscreen pixel of the
68 @param {Number} scrollTop Y value of the top-most onscreen pixel of the
70 @param {Number} scrollWidth Total width in pixels of the scrollable region,
71 including offscreen pixels.
75 var EVT_SCROLL = 'scroll',
78 Fired when the user scrolls down within the host node.
80 This event provides the same event facade as the `scroll` event. See that
86 EVT_SCROLL_DOWN = 'scrollDown',
89 Fired when the user scrolls left within the host node.
91 This event provides the same event facade as the `scroll` event. See that
97 EVT_SCROLL_LEFT = 'scrollLeft',
100 Fired when the user scrolls right within the host node.
102 This event provides the same event facade as the `scroll` event. See that
108 EVT_SCROLL_RIGHT = 'scrollRight',
111 Fired when the user scrolls up within the host node.
113 This event provides the same event facade as the `scroll` event. See that
119 EVT_SCROLL_UP = 'scrollUp',
122 Fired when the user scrolls to the bottom of the scrollable region within
125 This event provides the same event facade as the `scroll` event. See that
128 @event scrollToBottom
131 EVT_SCROLL_TO_BOTTOM = 'scrollToBottom',
134 Fired when the user scrolls to the extreme left of the scrollable region
135 within the host node.
137 This event provides the same event facade as the `scroll` event. See that
143 EVT_SCROLL_TO_LEFT = 'scrollToLeft',
146 Fired when the user scrolls to the extreme right of the scrollable region
147 within the host node.
149 This event provides the same event facade as the `scroll` event. See that
155 EVT_SCROLL_TO_RIGHT = 'scrollToRight',
158 Fired when the user scrolls to the top of the scrollable region within the
161 This event provides the same event facade as the `scroll` event. See that
167 EVT_SCROLL_TO_TOP = 'scrollToTop';
169 Y.Plugin.ScrollInfo = Y.Base.create('scrollInfoPlugin', Y.Plugin.Base, [], {
170 // -- Lifecycle Methods ----------------------------------------------------
171 initializer: function (config) {
172 // Cache for quicker lookups in the critical path.
173 this._host = config.host;
174 this._hostIsBody = this._host.get('nodeName').toLowerCase() === 'body';
175 this._scrollDelay = this.get('scrollDelay');
176 this._scrollMargin = this.get('scrollMargin');
177 this._scrollNode = this._getScrollNode();
179 this.refreshDimensions();
181 this._lastScroll = this.getScrollInfo();
186 destructor: function () {
187 (new Y.EventHandle(this._events)).detach();
191 // -- Public Methods -------------------------------------------------------
194 Returns a NodeList containing all offscreen nodes inside the host node that
195 match the given CSS selector. An offscreen node is any node that is entirely
196 outside the visible (onscreen) region of the host node based on the current
199 @method getOffscreenNodes
200 @param {String} [selector] CSS selector. If omitted, all offscreen nodes
202 @param {Number} [margin] Additional margin in pixels beyond the actual
203 onscreen region that should be considered "onscreen" for the purposes of
204 this query. Defaults to the value of the `scrollMargin` attribute.
205 @return {NodeList} Offscreen nodes matching _selector_.
208 getOffscreenNodes: function (selector, margin) {
209 if (typeof margin === 'undefined') {
210 margin = this._scrollMargin;
213 var lastScroll = this._lastScroll,
214 nodes = this._host.all(selector || '*'),
216 scrollBottom = lastScroll.scrollBottom + margin,
217 scrollLeft = lastScroll.scrollLeft - margin,
218 scrollRight = lastScroll.scrollRight + margin,
219 scrollTop = lastScroll.scrollTop - margin,
223 return nodes.filter(function (el) {
224 var xy = Y.DOM.getXY(el),
225 elLeft = xy[0] - self._left,
226 elTop = xy[1] - self._top,
229 // Check whether the element's top left point is within the
230 // viewport. This is the least expensive check.
231 if (elLeft >= scrollLeft && elLeft < scrollRight &&
232 elTop >= scrollTop && elTop < scrollBottom) {
237 // Check whether the element's bottom right point is within the
238 // viewport. This check is more expensive since we have to get the
239 // element's height and width.
240 elBottom = elTop + el.offsetHeight;
241 elRight = elLeft + el.offsetWidth;
243 if (elRight < scrollRight && elRight >= scrollLeft &&
244 elBottom < scrollBottom && elBottom >= scrollTop) {
249 // If we get here, the element isn't within the viewport.
255 Returns a NodeList containing all onscreen nodes inside the host node that
256 match the given CSS selector. An onscreen node is any node that is fully or
257 partially within the visible (onscreen) region of the host node based on the
258 current scroll location.
260 @method getOnscreenNodes
261 @param {String} [selector] CSS selector. If omitted, all onscreen nodes will
263 @param {Number} [margin] Additional margin in pixels beyond the actual
264 onscreen region that should be considered "onscreen" for the purposes of
265 this query. Defaults to the value of the `scrollMargin` attribute.
266 @return {NodeList} Onscreen nodes matching _selector_.
269 getOnscreenNodes: function (selector, margin) {
270 if (typeof margin === 'undefined') {
271 margin = this._scrollMargin;
274 var lastScroll = this._lastScroll,
275 nodes = this._host.all(selector || '*'),
277 scrollBottom = lastScroll.scrollBottom + margin,
278 scrollLeft = lastScroll.scrollLeft - margin,
279 scrollRight = lastScroll.scrollRight + margin,
280 scrollTop = lastScroll.scrollTop - margin,
284 return nodes.filter(function (el) {
285 var xy = Y.DOM.getXY(el),
286 elLeft = xy[0] - self._left,
287 elTop = xy[1] - self._top,
290 // Check whether the element's top left point is within the
291 // viewport. This is the least expensive check.
292 if (elLeft >= scrollLeft && elLeft < scrollRight &&
293 elTop >= scrollTop && elTop < scrollBottom) {
298 // Check whether the element's bottom right point is within the
299 // viewport. This check is more expensive since we have to get the
300 // element's height and width.
301 elBottom = elTop + el.offsetHeight;
302 elRight = elLeft + el.offsetWidth;
304 if (elRight < scrollRight && elRight >= scrollLeft &&
305 elBottom < scrollBottom && elBottom >= scrollTop) {
310 // If we get here, the element isn't within the viewport.
316 Returns an object hash containing information about the current scroll
317 position of the host node. This is the same information that's mixed into
318 the event facade of the `scroll` event and other scroll-related events.
320 @method getScrollInfo
321 @return {Object} Object hash containing information about the current scroll
322 position. See the `scroll` event for details on what properties this
326 getScrollInfo: function () {
327 var domNode = this._scrollNode,
328 lastScroll = this._lastScroll,
329 margin = this._scrollMargin,
331 scrollLeft = domNode.scrollLeft,
332 scrollHeight = domNode.scrollHeight,
333 scrollTop = domNode.scrollTop,
334 scrollWidth = domNode.scrollWidth,
336 scrollBottom = scrollTop + this._height,
337 scrollRight = scrollLeft + this._width;
340 atBottom: scrollBottom > (scrollHeight - margin),
341 atLeft : scrollLeft < margin,
342 atRight : scrollRight > (scrollWidth - margin),
343 atTop : scrollTop < margin,
345 isScrollDown : lastScroll && scrollTop > lastScroll.scrollTop,
346 isScrollLeft : lastScroll && scrollLeft < lastScroll.scrollLeft,
347 isScrollRight: lastScroll && scrollLeft > lastScroll.scrollLeft,
348 isScrollUp : lastScroll && scrollTop < lastScroll.scrollTop,
350 scrollBottom: scrollBottom,
351 scrollHeight: scrollHeight,
352 scrollLeft : scrollLeft,
353 scrollRight : scrollRight,
354 scrollTop : scrollTop,
355 scrollWidth : scrollWidth
360 Refreshes cached position, height, and width dimensions for the host node.
361 If the host node is the body, then the viewport height and width will be
364 This info is cached to improve performance during scroll events, since it's
365 expensive to touch the DOM for these values. Dimensions are automatically
366 refreshed whenever the browser is resized, but if you change the dimensions
367 or position of the host node in JS, you may need to call
368 `refreshDimensions()` manually to cache the new dimensions.
370 @method refreshDimensions
372 refreshDimensions: function () {
373 // WebKit only returns reliable scroll info on the body, and only
374 // returns reliable height/width info on the documentElement, so we
375 // have to special-case it (see the other special case in
376 // _getScrollNode()).
378 // On iOS devices, documentElement.clientHeight/Width aren't reliable,
379 // but window.innerHeight/Width are. And no, dom-screen's viewport size
380 // methods don't account for this, which is why we do it here.
382 var hostIsBody = this._hostIsBody,
383 iosHack = hostIsBody && Y.UA.ios,
387 if (hostIsBody && Y.UA.webkit) {
388 el = Y.config.doc.documentElement;
390 el = this._scrollNode;
393 this._height = iosHack ? win.innerHeight : el.clientHeight;
394 this._left = el.offsetLeft;
395 this._top = el.offsetTop;
396 this._width = iosHack ? win.innerWidth : el.clientWidth;
399 // -- Protected Methods ----------------------------------------------------
402 Binds event handlers.
408 var winNode = Y.one('win');
412 scrollDelayChange : this._afterScrollDelayChange,
413 scrollMarginChange: this._afterScrollMarginChange
416 winNode.on('windowresize', this._afterResize, this),
418 // If we're attached to the body, listen for the scroll event on the
419 // window, since <body> doesn't have a scroll event.
420 (this._hostIsBody ? winNode : this._host).after(
421 'scroll', this._afterScroll, this)
426 Returns the DOM node that should be used to lookup scroll coordinates. In
427 some browsers, the `<body>` element doesn't return scroll coordinates, and
428 the documentElement must be used instead; this method takes care of
429 determining which node should be used.
431 @method _getScrollNode
432 @return {HTMLElement} DOM node.
435 _getScrollNode: function () {
436 // WebKit returns scroll coordinates on the body element, but other
437 // browsers don't, so we have to use the documentElement.
438 return this._hostIsBody && !Y.UA.webkit ? Y.config.doc.documentElement :
439 Y.Node.getDOMNode(this._host);
443 Mixes detailed scroll information into the given DOM `scroll` event facade
444 and fires appropriate local events.
446 @method _triggerScroll
447 @param {EventFacade} e Event facade from the DOM `scroll` event.
450 _triggerScroll: function (e) {
451 var info = this.getScrollInfo(),
452 facade = Y.merge(e, info),
453 lastScroll = this._lastScroll;
455 this._lastScroll = info;
457 this.fire(EVT_SCROLL, facade);
459 if (info.isScrollLeft) {
460 this.fire(EVT_SCROLL_LEFT, facade);
461 } else if (info.isScrollRight) {
462 this.fire(EVT_SCROLL_RIGHT, facade);
465 if (info.isScrollUp) {
466 this.fire(EVT_SCROLL_UP, facade);
467 } else if (info.isScrollDown) {
468 this.fire(EVT_SCROLL_DOWN, facade);
471 if (info.atBottom && (!lastScroll.atBottom ||
472 info.scrollHeight > lastScroll.scrollHeight)) {
474 this.fire(EVT_SCROLL_TO_BOTTOM, facade);
477 if (info.atLeft && !lastScroll.atLeft) {
478 this.fire(EVT_SCROLL_TO_LEFT, facade);
481 if (info.atRight && (!lastScroll.atRight ||
482 info.scrollWidth > lastScroll.scrollWidth)) {
484 this.fire(EVT_SCROLL_TO_RIGHT, facade);
487 if (info.atTop && !lastScroll.atTop) {
488 this.fire(EVT_SCROLL_TO_TOP, facade);
492 // -- Protected Event Handlers ---------------------------------------------
495 Handles browser resize events.
498 @param {EventFacade} e
501 _afterResize: function (e) {
502 this.refreshDimensions();
506 Handles DOM `scroll` events.
509 @param {EventFacade} e
512 _afterScroll: function (e) {
515 clearTimeout(this._scrollTimeout);
517 this._scrollTimeout = setTimeout(function () {
518 self._triggerScroll(e);
519 }, this._scrollDelay);
523 Caches the `scrollDelay` value after that attribute changes to allow
524 quicker lookups in critical path code.
526 @method _afterScrollDelayChange
527 @param {EventFacade} e
530 _afterScrollDelayChange: function (e) {
531 this._scrollDelay = e.newVal;
535 Caches the `scrollMargin` value after that attribute changes to allow
536 quicker lookups in critical path code.
538 @method _afterScrollMarginChange
539 @param {EventFacade} e
542 _afterScrollMarginChange: function (e) {
543 this._scrollMargin = e.newVal;
550 Number of milliseconds to wait after a native `scroll` event before
551 firing local scroll events. If another native scroll event occurs during
552 this time, previous events will be ignored. This ensures that we don't
553 fire thousands of events when the user is scrolling quickly.
555 @attribute scrollDelay
564 Additional margin in pixels beyond the onscreen region of the host node
565 that should be considered "onscreen".
567 For example, if set to 50, then a `scrollToBottom` event would be fired
568 when the user scrolls to within 50 pixels of the bottom of the
569 scrollable region, even if they don't actually scroll completely to the
572 This margin also applies to the `getOffscreenNodes()` and
573 `getOnscreenNodes()` methods by default.
575 @attribute scrollMargin
586 }, '3.7.1', {"requires": ["base-build", "dom-screen", "event-resize", "node-pluginhost", "plugin"]});