|
1 /* |
|
2 YUI 3.10.3 (build 2fb5187) |
|
3 Copyright 2013 Yahoo! Inc. All rights reserved. |
|
4 Licensed under the BSD License. |
|
5 http://yuilibrary.com/license/ |
|
6 */ |
|
7 |
|
8 YUI.add('node-scroll-info', function (Y, NAME) { |
|
9 |
|
10 /** |
|
11 Provides the ScrollInfo Node plugin, which exposes convenient events and methods |
|
12 related to scrolling. |
|
13 |
|
14 @module node-scroll-info |
|
15 @since 3.7.0 |
|
16 **/ |
|
17 |
|
18 /** |
|
19 Provides convenient events and methods related to scrolling. This could be used, |
|
20 for example, to implement infinite scrolling, or to lazy-load content based on |
|
21 the current scroll position. |
|
22 |
|
23 ### Example |
|
24 |
|
25 var body = Y.one('body'); |
|
26 |
|
27 body.plug(Y.Plugin.ScrollInfo); |
|
28 |
|
29 body.scrollInfo.on('scrollToBottom', function (e) { |
|
30 // Load more content when the user scrolls to the bottom of the page. |
|
31 }); |
|
32 |
|
33 @class Plugin.ScrollInfo |
|
34 @extends Plugin.Base |
|
35 @since 3.7.0 |
|
36 **/ |
|
37 |
|
38 /** |
|
39 Fired when the user scrolls within the host node. |
|
40 |
|
41 This event (like all scroll events exposed by ScrollInfo) is throttled and fired |
|
42 only after the number of milliseconds specified by the `scrollDelay` attribute |
|
43 have passed in order to prevent thrashing. |
|
44 |
|
45 This event passes along the event facade for the standard DOM `scroll` event and |
|
46 mixes in the following additional properties. |
|
47 |
|
48 @event scroll |
|
49 @param {Boolean} atBottom Whether the current scroll position is at the bottom |
|
50 of the scrollable region. |
|
51 @param {Boolean} atLeft Whether the current scroll position is at the extreme |
|
52 left of the scrollable region. |
|
53 @param {Boolean} atRight Whether the current scroll position is at the extreme |
|
54 right of the scrollable region. |
|
55 @param {Boolean} atTop Whether the current scroll position is at the top of the |
|
56 scrollable region. |
|
57 @param {Boolean} isScrollDown `true` if the user scrolled down. |
|
58 @param {Boolean} isScrollLeft `true` if the user scrolled left. |
|
59 @param {Boolean} isScrollRight `true` if the user scrolled right. |
|
60 @param {Boolean} isScrollUp `true` if the user scrolled up. |
|
61 @param {Number} scrollBottom Y value of the bottom-most onscreen pixel of the |
|
62 scrollable region. |
|
63 @param {Number} scrollHeight Total height in pixels of the scrollable region, |
|
64 including offscreen pixels. |
|
65 @param {Number} scrollLeft X value of the left-most onscreen pixel of the |
|
66 scrollable region. |
|
67 @param {Number} scrollRight X value of the right-most onscreen pixel of the |
|
68 scrollable region. |
|
69 @param {Number} scrollTop Y value of the top-most onscreen pixel of the |
|
70 scrollable region. |
|
71 @param {Number} scrollWidth Total width in pixels of the scrollable region, |
|
72 including offscreen pixels. |
|
73 @see scrollDelay |
|
74 @see scrollMargin |
|
75 **/ |
|
76 var EVT_SCROLL = 'scroll', |
|
77 |
|
78 /** |
|
79 Fired when the user scrolls down within the host node. |
|
80 |
|
81 This event provides the same event facade as the `scroll` event. See that |
|
82 event for details. |
|
83 |
|
84 @event scrollDown |
|
85 @see scroll |
|
86 **/ |
|
87 EVT_SCROLL_DOWN = 'scrollDown', |
|
88 |
|
89 /** |
|
90 Fired when the user scrolls left within the host node. |
|
91 |
|
92 This event provides the same event facade as the `scroll` event. See that |
|
93 event for details. |
|
94 |
|
95 @event scrollLeft |
|
96 @see scroll |
|
97 **/ |
|
98 EVT_SCROLL_LEFT = 'scrollLeft', |
|
99 |
|
100 /** |
|
101 Fired when the user scrolls right within the host node. |
|
102 |
|
103 This event provides the same event facade as the `scroll` event. See that |
|
104 event for details. |
|
105 |
|
106 @event scrollRight |
|
107 @see scroll |
|
108 **/ |
|
109 EVT_SCROLL_RIGHT = 'scrollRight', |
|
110 |
|
111 /** |
|
112 Fired when the user scrolls up within the host node. |
|
113 |
|
114 This event provides the same event facade as the `scroll` event. See that |
|
115 event for details. |
|
116 |
|
117 @event scrollUp |
|
118 @see scroll |
|
119 **/ |
|
120 EVT_SCROLL_UP = 'scrollUp', |
|
121 |
|
122 /** |
|
123 Fired when the user scrolls to the bottom of the scrollable region within |
|
124 the host node. |
|
125 |
|
126 This event provides the same event facade as the `scroll` event. See that |
|
127 event for details. |
|
128 |
|
129 @event scrollToBottom |
|
130 @see scroll |
|
131 **/ |
|
132 EVT_SCROLL_TO_BOTTOM = 'scrollToBottom', |
|
133 |
|
134 /** |
|
135 Fired when the user scrolls to the extreme left of the scrollable region |
|
136 within the host node. |
|
137 |
|
138 This event provides the same event facade as the `scroll` event. See that |
|
139 event for details. |
|
140 |
|
141 @event scrollToLeft |
|
142 @see scroll |
|
143 **/ |
|
144 EVT_SCROLL_TO_LEFT = 'scrollToLeft', |
|
145 |
|
146 /** |
|
147 Fired when the user scrolls to the extreme right of the scrollable region |
|
148 within the host node. |
|
149 |
|
150 This event provides the same event facade as the `scroll` event. See that |
|
151 event for details. |
|
152 |
|
153 @event scrollToRight |
|
154 @see scroll |
|
155 **/ |
|
156 EVT_SCROLL_TO_RIGHT = 'scrollToRight', |
|
157 |
|
158 /** |
|
159 Fired when the user scrolls to the top of the scrollable region within the |
|
160 host node. |
|
161 |
|
162 This event provides the same event facade as the `scroll` event. See that |
|
163 event for details. |
|
164 |
|
165 @event scrollToTop |
|
166 @see scroll |
|
167 **/ |
|
168 EVT_SCROLL_TO_TOP = 'scrollToTop'; |
|
169 |
|
170 Y.Plugin.ScrollInfo = Y.Base.create('scrollInfoPlugin', Y.Plugin.Base, [], { |
|
171 // -- Lifecycle Methods ---------------------------------------------------- |
|
172 initializer: function (config) { |
|
173 // Cache for quicker lookups in the critical path. |
|
174 this._host = config.host; |
|
175 this._hostIsBody = this._host.get('nodeName').toLowerCase() === 'body'; |
|
176 this._scrollDelay = this.get('scrollDelay'); |
|
177 this._scrollMargin = this.get('scrollMargin'); |
|
178 this._scrollNode = this._getScrollNode(); |
|
179 |
|
180 this.refreshDimensions(); |
|
181 |
|
182 this._lastScroll = this.getScrollInfo(); |
|
183 |
|
184 this._bind(); |
|
185 }, |
|
186 |
|
187 destructor: function () { |
|
188 (new Y.EventHandle(this._events)).detach(); |
|
189 delete this._events; |
|
190 }, |
|
191 |
|
192 // -- Public Methods ------------------------------------------------------- |
|
193 |
|
194 /** |
|
195 Returns a NodeList containing all offscreen nodes inside the host node that |
|
196 match the given CSS selector. An offscreen node is any node that is entirely |
|
197 outside the visible (onscreen) region of the host node based on the current |
|
198 scroll location. |
|
199 |
|
200 @method getOffscreenNodes |
|
201 @param {String} [selector] CSS selector. If omitted, all offscreen nodes |
|
202 will be returned. |
|
203 @param {Number} [margin] Additional margin in pixels beyond the actual |
|
204 onscreen region that should be considered "onscreen" for the purposes of |
|
205 this query. Defaults to the value of the `scrollMargin` attribute. |
|
206 @return {NodeList} Offscreen nodes matching _selector_. |
|
207 @see scrollMargin |
|
208 **/ |
|
209 getOffscreenNodes: function (selector, margin) { |
|
210 if (typeof margin === 'undefined') { |
|
211 margin = this._scrollMargin; |
|
212 } |
|
213 |
|
214 var lastScroll = this._lastScroll, |
|
215 nodes = this._host.all(selector || '*'), |
|
216 |
|
217 scrollBottom = lastScroll.scrollBottom + margin, |
|
218 scrollLeft = lastScroll.scrollLeft - margin, |
|
219 scrollRight = lastScroll.scrollRight + margin, |
|
220 scrollTop = lastScroll.scrollTop - margin, |
|
221 |
|
222 self = this; |
|
223 |
|
224 return nodes.filter(function (el) { |
|
225 var xy = Y.DOM.getXY(el), |
|
226 elLeft = xy[0] - self._left, |
|
227 elTop = xy[1] - self._top, |
|
228 elBottom, elRight; |
|
229 |
|
230 // Check whether the element's top left point is within the |
|
231 // viewport. This is the least expensive check. |
|
232 if (elLeft >= scrollLeft && elLeft < scrollRight && |
|
233 elTop >= scrollTop && elTop < scrollBottom) { |
|
234 |
|
235 return false; |
|
236 } |
|
237 |
|
238 // Check whether the element's bottom right point is within the |
|
239 // viewport. This check is more expensive since we have to get the |
|
240 // element's height and width. |
|
241 elBottom = elTop + el.offsetHeight; |
|
242 elRight = elLeft + el.offsetWidth; |
|
243 |
|
244 if (elRight < scrollRight && elRight >= scrollLeft && |
|
245 elBottom < scrollBottom && elBottom >= scrollTop) { |
|
246 |
|
247 return false; |
|
248 } |
|
249 |
|
250 // If we get here, the element isn't within the viewport. |
|
251 return true; |
|
252 }); |
|
253 }, |
|
254 |
|
255 /** |
|
256 Returns a NodeList containing all onscreen nodes inside the host node that |
|
257 match the given CSS selector. An onscreen node is any node that is fully or |
|
258 partially within the visible (onscreen) region of the host node based on the |
|
259 current scroll location. |
|
260 |
|
261 @method getOnscreenNodes |
|
262 @param {String} [selector] CSS selector. If omitted, all onscreen nodes will |
|
263 be returned. |
|
264 @param {Number} [margin] Additional margin in pixels beyond the actual |
|
265 onscreen region that should be considered "onscreen" for the purposes of |
|
266 this query. Defaults to the value of the `scrollMargin` attribute. |
|
267 @return {NodeList} Onscreen nodes matching _selector_. |
|
268 @see scrollMargin |
|
269 **/ |
|
270 getOnscreenNodes: function (selector, margin) { |
|
271 if (typeof margin === 'undefined') { |
|
272 margin = this._scrollMargin; |
|
273 } |
|
274 |
|
275 var lastScroll = this._lastScroll, |
|
276 nodes = this._host.all(selector || '*'), |
|
277 |
|
278 scrollBottom = lastScroll.scrollBottom + margin, |
|
279 scrollLeft = lastScroll.scrollLeft - margin, |
|
280 scrollRight = lastScroll.scrollRight + margin, |
|
281 scrollTop = lastScroll.scrollTop - margin, |
|
282 |
|
283 self = this; |
|
284 |
|
285 return nodes.filter(function (el) { |
|
286 var xy = Y.DOM.getXY(el), |
|
287 elLeft = xy[0] - self._left, |
|
288 elTop = xy[1] - self._top, |
|
289 elBottom, elRight; |
|
290 |
|
291 // Check whether the element's top left point is within the |
|
292 // viewport. This is the least expensive check. |
|
293 if (elLeft >= scrollLeft && elLeft < scrollRight && |
|
294 elTop >= scrollTop && elTop < scrollBottom) { |
|
295 |
|
296 return true; |
|
297 } |
|
298 |
|
299 // Check whether the element's bottom right point is within the |
|
300 // viewport. This check is more expensive since we have to get the |
|
301 // element's height and width. |
|
302 elBottom = elTop + el.offsetHeight; |
|
303 elRight = elLeft + el.offsetWidth; |
|
304 |
|
305 if (elRight < scrollRight && elRight >= scrollLeft && |
|
306 elBottom < scrollBottom && elBottom >= scrollTop) { |
|
307 |
|
308 return true; |
|
309 } |
|
310 |
|
311 // If we get here, the element isn't within the viewport. |
|
312 return false; |
|
313 }); |
|
314 }, |
|
315 |
|
316 /** |
|
317 Returns an object hash containing information about the current scroll |
|
318 position of the host node. This is the same information that's mixed into |
|
319 the event facade of the `scroll` event and other scroll-related events. |
|
320 |
|
321 @method getScrollInfo |
|
322 @return {Object} Object hash containing information about the current scroll |
|
323 position. See the `scroll` event for details on what properties this |
|
324 object contains. |
|
325 @see scroll |
|
326 **/ |
|
327 getScrollInfo: function () { |
|
328 var domNode = this._scrollNode, |
|
329 lastScroll = this._lastScroll, |
|
330 margin = this._scrollMargin, |
|
331 |
|
332 scrollLeft = domNode.scrollLeft, |
|
333 scrollHeight = domNode.scrollHeight, |
|
334 scrollTop = domNode.scrollTop, |
|
335 scrollWidth = domNode.scrollWidth, |
|
336 |
|
337 scrollBottom = scrollTop + this._height, |
|
338 scrollRight = scrollLeft + this._width; |
|
339 |
|
340 return { |
|
341 atBottom: scrollBottom > (scrollHeight - margin), |
|
342 atLeft : scrollLeft < margin, |
|
343 atRight : scrollRight > (scrollWidth - margin), |
|
344 atTop : scrollTop < margin, |
|
345 |
|
346 isScrollDown : lastScroll && scrollTop > lastScroll.scrollTop, |
|
347 isScrollLeft : lastScroll && scrollLeft < lastScroll.scrollLeft, |
|
348 isScrollRight: lastScroll && scrollLeft > lastScroll.scrollLeft, |
|
349 isScrollUp : lastScroll && scrollTop < lastScroll.scrollTop, |
|
350 |
|
351 scrollBottom: scrollBottom, |
|
352 scrollHeight: scrollHeight, |
|
353 scrollLeft : scrollLeft, |
|
354 scrollRight : scrollRight, |
|
355 scrollTop : scrollTop, |
|
356 scrollWidth : scrollWidth |
|
357 }; |
|
358 }, |
|
359 |
|
360 /** |
|
361 Refreshes cached position, height, and width dimensions for the host node. |
|
362 If the host node is the body, then the viewport height and width will be |
|
363 used. |
|
364 |
|
365 This info is cached to improve performance during scroll events, since it's |
|
366 expensive to touch the DOM for these values. Dimensions are automatically |
|
367 refreshed whenever the browser is resized, but if you change the dimensions |
|
368 or position of the host node in JS, you may need to call |
|
369 `refreshDimensions()` manually to cache the new dimensions. |
|
370 |
|
371 @method refreshDimensions |
|
372 **/ |
|
373 refreshDimensions: function () { |
|
374 // WebKit only returns reliable scroll info on the body, and only |
|
375 // returns reliable height/width info on the documentElement, so we |
|
376 // have to special-case it (see the other special case in |
|
377 // _getScrollNode()). |
|
378 // |
|
379 // On iOS devices, documentElement.clientHeight/Width aren't reliable, |
|
380 // but window.innerHeight/Width are. And no, dom-screen's viewport size |
|
381 // methods don't account for this, which is why we do it here. |
|
382 |
|
383 var hostIsBody = this._hostIsBody, |
|
384 iosHack = hostIsBody && Y.UA.ios, |
|
385 win = Y.config.win, |
|
386 el; |
|
387 |
|
388 if (hostIsBody && Y.UA.webkit) { |
|
389 el = Y.config.doc.documentElement; |
|
390 } else { |
|
391 el = this._scrollNode; |
|
392 } |
|
393 |
|
394 this._height = iosHack ? win.innerHeight : el.clientHeight; |
|
395 this._left = el.offsetLeft; |
|
396 this._top = el.offsetTop; |
|
397 this._width = iosHack ? win.innerWidth : el.clientWidth; |
|
398 }, |
|
399 |
|
400 // -- Protected Methods ---------------------------------------------------- |
|
401 |
|
402 /** |
|
403 Binds event handlers. |
|
404 |
|
405 @method _bind |
|
406 @protected |
|
407 **/ |
|
408 _bind: function () { |
|
409 var winNode = Y.one('win'); |
|
410 |
|
411 this._events = [ |
|
412 this.after({ |
|
413 scrollDelayChange : this._afterScrollDelayChange, |
|
414 scrollMarginChange: this._afterScrollMarginChange |
|
415 }), |
|
416 |
|
417 winNode.on('windowresize', this._afterResize, this), |
|
418 |
|
419 // If we're attached to the body, listen for the scroll event on the |
|
420 // window, since <body> doesn't have a scroll event. |
|
421 (this._hostIsBody ? winNode : this._host).after( |
|
422 'scroll', this._afterScroll, this) |
|
423 ]; |
|
424 }, |
|
425 |
|
426 /** |
|
427 Returns the DOM node that should be used to lookup scroll coordinates. In |
|
428 some browsers, the `<body>` element doesn't return scroll coordinates, and |
|
429 the documentElement must be used instead; this method takes care of |
|
430 determining which node should be used. |
|
431 |
|
432 @method _getScrollNode |
|
433 @return {HTMLElement} DOM node. |
|
434 @protected |
|
435 **/ |
|
436 _getScrollNode: function () { |
|
437 // WebKit returns scroll coordinates on the body element, but other |
|
438 // browsers don't, so we have to use the documentElement. |
|
439 return this._hostIsBody && !Y.UA.webkit ? Y.config.doc.documentElement : |
|
440 Y.Node.getDOMNode(this._host); |
|
441 }, |
|
442 |
|
443 /** |
|
444 Mixes detailed scroll information into the given DOM `scroll` event facade |
|
445 and fires appropriate local events. |
|
446 |
|
447 @method _triggerScroll |
|
448 @param {EventFacade} e Event facade from the DOM `scroll` event. |
|
449 @protected |
|
450 **/ |
|
451 _triggerScroll: function (e) { |
|
452 var info = this.getScrollInfo(), |
|
453 facade = Y.merge(e, info), |
|
454 lastScroll = this._lastScroll; |
|
455 |
|
456 this._lastScroll = info; |
|
457 |
|
458 this.fire(EVT_SCROLL, facade); |
|
459 |
|
460 if (info.isScrollLeft) { |
|
461 this.fire(EVT_SCROLL_LEFT, facade); |
|
462 } else if (info.isScrollRight) { |
|
463 this.fire(EVT_SCROLL_RIGHT, facade); |
|
464 } |
|
465 |
|
466 if (info.isScrollUp) { |
|
467 this.fire(EVT_SCROLL_UP, facade); |
|
468 } else if (info.isScrollDown) { |
|
469 this.fire(EVT_SCROLL_DOWN, facade); |
|
470 } |
|
471 |
|
472 if (info.atBottom && (!lastScroll.atBottom || |
|
473 info.scrollHeight > lastScroll.scrollHeight)) { |
|
474 |
|
475 this.fire(EVT_SCROLL_TO_BOTTOM, facade); |
|
476 } |
|
477 |
|
478 if (info.atLeft && !lastScroll.atLeft) { |
|
479 this.fire(EVT_SCROLL_TO_LEFT, facade); |
|
480 } |
|
481 |
|
482 if (info.atRight && (!lastScroll.atRight || |
|
483 info.scrollWidth > lastScroll.scrollWidth)) { |
|
484 |
|
485 this.fire(EVT_SCROLL_TO_RIGHT, facade); |
|
486 } |
|
487 |
|
488 if (info.atTop && !lastScroll.atTop) { |
|
489 this.fire(EVT_SCROLL_TO_TOP, facade); |
|
490 } |
|
491 }, |
|
492 |
|
493 // -- Protected Event Handlers --------------------------------------------- |
|
494 |
|
495 /** |
|
496 Handles browser resize events. |
|
497 |
|
498 @method _afterResize |
|
499 @param {EventFacade} e |
|
500 @protected |
|
501 **/ |
|
502 _afterResize: function (e) { |
|
503 this.refreshDimensions(); |
|
504 }, |
|
505 |
|
506 /** |
|
507 Handles DOM `scroll` events. |
|
508 |
|
509 @method _afterScroll |
|
510 @param {EventFacade} e |
|
511 @protected |
|
512 **/ |
|
513 _afterScroll: function (e) { |
|
514 var self = this; |
|
515 |
|
516 clearTimeout(this._scrollTimeout); |
|
517 |
|
518 this._scrollTimeout = setTimeout(function () { |
|
519 self._triggerScroll(e); |
|
520 }, this._scrollDelay); |
|
521 }, |
|
522 |
|
523 /** |
|
524 Caches the `scrollDelay` value after that attribute changes to allow |
|
525 quicker lookups in critical path code. |
|
526 |
|
527 @method _afterScrollDelayChange |
|
528 @param {EventFacade} e |
|
529 @protected |
|
530 **/ |
|
531 _afterScrollDelayChange: function (e) { |
|
532 this._scrollDelay = e.newVal; |
|
533 }, |
|
534 |
|
535 /** |
|
536 Caches the `scrollMargin` value after that attribute changes to allow |
|
537 quicker lookups in critical path code. |
|
538 |
|
539 @method _afterScrollMarginChange |
|
540 @param {EventFacade} e |
|
541 @protected |
|
542 **/ |
|
543 _afterScrollMarginChange: function (e) { |
|
544 this._scrollMargin = e.newVal; |
|
545 } |
|
546 }, { |
|
547 NS: 'scrollInfo', |
|
548 |
|
549 ATTRS: { |
|
550 /** |
|
551 Number of milliseconds to wait after a native `scroll` event before |
|
552 firing local scroll events. If another native scroll event occurs during |
|
553 this time, previous events will be ignored. This ensures that we don't |
|
554 fire thousands of events when the user is scrolling quickly. |
|
555 |
|
556 @attribute scrollDelay |
|
557 @type Number |
|
558 @default 50 |
|
559 **/ |
|
560 scrollDelay: { |
|
561 value: 50 |
|
562 }, |
|
563 |
|
564 /** |
|
565 Additional margin in pixels beyond the onscreen region of the host node |
|
566 that should be considered "onscreen". |
|
567 |
|
568 For example, if set to 50, then a `scrollToBottom` event would be fired |
|
569 when the user scrolls to within 50 pixels of the bottom of the |
|
570 scrollable region, even if they don't actually scroll completely to the |
|
571 very bottom pixel. |
|
572 |
|
573 This margin also applies to the `getOffscreenNodes()` and |
|
574 `getOnscreenNodes()` methods by default. |
|
575 |
|
576 @attribute scrollMargin |
|
577 @type Number |
|
578 @default 50 |
|
579 **/ |
|
580 scrollMargin: { |
|
581 value: 50 |
|
582 } |
|
583 } |
|
584 }); |
|
585 |
|
586 |
|
587 }, '3.10.3', {"requires": ["base-build", "dom-screen", "event-resize", "node-pluginhost", "plugin"]}); |