NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / node-scroll-info / node-scroll-info.js
blob9ebc6f4c380c5c2a2a93c7c0dada746ca174bc83
1 /*
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/
6 */
8 YUI.add('node-scroll-info', function (Y, NAME) {
10 /*jshint onevar:false */
12 /**
13 Provides the ScrollInfo Node plugin, which exposes convenient events and methods
14 related to scrolling.
16 @module node-scroll-info
17 @since 3.7.0
18 **/
20 /**
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.
25 ### Example
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.
33     });
35 @class Plugin.ScrollInfo
36 @extends Plugin.Base
37 @since 3.7.0
38 **/
40 var doc = Y.config.doc,
41     win = Y.config.win;
43 /**
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.
53 @event scroll
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
61     scrollable region.
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
67     scrollable region.
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
71     scrollable region.
72 @param {Number} scrollRight X value of the right-most onscreen pixel of the
73     scrollable region.
74 @param {Number} scrollTop Y value of the top-most onscreen pixel of the
75     scrollable region.
76 @param {Number} scrollWidth Total width in pixels of the scrollable region,
77     including offscreen pixels.
78 @see scrollDelay
79 @see scrollMargin
80 **/
81 var EVT_SCROLL = 'scroll',
83     /**
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
87     event for details.
89     @event scrollDown
90     @see scroll
91     **/
92     EVT_SCROLL_DOWN = 'scrollDown',
94     /**
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
98     event for details.
100     @event scrollLeft
101     @see scroll
102     **/
103     EVT_SCROLL_LEFT = 'scrollLeft',
105     /**
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
109     event for details.
111     @event scrollRight
112     @see scroll
113     **/
114     EVT_SCROLL_RIGHT = 'scrollRight',
116     /**
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
120     event for details.
122     @event scrollUp
123     @see scroll
124     **/
125     EVT_SCROLL_UP = 'scrollUp',
127     /**
128     Fired when the user scrolls to the bottom of the scrollable region within
129     the host node.
131     This event provides the same event facade as the `scroll` event. See that
132     event for details.
134     @event scrollToBottom
135     @see scroll
136     **/
137     EVT_SCROLL_TO_BOTTOM = 'scrollToBottom',
139     /**
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
144     event for details.
146     @event scrollToLeft
147     @see scroll
148     **/
149     EVT_SCROLL_TO_LEFT = 'scrollToLeft',
151     /**
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
156     event for details.
158     @event scrollToRight
159     @see scroll
160     **/
161     EVT_SCROLL_TO_RIGHT = 'scrollToRight',
163     /**
164     Fired when the user scrolls to the top of the scrollable region within the
165     host node.
167     This event provides the same event facade as the `scroll` event. See that
168     event for details.
170     @event scrollToTop
171     @see scroll
172     **/
173     EVT_SCROLL_TO_TOP = 'scrollToTop';
175 Y.Plugin.ScrollInfo = Y.Base.create('scrollInfoPlugin', Y.Plugin.Base, [], {
176     // -- Protected Properties -------------------------------------------------
178     /**
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
183     @protected
184     **/
186     /**
187     Whether or not the host node is the `<body>` element.
189     @property {Boolean} _hostIsBody
190     @protected
191     **/
193     /**
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
198     @protected
199     **/
201     /**
202     Height of the viewport in pixels.
204     @property {Number} _winHeight
205     @protected
206     **/
208     /**
209     Width of the viewport in pixels.
211     @property {Number} _winWidth
212     @protected
213     **/
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();
228         this._bind();
229     },
231     destructor: function () {
232         new Y.EventHandle(this._events).detach();
233         this._events = null;
234     },
236     // -- Public Methods -------------------------------------------------------
238     /**
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
242     scroll location.
244     @method getOffscreenNodes
245     @param {String} [selector] CSS selector. If omitted, all offscreen nodes
246         will be returned.
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_.
251     @see scrollMargin
252     **/
253     getOffscreenNodes: function (selector, margin) {
254         if (typeof margin === 'undefined') {
255             margin = this._scrollMargin;
256         }
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);
262         }, this));
263     },
265     /**
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
273         be returned.
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_.
278     @see scrollMargin
279     **/
280     getOnscreenNodes: function (selector, margin) {
281         if (typeof margin === 'undefined') {
282             margin = this._scrollMargin;
283         }
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);
289         }, this));
290     },
292     /**
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
300         object contains.
301     @see scroll
302     **/
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;
316         return {
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
333         };
334     },
336     /**
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.
347     @since 3.11.0
348     **/
349     isNodeOnscreen: function (node, margin) {
350         node = Y.one(node);
351         return !!(node && this._isElementOnscreen(node._node, margin));
352     },
354     /**
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
357     used.
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
366     **/
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.
373         if (Y.UA.ios) {
374             this._winHeight = win.innerHeight;
375             this._winWidth  = win.innerWidth;
376         } else {
377             this._winHeight = docEl.clientHeight;
378             this._winWidth  = docEl.clientWidth;
379         }
381         if (this._hostIsBody) {
382             this._height = this._winHeight;
383             this._width  = this._winWidth;
384         } else {
385             this._height = this._scrollNode.clientHeight;
386             this._width  = this._scrollNode.clientWidth;
387         }
389         this._refreshHostBoundingRect();
390     },
392     // -- Protected Methods ----------------------------------------------------
394     /**
395     Binds event handlers.
397     @method _bind
398     @protected
399     **/
400     _bind: function () {
401         var winNode = Y.one('win');
403         this._events = [
404             this.after({
405                 scrollDelayChange : this._afterScrollDelayChange,
406                 scrollMarginChange: this._afterScrollMarginChange
407             }),
409             winNode.on('windowresize', this._afterResize, this)
410         ];
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));
416         } else {
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
419             // onscreen.
420             this._events.push(
421                 winNode.after('scroll', this._afterWindowScroll, this),
422                 this._host.after('scroll', this._afterHostScroll, this)
423             );
424         }
425     },
427     /**
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.
435     @protected
436     **/
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);
442     },
444     /**
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.
454     @protected
455     @since 3.11.0
456     **/
457     _isElementOnscreen: function (el, margin) {
458         var hostRect = this._hostRect,
459             rect     = el.getBoundingClientRect();
461         if (typeof margin === 'undefined') {
462             margin = this._scrollMargin;
463         }
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
467         // the host element.
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);
472     },
474     /**
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
482     @protected
483     **/
484     _refreshHostBoundingRect: function () {
485         var winHeight = this._winHeight,
486             winWidth  = this._winWidth,
488             hostRect;
490         if (this._hostIsBody) {
491             hostRect = {
492                 bottom: winHeight,
493                 height: winHeight,
494                 left  : 0,
495                 right : winWidth,
496                 top   : 0,
497                 width : winWidth
498             };
500             this._isHostOnscreen = true;
501         } else {
502             hostRect = this._scrollNode.getBoundingClientRect();
503         }
505         this._hostRect = hostRect;
506     },
508     /**
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.
514     @protected
515     **/
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);
529         }
531         if (info.isScrollUp) {
532             this.fire(EVT_SCROLL_UP, facade);
533         } else if (info.isScrollDown) {
534             this.fire(EVT_SCROLL_DOWN, facade);
535         }
537         if (info.atBottom && (!lastScroll.atBottom ||
538                 info.scrollHeight > lastScroll.scrollHeight)) {
540             this.fire(EVT_SCROLL_TO_BOTTOM, facade);
541         }
543         if (info.atLeft && !lastScroll.atLeft) {
544             this.fire(EVT_SCROLL_TO_LEFT, facade);
545         }
547         if (info.atRight && (!lastScroll.atRight ||
548                 info.scrollWidth > lastScroll.scrollWidth)) {
550             this.fire(EVT_SCROLL_TO_RIGHT, facade);
551         }
553         if (info.atTop && !lastScroll.atTop) {
554             this.fire(EVT_SCROLL_TO_TOP, facade);
555         }
556     },
558     // -- Protected Event Handlers ---------------------------------------------
560     /**
561     Handles DOM `scroll` events on the host node.
563     @method _afterHostScroll
564     @param {EventFacade} e
565     @protected
566     **/
567     _afterHostScroll: function (e) {
568         var self = this;
570         clearTimeout(this._scrollTimeout);
572         this._scrollTimeout = setTimeout(function () {
573             self._triggerScroll(e);
574         }, this._scrollDelay);
575     },
577     /**
578     Handles browser resize events.
580     @method _afterResize
581     @protected
582     **/
583     _afterResize: function () {
584         this.refreshDimensions();
585     },
587     /**
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
593     @protected
594     **/
595     _afterScrollDelayChange: function (e) {
596         this._scrollDelay = e.newVal;
597     },
599     /**
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
605     @protected
606     **/
607     _afterScrollMarginChange: function (e) {
608         this._scrollMargin = e.newVal;
609     },
611     /**
612     Handles DOM `scroll` events on the window.
614     @method _afterWindowScroll
615     @param {EventFacade} e
616     @protected
617     **/
618     _afterWindowScroll: function () {
619         this._refreshHostBoundingRect();
620     }
621 }, {
622     NS: 'scrollInfo',
624     ATTRS: {
625         /**
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
632         @type Number
633         @default 50
634         **/
635         scrollDelay: {
636             value: 50
637         },
639         /**
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
646         very bottom pixel.
648         This margin also applies to the `getOffscreenNodes()` and
649         `getOnscreenNodes()` methods by default.
651         @attribute scrollMargin
652         @type Number
653         @default 50
654         **/
655         scrollMargin: {
656             value: 50
657         }
658     }
662 }, '3.13.0', {"requires": ["array-extras", "base-build", "event-resize", "node-pluginhost", "plugin", "selector"]});