API Docs for: 3.8.0
Show:

File: node-scroll-info/js/node-scroll-info.js

  1. /**
  2. Provides the ScrollInfo Node plugin, which exposes convenient events and methods
  3. related to scrolling.

  4. @module node-scroll-info
  5. @since 3.7.0
  6. **/

  7. /**
  8. Provides convenient events and methods related to scrolling. This could be used,
  9. for example, to implement infinite scrolling, or to lazy-load content based on
  10. the current scroll position.

  11. ### Example

  12.     var body = Y.one('body');

  13.     body.plug(Y.Plugin.ScrollInfo);

  14.     body.scrollInfo.on('scrollToBottom', function (e) {
  15.         // Load more content when the user scrolls to the bottom of the page.
  16.     });

  17. @class Plugin.ScrollInfo
  18. @extends Plugin.Base
  19. @since 3.7.0
  20. **/

  21. /**
  22. Fired when the user scrolls within the host node.

  23. This event (like all scroll events exposed by ScrollInfo) is throttled and fired
  24. only after the number of milliseconds specified by the `scrollDelay` attribute
  25. have passed in order to prevent thrashing.

  26. This event passes along the event facade for the standard DOM `scroll` event and
  27. mixes in the following additional properties.

  28. @event scroll
  29. @param {Boolean} atBottom Whether the current scroll position is at the bottom
  30.     of the scrollable region.
  31. @param {Boolean} atLeft Whether the current scroll position is at the extreme
  32.     left of the scrollable region.
  33. @param {Boolean} atRight Whether the current scroll position is at the extreme
  34.     right of the scrollable region.
  35. @param {Boolean} atTop Whether the current scroll position is at the top of the
  36.     scrollable region.
  37. @param {Boolean} isScrollDown `true` if the user scrolled down.
  38. @param {Boolean} isScrollLeft `true` if the user scrolled left.
  39. @param {Boolean} isScrollRight `true` if the user scrolled right.
  40. @param {Boolean} isScrollUp `true` if the user scrolled up.
  41. @param {Number} scrollBottom Y value of the bottom-most onscreen pixel of the
  42.     scrollable region.
  43. @param {Number} scrollHeight Total height in pixels of the scrollable region,
  44.     including offscreen pixels.
  45. @param {Number} scrollLeft X value of the left-most onscreen pixel of the
  46.     scrollable region.
  47. @param {Number} scrollRight X value of the right-most onscreen pixel of the
  48.     scrollable region.
  49. @param {Number} scrollTop Y value of the top-most onscreen pixel of the
  50.     scrollable region.
  51. @param {Number} scrollWidth Total width in pixels of the scrollable region,
  52.     including offscreen pixels.
  53. @see scrollDelay
  54. @see scrollMargin
  55. **/
  56. var EVT_SCROLL = 'scroll',

  57.     /**
  58.     Fired when the user scrolls down within the host node.

  59.     This event provides the same event facade as the `scroll` event. See that
  60.     event for details.

  61.     @event scrollDown
  62.     @see scroll
  63.     **/
  64.     EVT_SCROLL_DOWN = 'scrollDown',

  65.     /**
  66.     Fired when the user scrolls left within the host node.

  67.     This event provides the same event facade as the `scroll` event. See that
  68.     event for details.

  69.     @event scrollLeft
  70.     @see scroll
  71.     **/
  72.     EVT_SCROLL_LEFT = 'scrollLeft',

  73.     /**
  74.     Fired when the user scrolls right within the host node.

  75.     This event provides the same event facade as the `scroll` event. See that
  76.     event for details.

  77.     @event scrollRight
  78.     @see scroll
  79.     **/
  80.     EVT_SCROLL_RIGHT = 'scrollRight',

  81.     /**
  82.     Fired when the user scrolls up within the host node.

  83.     This event provides the same event facade as the `scroll` event. See that
  84.     event for details.

  85.     @event scrollUp
  86.     @see scroll
  87.     **/
  88.     EVT_SCROLL_UP = 'scrollUp',

  89.     /**
  90.     Fired when the user scrolls to the bottom of the scrollable region within
  91.     the host node.

  92.     This event provides the same event facade as the `scroll` event. See that
  93.     event for details.

  94.     @event scrollToBottom
  95.     @see scroll
  96.     **/
  97.     EVT_SCROLL_TO_BOTTOM = 'scrollToBottom',

  98.     /**
  99.     Fired when the user scrolls to the extreme left of the scrollable region
  100.     within the host node.

  101.     This event provides the same event facade as the `scroll` event. See that
  102.     event for details.

  103.     @event scrollToLeft
  104.     @see scroll
  105.     **/
  106.     EVT_SCROLL_TO_LEFT = 'scrollToLeft',

  107.     /**
  108.     Fired when the user scrolls to the extreme right of the scrollable region
  109.     within the host node.

  110.     This event provides the same event facade as the `scroll` event. See that
  111.     event for details.

  112.     @event scrollToRight
  113.     @see scroll
  114.     **/
  115.     EVT_SCROLL_TO_RIGHT = 'scrollToRight',

  116.     /**
  117.     Fired when the user scrolls to the top of the scrollable region within the
  118.     host node.

  119.     This event provides the same event facade as the `scroll` event. See that
  120.     event for details.

  121.     @event scrollToTop
  122.     @see scroll
  123.     **/
  124.     EVT_SCROLL_TO_TOP = 'scrollToTop';

  125. Y.Plugin.ScrollInfo = Y.Base.create('scrollInfoPlugin', Y.Plugin.Base, [], {
  126.     // -- Lifecycle Methods ----------------------------------------------------
  127.     initializer: function (config) {
  128.         // Cache for quicker lookups in the critical path.
  129.         this._host         = config.host;
  130.         this._hostIsBody   = this._host.get('nodeName').toLowerCase() === 'body';
  131.         this._scrollDelay  = this.get('scrollDelay');
  132.         this._scrollMargin = this.get('scrollMargin');
  133.         this._scrollNode   = this._getScrollNode();

  134.         this.refreshDimensions();

  135.         this._lastScroll = this.getScrollInfo();

  136.         this._bind();
  137.     },

  138.     destructor: function () {
  139.         (new Y.EventHandle(this._events)).detach();
  140.         delete this._events;
  141.     },

  142.     // -- Public Methods -------------------------------------------------------

  143.     /**
  144.     Returns a NodeList containing all offscreen nodes inside the host node that
  145.     match the given CSS selector. An offscreen node is any node that is entirely
  146.     outside the visible (onscreen) region of the host node based on the current
  147.     scroll location.

  148.     @method getOffscreenNodes
  149.     @param {String} [selector] CSS selector. If omitted, all offscreen nodes
  150.         will be returned.
  151.     @param {Number} [margin] Additional margin in pixels beyond the actual
  152.         onscreen region that should be considered "onscreen" for the purposes of
  153.         this query. Defaults to the value of the `scrollMargin` attribute.
  154.     @return {NodeList} Offscreen nodes matching _selector_.
  155.     @see scrollMargin
  156.     **/
  157.     getOffscreenNodes: function (selector, margin) {
  158.         if (typeof margin === 'undefined') {
  159.             margin = this._scrollMargin;
  160.         }

  161.         var lastScroll = this._lastScroll,
  162.             nodes      = this._host.all(selector || '*'),

  163.             scrollBottom = lastScroll.scrollBottom + margin,
  164.             scrollLeft   = lastScroll.scrollLeft - margin,
  165.             scrollRight  = lastScroll.scrollRight + margin,
  166.             scrollTop    = lastScroll.scrollTop - margin,

  167.             self = this;

  168.         return nodes.filter(function (el) {
  169.             var xy     = Y.DOM.getXY(el),
  170.                 elLeft = xy[0] - self._left,
  171.                 elTop  = xy[1] - self._top,
  172.                 elBottom, elRight;

  173.             // Check whether the element's top left point is within the
  174.             // viewport. This is the least expensive check.
  175.             if (elLeft >= scrollLeft && elLeft < scrollRight &&
  176.                     elTop >= scrollTop && elTop < scrollBottom) {

  177.                 return false;
  178.             }

  179.             // Check whether the element's bottom right point is within the
  180.             // viewport. This check is more expensive since we have to get the
  181.             // element's height and width.
  182.             elBottom = elTop + el.offsetHeight;
  183.             elRight  = elLeft + el.offsetWidth;

  184.             if (elRight < scrollRight && elRight >= scrollLeft &&
  185.                     elBottom < scrollBottom && elBottom >= scrollTop) {

  186.                 return false;
  187.             }

  188.             // If we get here, the element isn't within the viewport.
  189.             return true;
  190.         });
  191.     },

  192.     /**
  193.     Returns a NodeList containing all onscreen nodes inside the host node that
  194.     match the given CSS selector. An onscreen node is any node that is fully or
  195.     partially within the visible (onscreen) region of the host node based on the
  196.     current scroll location.

  197.     @method getOnscreenNodes
  198.     @param {String} [selector] CSS selector. If omitted, all onscreen nodes will
  199.         be returned.
  200.     @param {Number} [margin] Additional margin in pixels beyond the actual
  201.         onscreen region that should be considered "onscreen" for the purposes of
  202.         this query. Defaults to the value of the `scrollMargin` attribute.
  203.     @return {NodeList} Onscreen nodes matching _selector_.
  204.     @see scrollMargin
  205.     **/
  206.     getOnscreenNodes: function (selector, margin) {
  207.         if (typeof margin === 'undefined') {
  208.             margin = this._scrollMargin;
  209.         }

  210.         var lastScroll = this._lastScroll,
  211.             nodes      = this._host.all(selector || '*'),

  212.             scrollBottom = lastScroll.scrollBottom + margin,
  213.             scrollLeft   = lastScroll.scrollLeft - margin,
  214.             scrollRight  = lastScroll.scrollRight + margin,
  215.             scrollTop    = lastScroll.scrollTop - margin,

  216.             self = this;

  217.         return nodes.filter(function (el) {
  218.             var xy     = Y.DOM.getXY(el),
  219.                 elLeft = xy[0] - self._left,
  220.                 elTop  = xy[1] - self._top,
  221.                 elBottom, elRight;

  222.             // Check whether the element's top left point is within the
  223.             // viewport. This is the least expensive check.
  224.             if (elLeft >= scrollLeft && elLeft < scrollRight &&
  225.                     elTop >= scrollTop && elTop < scrollBottom) {

  226.                 return true;
  227.             }

  228.             // Check whether the element's bottom right point is within the
  229.             // viewport. This check is more expensive since we have to get the
  230.             // element's height and width.
  231.             elBottom = elTop + el.offsetHeight;
  232.             elRight  = elLeft + el.offsetWidth;

  233.             if (elRight < scrollRight && elRight >= scrollLeft &&
  234.                     elBottom < scrollBottom && elBottom >= scrollTop) {

  235.                 return true;
  236.             }

  237.             // If we get here, the element isn't within the viewport.
  238.             return false;
  239.         });
  240.     },

  241.     /**
  242.     Returns an object hash containing information about the current scroll
  243.     position of the host node. This is the same information that's mixed into
  244.     the event facade of the `scroll` event and other scroll-related events.

  245.     @method getScrollInfo
  246.     @return {Object} Object hash containing information about the current scroll
  247.         position. See the `scroll` event for details on what properties this
  248.         object contains.
  249.     @see scroll
  250.     **/
  251.     getScrollInfo: function () {
  252.         var domNode    = this._scrollNode,
  253.             lastScroll = this._lastScroll,
  254.             margin     = this._scrollMargin,

  255.             scrollLeft   = domNode.scrollLeft,
  256.             scrollHeight = domNode.scrollHeight,
  257.             scrollTop    = domNode.scrollTop,
  258.             scrollWidth  = domNode.scrollWidth,

  259.             scrollBottom = scrollTop + this._height,
  260.             scrollRight  = scrollLeft + this._width;

  261.         return {
  262.             atBottom: scrollBottom > (scrollHeight - margin),
  263.             atLeft  : scrollLeft < margin,
  264.             atRight : scrollRight > (scrollWidth - margin),
  265.             atTop   : scrollTop < margin,

  266.             isScrollDown : lastScroll && scrollTop > lastScroll.scrollTop,
  267.             isScrollLeft : lastScroll && scrollLeft < lastScroll.scrollLeft,
  268.             isScrollRight: lastScroll && scrollLeft > lastScroll.scrollLeft,
  269.             isScrollUp   : lastScroll && scrollTop < lastScroll.scrollTop,

  270.             scrollBottom: scrollBottom,
  271.             scrollHeight: scrollHeight,
  272.             scrollLeft  : scrollLeft,
  273.             scrollRight : scrollRight,
  274.             scrollTop   : scrollTop,
  275.             scrollWidth : scrollWidth
  276.         };
  277.     },

  278.     /**
  279.     Refreshes cached position, height, and width dimensions for the host node.
  280.     If the host node is the body, then the viewport height and width will be
  281.     used.

  282.     This info is cached to improve performance during scroll events, since it's
  283.     expensive to touch the DOM for these values. Dimensions are automatically
  284.     refreshed whenever the browser is resized, but if you change the dimensions
  285.     or position of the host node in JS, you may need to call
  286.     `refreshDimensions()` manually to cache the new dimensions.

  287.     @method refreshDimensions
  288.     **/
  289.     refreshDimensions: function () {
  290.         // WebKit only returns reliable scroll info on the body, and only
  291.         // returns reliable height/width info on the documentElement, so we
  292.         // have to special-case it (see the other special case in
  293.         // _getScrollNode()).
  294.         //
  295.         // On iOS devices, documentElement.clientHeight/Width aren't reliable,
  296.         // but window.innerHeight/Width are. And no, dom-screen's viewport size
  297.         // methods don't account for this, which is why we do it here.

  298.         var hostIsBody = this._hostIsBody,
  299.             iosHack    = hostIsBody && Y.UA.ios,
  300.             win        = Y.config.win,
  301.             el;

  302.         if (hostIsBody && Y.UA.webkit) {
  303.             el = Y.config.doc.documentElement;
  304.         } else {
  305.             el = this._scrollNode;
  306.         }

  307.         this._height = iosHack ? win.innerHeight : el.clientHeight;
  308.         this._left   = el.offsetLeft;
  309.         this._top    = el.offsetTop;
  310.         this._width  = iosHack ? win.innerWidth : el.clientWidth;
  311.     },

  312.     // -- Protected Methods ----------------------------------------------------

  313.     /**
  314.     Binds event handlers.

  315.     @method _bind
  316.     @protected
  317.     **/
  318.     _bind: function () {
  319.         var winNode = Y.one('win');

  320.         this._events = [
  321.             this.after({
  322.                 scrollDelayChange : this._afterScrollDelayChange,
  323.                 scrollMarginChange: this._afterScrollMarginChange
  324.             }),

  325.             winNode.on('windowresize', this._afterResize, this),

  326.             // If we're attached to the body, listen for the scroll event on the
  327.             // window, since <body> doesn't have a scroll event.
  328.             (this._hostIsBody ? winNode : this._host).after(
  329.                 'scroll', this._afterScroll, this)
  330.         ];
  331.     },

  332.     /**
  333.     Returns the DOM node that should be used to lookup scroll coordinates. In
  334.     some browsers, the `<body>` element doesn't return scroll coordinates, and
  335.     the documentElement must be used instead; this method takes care of
  336.     determining which node should be used.

  337.     @method _getScrollNode
  338.     @return {HTMLElement} DOM node.
  339.     @protected
  340.     **/
  341.     _getScrollNode: function () {
  342.         // WebKit returns scroll coordinates on the body element, but other
  343.         // browsers don't, so we have to use the documentElement.
  344.         return this._hostIsBody && !Y.UA.webkit ? Y.config.doc.documentElement :
  345.                 Y.Node.getDOMNode(this._host);
  346.     },

  347.     /**
  348.     Mixes detailed scroll information into the given DOM `scroll` event facade
  349.     and fires appropriate local events.

  350.     @method _triggerScroll
  351.     @param {EventFacade} e Event facade from the DOM `scroll` event.
  352.     @protected
  353.     **/
  354.     _triggerScroll: function (e) {
  355.         var info       = this.getScrollInfo(),
  356.             facade     = Y.merge(e, info),
  357.             lastScroll = this._lastScroll;

  358.         this._lastScroll = info;

  359.         this.fire(EVT_SCROLL, facade);

  360.         if (info.isScrollLeft) {
  361.             this.fire(EVT_SCROLL_LEFT, facade);
  362.         } else if (info.isScrollRight) {
  363.             this.fire(EVT_SCROLL_RIGHT, facade);
  364.         }

  365.         if (info.isScrollUp) {
  366.             this.fire(EVT_SCROLL_UP, facade);
  367.         } else if (info.isScrollDown) {
  368.             this.fire(EVT_SCROLL_DOWN, facade);
  369.         }

  370.         if (info.atBottom && (!lastScroll.atBottom ||
  371.                 info.scrollHeight > lastScroll.scrollHeight)) {

  372.             this.fire(EVT_SCROLL_TO_BOTTOM, facade);
  373.         }

  374.         if (info.atLeft && !lastScroll.atLeft) {
  375.             this.fire(EVT_SCROLL_TO_LEFT, facade);
  376.         }

  377.         if (info.atRight && (!lastScroll.atRight ||
  378.                 info.scrollWidth > lastScroll.scrollWidth)) {

  379.             this.fire(EVT_SCROLL_TO_RIGHT, facade);
  380.         }

  381.         if (info.atTop && !lastScroll.atTop) {
  382.             this.fire(EVT_SCROLL_TO_TOP, facade);
  383.         }
  384.     },

  385.     // -- Protected Event Handlers ---------------------------------------------

  386.     /**
  387.     Handles browser resize events.

  388.     @method _afterResize
  389.     @param {EventFacade} e
  390.     @protected
  391.     **/
  392.     _afterResize: function (e) {
  393.         this.refreshDimensions();
  394.     },

  395.     /**
  396.     Handles DOM `scroll` events.

  397.     @method _afterScroll
  398.     @param {EventFacade} e
  399.     @protected
  400.     **/
  401.     _afterScroll: function (e) {
  402.         var self = this;

  403.         clearTimeout(this._scrollTimeout);

  404.         this._scrollTimeout = setTimeout(function () {
  405.             self._triggerScroll(e);
  406.         }, this._scrollDelay);
  407.     },

  408.     /**
  409.     Caches the `scrollDelay` value after that attribute changes to allow
  410.     quicker lookups in critical path code.

  411.     @method _afterScrollDelayChange
  412.     @param {EventFacade} e
  413.     @protected
  414.     **/
  415.     _afterScrollDelayChange: function (e) {
  416.         this._scrollDelay = e.newVal;
  417.     },

  418.     /**
  419.     Caches the `scrollMargin` value after that attribute changes to allow
  420.     quicker lookups in critical path code.

  421.     @method _afterScrollMarginChange
  422.     @param {EventFacade} e
  423.     @protected
  424.     **/
  425.     _afterScrollMarginChange: function (e) {
  426.         this._scrollMargin = e.newVal;
  427.     }
  428. }, {
  429.     NS: 'scrollInfo',

  430.     ATTRS: {
  431.         /**
  432.         Number of milliseconds to wait after a native `scroll` event before
  433.         firing local scroll events. If another native scroll event occurs during
  434.         this time, previous events will be ignored. This ensures that we don't
  435.         fire thousands of events when the user is scrolling quickly.

  436.         @attribute scrollDelay
  437.         @type Number
  438.         @default 50
  439.         **/
  440.         scrollDelay: {
  441.             value: 50
  442.         },

  443.         /**
  444.         Additional margin in pixels beyond the onscreen region of the host node
  445.         that should be considered "onscreen".

  446.         For example, if set to 50, then a `scrollToBottom` event would be fired
  447.         when the user scrolls to within 50 pixels of the bottom of the
  448.         scrollable region, even if they don't actually scroll completely to the
  449.         very bottom pixel.

  450.         This margin also applies to the `getOffscreenNodes()` and
  451.         `getOnscreenNodes()` methods by default.

  452.         @attribute scrollMargin
  453.         @type Number
  454.         @default 50
  455.         **/
  456.         scrollMargin: {
  457.             value: 50
  458.         }
  459.     }
  460. });

  461.