weekly release 2.4dev
[moodle.git] / lib / yuilib / 3.7.1 / build / node-scroll-info / node-scroll-info-debug.js
blobf5e9e4a7c23eb3aeeff0dc7f0c036a9219d48902
1 /*
2 YUI 3.7.1 (build 5627)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('node-scroll-info', function (Y, NAME) {
9 /**
10 Provides the ScrollInfo Node plugin, which exposes convenient events and methods
11 related to scrolling.
13 @module node-scroll-info
14 @since 3.7.0
15 **/
17 /**
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.
22 ### Example
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.
30     });
32 @class Plugin.ScrollInfo
33 @extends Plugin.Base
34 @since 3.7.0
35 **/
37 /**
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.
47 @event scroll
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
55     scrollable region.
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
61     scrollable region.
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
65     scrollable region.
66 @param {Number} scrollRight X value of the right-most onscreen pixel of the
67     scrollable region.
68 @param {Number} scrollTop Y value of the top-most onscreen pixel of the
69     scrollable region.
70 @param {Number} scrollWidth Total width in pixels of the scrollable region,
71     including offscreen pixels.
72 @see scrollDelay
73 @see scrollMargin
74 **/
75 var EVT_SCROLL = 'scroll',
77     /**
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
81     event for details.
83     @event scrollDown
84     @see scroll
85     **/
86     EVT_SCROLL_DOWN = 'scrollDown',
88     /**
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
92     event for details.
94     @event scrollLeft
95     @see scroll
96     **/
97     EVT_SCROLL_LEFT = 'scrollLeft',
99     /**
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
103     event for details.
105     @event scrollRight
106     @see scroll
107     **/
108     EVT_SCROLL_RIGHT = 'scrollRight',
110     /**
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
114     event for details.
116     @event scrollUp
117     @see scroll
118     **/
119     EVT_SCROLL_UP = 'scrollUp',
121     /**
122     Fired when the user scrolls to the bottom of the scrollable region within
123     the host node.
125     This event provides the same event facade as the `scroll` event. See that
126     event for details.
128     @event scrollToBottom
129     @see scroll
130     **/
131     EVT_SCROLL_TO_BOTTOM = 'scrollToBottom',
133     /**
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
138     event for details.
140     @event scrollToLeft
141     @see scroll
142     **/
143     EVT_SCROLL_TO_LEFT = 'scrollToLeft',
145     /**
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
150     event for details.
152     @event scrollToRight
153     @see scroll
154     **/
155     EVT_SCROLL_TO_RIGHT = 'scrollToRight',
157     /**
158     Fired when the user scrolls to the top of the scrollable region within the
159     host node.
161     This event provides the same event facade as the `scroll` event. See that
162     event for details.
164     @event scrollToTop
165     @see scroll
166     **/
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();
183         this._bind();
184     },
186     destructor: function () {
187         (new Y.EventHandle(this._events)).detach();
188         delete this._events;
189     },
191     // -- Public Methods -------------------------------------------------------
193     /**
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
197     scroll location.
199     @method getOffscreenNodes
200     @param {String} [selector] CSS selector. If omitted, all offscreen nodes
201         will be returned.
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_.
206     @see scrollMargin
207     **/
208     getOffscreenNodes: function (selector, margin) {
209         if (typeof margin === 'undefined') {
210             margin = this._scrollMargin;
211         }
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,
221             self = this;
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,
227                 elBottom, elRight;
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) {
234                 return false;
235             }
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) {
246                 return false;
247             }
249             // If we get here, the element isn't within the viewport.
250             return true;
251         });
252     },
254     /**
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
262         be returned.
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_.
267     @see scrollMargin
268     **/
269     getOnscreenNodes: function (selector, margin) {
270         if (typeof margin === 'undefined') {
271             margin = this._scrollMargin;
272         }
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,
282             self = this;
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,
288                 elBottom, elRight;
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) {
295                 return true;
296             }
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) {
307                 return true;
308             }
310             // If we get here, the element isn't within the viewport.
311             return false;
312         });
313     },
315     /**
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
323         object contains.
324     @see scroll
325     **/
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;
339         return {
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
356         };
357     },
359     /**
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
362     used.
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
371     **/
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()).
377         //
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,
384             win        = Y.config.win,
385             el;
387         if (hostIsBody && Y.UA.webkit) {
388             el = Y.config.doc.documentElement;
389         } else {
390             el = this._scrollNode;
391         }
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;
397     },
399     // -- Protected Methods ----------------------------------------------------
401     /**
402     Binds event handlers.
404     @method _bind
405     @protected
406     **/
407     _bind: function () {
408         var winNode = Y.one('win');
410         this._events = [
411             this.after({
412                 scrollDelayChange : this._afterScrollDelayChange,
413                 scrollMarginChange: this._afterScrollMarginChange
414             }),
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)
422         ];
423     },
425     /**
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.
433     @protected
434     **/
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);
440     },
442     /**
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.
448     @protected
449     **/
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);
463         }
465         if (info.isScrollUp) {
466             this.fire(EVT_SCROLL_UP, facade);
467         } else if (info.isScrollDown) {
468             this.fire(EVT_SCROLL_DOWN, facade);
469         }
471         if (info.atBottom && (!lastScroll.atBottom ||
472                 info.scrollHeight > lastScroll.scrollHeight)) {
474             this.fire(EVT_SCROLL_TO_BOTTOM, facade);
475         }
477         if (info.atLeft && !lastScroll.atLeft) {
478             this.fire(EVT_SCROLL_TO_LEFT, facade);
479         }
481         if (info.atRight && (!lastScroll.atRight ||
482                 info.scrollWidth > lastScroll.scrollWidth)) {
484             this.fire(EVT_SCROLL_TO_RIGHT, facade);
485         }
487         if (info.atTop && !lastScroll.atTop) {
488             this.fire(EVT_SCROLL_TO_TOP, facade);
489         }
490     },
492     // -- Protected Event Handlers ---------------------------------------------
494     /**
495     Handles browser resize events.
497     @method _afterResize
498     @param {EventFacade} e
499     @protected
500     **/
501     _afterResize: function (e) {
502         this.refreshDimensions();
503     },
505     /**
506     Handles DOM `scroll` events.
508     @method _afterScroll
509     @param {EventFacade} e
510     @protected
511     **/
512     _afterScroll: function (e) {
513         var self = this;
515         clearTimeout(this._scrollTimeout);
517         this._scrollTimeout = setTimeout(function () {
518             self._triggerScroll(e);
519         }, this._scrollDelay);
520     },
522     /**
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
528     @protected
529     **/
530     _afterScrollDelayChange: function (e) {
531         this._scrollDelay = e.newVal;
532     },
534     /**
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
540     @protected
541     **/
542     _afterScrollMarginChange: function (e) {
543         this._scrollMargin = e.newVal;
544     }
545 }, {
546     NS: 'scrollInfo',
548     ATTRS: {
549         /**
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
556         @type Number
557         @default 50
558         **/
559         scrollDelay: {
560             value: 50
561         },
563         /**
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
570         very bottom pixel.
572         This margin also applies to the `getOffscreenNodes()` and
573         `getOnscreenNodes()` methods by default.
575         @attribute scrollMargin
576         @type Number
577         @default 50
578         **/
579         scrollMargin: {
580             value: 50
581         }
582     }
586 }, '3.7.1', {"requires": ["base-build", "dom-screen", "event-resize", "node-pluginhost", "plugin"]});