2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('node-scroll-info', function (Y, NAME) {
10 /*jshint onevar:false */
13 Provides the ScrollInfo Node plugin, which exposes convenient events and methods
16 @module node-scroll-info
21 Provides convenient events and methods related to scrolling. This could be used,
22 for example, to implement infinite scrolling, or to lazy-load content based on
23 the current scroll position.
27 var body = Y.one('body');
29 body.plug(Y.Plugin.ScrollInfo);
31 body.scrollInfo.on('scrollToBottom', function (e) {
32 // Load more content when the user scrolls to the bottom of the page.
35 @class Plugin.ScrollInfo
40 var doc = Y.config.doc,
44 Fired when the user scrolls within the host node.
46 This event (like all scroll events exposed by ScrollInfo) is throttled and fired
47 only after the number of milliseconds specified by the `scrollDelay` attribute
48 have passed in order to prevent thrashing.
50 This event passes along the event facade for the standard DOM `scroll` event and
51 mixes in the following additional properties.
54 @param {Boolean} atBottom Whether the current scroll position is at the bottom
55 of the scrollable region.
56 @param {Boolean} atLeft Whether the current scroll position is at the extreme
57 left of the scrollable region.
58 @param {Boolean} atRight Whether the current scroll position is at the extreme
59 right of the scrollable region.
60 @param {Boolean} atTop Whether the current scroll position is at the top of the
62 @param {Boolean} isScrollDown `true` if the user scrolled down.
63 @param {Boolean} isScrollLeft `true` if the user scrolled left.
64 @param {Boolean} isScrollRight `true` if the user scrolled right.
65 @param {Boolean} isScrollUp `true` if the user scrolled up.
66 @param {Number} scrollBottom Y value of the bottom-most onscreen pixel of the
68 @param {Number} scrollHeight Total height in pixels of the scrollable region,
69 including offscreen pixels.
70 @param {Number} scrollLeft X value of the left-most onscreen pixel of the
72 @param {Number} scrollRight X value of the right-most onscreen pixel of the
74 @param {Number} scrollTop Y value of the top-most onscreen pixel of the
76 @param {Number} scrollWidth Total width in pixels of the scrollable region,
77 including offscreen pixels.
81 var EVT_SCROLL = 'scroll',
84 Fired when the user scrolls down within the host node.
86 This event provides the same event facade as the `scroll` event. See that
92 EVT_SCROLL_DOWN = 'scrollDown',
95 Fired when the user scrolls left within the host node.
97 This event provides the same event facade as the `scroll` event. See that
103 EVT_SCROLL_LEFT = 'scrollLeft',
106 Fired when the user scrolls right within the host node.
108 This event provides the same event facade as the `scroll` event. See that
114 EVT_SCROLL_RIGHT = 'scrollRight',
117 Fired when the user scrolls up within the host node.
119 This event provides the same event facade as the `scroll` event. See that
125 EVT_SCROLL_UP = 'scrollUp',
128 Fired when the user scrolls to the bottom of the scrollable region within
131 This event provides the same event facade as the `scroll` event. See that
134 @event scrollToBottom
137 EVT_SCROLL_TO_BOTTOM = 'scrollToBottom',
140 Fired when the user scrolls to the extreme left of the scrollable region
141 within the host node.
143 This event provides the same event facade as the `scroll` event. See that
149 EVT_SCROLL_TO_LEFT = 'scrollToLeft',
152 Fired when the user scrolls to the extreme right of the scrollable region
153 within the host node.
155 This event provides the same event facade as the `scroll` event. See that
161 EVT_SCROLL_TO_RIGHT = 'scrollToRight',
164 Fired when the user scrolls to the top of the scrollable region within the
167 This event provides the same event facade as the `scroll` event. See that
173 EVT_SCROLL_TO_TOP = 'scrollToTop';
175 Y.Plugin.ScrollInfo = Y.Base.create('scrollInfoPlugin', Y.Plugin.Base, [], {
176 // -- Protected Properties -------------------------------------------------
179 Height of the visible region of the host node in pixels. If the host node is
180 the body, this will be the same as `_winHeight`.
182 @property {Number} _height
187 Whether or not the host node is the `<body>` element.
189 @property {Boolean} _hostIsBody
194 Width of the visible region of the host node in pixels. If the host node is
195 the body, this will be the same as `_winWidth`.
197 @property {Number} _width
202 Height of the viewport in pixels.
204 @property {Number} _winHeight
209 Width of the viewport in pixels.
211 @property {Number} _winWidth
215 // -- Lifecycle Methods ----------------------------------------------------
216 initializer: function (config) {
217 // Cache for quicker lookups in the critical path.
218 this._host = config.host;
219 this._hostIsBody = this._host.get('nodeName').toLowerCase() === 'body';
220 this._scrollDelay = this.get('scrollDelay');
221 this._scrollMargin = this.get('scrollMargin');
222 this._scrollNode = this._getScrollNode();
224 this.refreshDimensions();
226 this._lastScroll = this.getScrollInfo();
231 destructor: function () {
232 new Y.EventHandle(this._events).detach();
236 // -- Public Methods -------------------------------------------------------
239 Returns a NodeList containing all offscreen nodes inside the host node that
240 match the given CSS selector. An offscreen node is any node that is entirely
241 outside the visible (onscreen) region of the host node based on the current
244 @method getOffscreenNodes
245 @param {String} [selector] CSS selector. If omitted, all offscreen nodes
247 @param {Number} [margin] Additional margin in pixels beyond the actual
248 onscreen region that should be considered "onscreen" for the purposes of
249 this query. Defaults to the value of the `scrollMargin` attribute.
250 @return {NodeList} Offscreen nodes matching _selector_.
253 getOffscreenNodes: function (selector, margin) {
254 if (typeof margin === 'undefined') {
255 margin = this._scrollMargin;
258 var elements = Y.Selector.query(selector || '*', this._host._node);
260 return new Y.NodeList(Y.Array.filter(elements, function (el) {
261 return !this._isElementOnscreen(el, margin);
266 Returns a NodeList containing all onscreen nodes inside the host node that
267 match the given CSS selector. An onscreen node is any node that is fully or
268 partially within the visible (onscreen) region of the host node based on the
269 current scroll location.
271 @method getOnscreenNodes
272 @param {String} [selector] CSS selector. If omitted, all onscreen nodes will
274 @param {Number} [margin] Additional margin in pixels beyond the actual
275 onscreen region that should be considered "onscreen" for the purposes of
276 this query. Defaults to the value of the `scrollMargin` attribute.
277 @return {NodeList} Onscreen nodes matching _selector_.
280 getOnscreenNodes: function (selector, margin) {
281 if (typeof margin === 'undefined') {
282 margin = this._scrollMargin;
285 var elements = Y.Selector.query(selector || '*', this._host._node);
287 return new Y.NodeList(Y.Array.filter(elements, function (el) {
288 return this._isElementOnscreen(el, margin);
293 Returns an object hash containing information about the current scroll
294 position of the host node. This is the same information that's mixed into
295 the event facade of the `scroll` event and other scroll-related events.
297 @method getScrollInfo
298 @return {Object} Object hash containing information about the current scroll
299 position. See the `scroll` event for details on what properties this
303 getScrollInfo: function () {
304 var domNode = this._scrollNode,
305 lastScroll = this._lastScroll,
306 margin = this._scrollMargin,
308 scrollLeft = domNode.scrollLeft,
309 scrollHeight = domNode.scrollHeight,
310 scrollTop = domNode.scrollTop,
311 scrollWidth = domNode.scrollWidth,
313 scrollBottom = scrollTop + this._height,
314 scrollRight = scrollLeft + this._width;
317 atBottom: scrollBottom > (scrollHeight - margin),
318 atLeft : scrollLeft < margin,
319 atRight : scrollRight > (scrollWidth - margin),
320 atTop : scrollTop < margin,
322 isScrollDown : lastScroll && scrollTop > lastScroll.scrollTop,
323 isScrollLeft : lastScroll && scrollLeft < lastScroll.scrollLeft,
324 isScrollRight: lastScroll && scrollLeft > lastScroll.scrollLeft,
325 isScrollUp : lastScroll && scrollTop < lastScroll.scrollTop,
327 scrollBottom: scrollBottom,
328 scrollHeight: scrollHeight,
329 scrollLeft : scrollLeft,
330 scrollRight : scrollRight,
331 scrollTop : scrollTop,
332 scrollWidth : scrollWidth
337 Returns `true` if _node_ is at least partially onscreen within the host
338 node, `false` otherwise.
340 @method isNodeOnscreen
341 @param {HTMLElement|Node|String} node Node or selector to check.
342 @param {Number} [margin] Additional margin in pixels beyond the actual
343 onscreen region that should be considered "onscreen" for the purposes of
344 this query. Defaults to the value of the `scrollMargin` attribute.
345 @return {Boolean} `true` if _node_ is at least partially onscreen within the
346 host node, `false` otherwise.
349 isNodeOnscreen: function (node, margin) {
351 return !!(node && this._isElementOnscreen(node._node, margin));
355 Refreshes cached position, height, and width dimensions for the host node.
356 If the host node is the body, then the viewport height and width will be
359 This info is cached to improve performance during scroll events, since it's
360 expensive to touch the DOM for these values. Dimensions are automatically
361 refreshed whenever the browser is resized, but if you change the dimensions
362 or position of the host node in JS, you may need to call
363 `refreshDimensions()` manually to cache the new dimensions.
365 @method refreshDimensions
367 refreshDimensions: function () {
368 var docEl = doc.documentElement;
370 // On iOS devices, documentElement.clientHeight/Width aren't reliable,
371 // but window.innerHeight/Width are. The dom-screen module's viewport
372 // size methods don't account for this, which is why we do it here.
374 this._winHeight = win.innerHeight;
375 this._winWidth = win.innerWidth;
377 this._winHeight = docEl.clientHeight;
378 this._winWidth = docEl.clientWidth;
381 if (this._hostIsBody) {
382 this._height = this._winHeight;
383 this._width = this._winWidth;
385 this._height = this._scrollNode.clientHeight;
386 this._width = this._scrollNode.clientWidth;
389 this._refreshHostBoundingRect();
392 // -- Protected Methods ----------------------------------------------------
395 Binds event handlers.
401 var winNode = Y.one('win');
405 scrollDelayChange : this._afterScrollDelayChange,
406 scrollMarginChange: this._afterScrollMarginChange
409 winNode.on('windowresize', this._afterResize, this)
412 // If the host node is the body, listen for the scroll event on the
413 // window, since <body> doesn't have a scroll event.
414 if (this._hostIsBody) {
415 this._events.push(winNode.after('scroll', this._afterHostScroll, this));
417 // The host node is not the body, but we still need to listen for
418 // window scroll events so we can determine whether nodes are
421 winNode.after('scroll', this._afterWindowScroll, this),
422 this._host.after('scroll', this._afterHostScroll, this)
428 Returns the DOM node that should be used to lookup scroll coordinates. In
429 some browsers, the `<body>` element doesn't return scroll coordinates, and
430 the documentElement must be used instead; this method takes care of
431 determining which node should be used.
433 @method _getScrollNode
434 @return {HTMLElement} DOM node.
437 _getScrollNode: function () {
438 // WebKit returns scroll coordinates on the body element, but other
439 // browsers don't, so we have to use the documentElement.
440 return this._hostIsBody && !Y.UA.webkit ? doc.documentElement :
441 Y.Node.getDOMNode(this._host);
445 Underlying element-based implementation for `isNodeOnscreen()`.
447 @method _isElementOnscreen
448 @param {HTMLElement} el HTML element.
449 @param {Number} [margin] Additional margin in pixels beyond the actual
450 onscreen region that should be considered "onscreen" for the purposes of
451 this query. Defaults to the value of the `scrollMargin` attribute.
452 @return {Boolean} `true` if _el_ is at least partially onscreen within the
453 host node, `false` otherwise.
457 _isElementOnscreen: function (el, margin) {
458 var hostRect = this._hostRect,
459 rect = el.getBoundingClientRect();
461 if (typeof margin === 'undefined') {
462 margin = this._scrollMargin;
465 // Determine whether any part of _el_ is within the visible region of
466 // the host element or the specified margin around the visible region of
468 return !(rect.top > hostRect.bottom + margin
469 || rect.bottom < hostRect.top - margin
470 || rect.right < hostRect.left - margin
471 || rect.left > hostRect.right + margin);
475 Caches the bounding rect of the host node.
477 If the host node is the body, the bounding rect will be faked to represent
478 the dimensions of the viewport, since the actual body dimensions may extend
479 beyond the viewport and we only care about the visible region.
481 @method _refreshHostBoundingRect
484 _refreshHostBoundingRect: function () {
485 var winHeight = this._winHeight,
486 winWidth = this._winWidth,
490 if (this._hostIsBody) {
500 this._isHostOnscreen = true;
502 hostRect = this._scrollNode.getBoundingClientRect();
505 this._hostRect = hostRect;
509 Mixes detailed scroll information into the given DOM `scroll` event facade
510 and fires appropriate local events.
512 @method _triggerScroll
513 @param {EventFacade} e Event facade from the DOM `scroll` event.
516 _triggerScroll: function (e) {
517 var info = this.getScrollInfo(),
518 facade = Y.merge(e, info),
519 lastScroll = this._lastScroll;
521 this._lastScroll = info;
523 this.fire(EVT_SCROLL, facade);
525 if (info.isScrollLeft) {
526 this.fire(EVT_SCROLL_LEFT, facade);
527 } else if (info.isScrollRight) {
528 this.fire(EVT_SCROLL_RIGHT, facade);
531 if (info.isScrollUp) {
532 this.fire(EVT_SCROLL_UP, facade);
533 } else if (info.isScrollDown) {
534 this.fire(EVT_SCROLL_DOWN, facade);
537 if (info.atBottom && (!lastScroll.atBottom ||
538 info.scrollHeight > lastScroll.scrollHeight)) {
540 this.fire(EVT_SCROLL_TO_BOTTOM, facade);
543 if (info.atLeft && !lastScroll.atLeft) {
544 this.fire(EVT_SCROLL_TO_LEFT, facade);
547 if (info.atRight && (!lastScroll.atRight ||
548 info.scrollWidth > lastScroll.scrollWidth)) {
550 this.fire(EVT_SCROLL_TO_RIGHT, facade);
553 if (info.atTop && !lastScroll.atTop) {
554 this.fire(EVT_SCROLL_TO_TOP, facade);
558 // -- Protected Event Handlers ---------------------------------------------
561 Handles DOM `scroll` events on the host node.
563 @method _afterHostScroll
564 @param {EventFacade} e
567 _afterHostScroll: function (e) {
570 clearTimeout(this._scrollTimeout);
572 this._scrollTimeout = setTimeout(function () {
573 self._triggerScroll(e);
574 }, this._scrollDelay);
578 Handles browser resize events.
583 _afterResize: function () {
584 this.refreshDimensions();
588 Caches the `scrollDelay` value after that attribute changes to allow
589 quicker lookups in critical path code.
591 @method _afterScrollDelayChange
592 @param {EventFacade} e
595 _afterScrollDelayChange: function (e) {
596 this._scrollDelay = e.newVal;
600 Caches the `scrollMargin` value after that attribute changes to allow
601 quicker lookups in critical path code.
603 @method _afterScrollMarginChange
604 @param {EventFacade} e
607 _afterScrollMarginChange: function (e) {
608 this._scrollMargin = e.newVal;
612 Handles DOM `scroll` events on the window.
614 @method _afterWindowScroll
615 @param {EventFacade} e
618 _afterWindowScroll: function () {
619 this._refreshHostBoundingRect();
626 Number of milliseconds to wait after a native `scroll` event before
627 firing local scroll events. If another native scroll event occurs during
628 this time, previous events will be ignored. This ensures that we don't
629 fire thousands of events when the user is scrolling quickly.
631 @attribute scrollDelay
640 Additional margin in pixels beyond the onscreen region of the host node
641 that should be considered "onscreen".
643 For example, if set to 50, then a `scrollToBottom` event would be fired
644 when the user scrolls to within 50 pixels of the bottom of the
645 scrollable region, even if they don't actually scroll completely to the
648 This margin also applies to the `getOffscreenNodes()` and
649 `getOnscreenNodes()` methods by default.
651 @attribute scrollMargin
662 }, '3.13.0', {"requires": ["array-extras", "base-build", "event-resize", "node-pluginhost", "plugin", "selector"]});