MDL-32843 import YUI 3.5.1
[moodle.git] / lib / yui / 3.5.1 / build / scrollview-paginator / scrollview-paginator.js
bloba657380f56cb76ab0add1ea9aa95cecddfebd167
1 /*
2 YUI 3.5.1 (build 22)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('scrollview-paginator', function(Y) {
9 /**
10  * Provides a plugin, which adds pagination support to ScrollView instances
11  *
12  * @module scrollview-paginator
13  */
15 var UI = (Y.ScrollView) ? Y.ScrollView.UI_SRC : "ui",
16     INDEX = "index",
17     PREVINDEX = "prevIndex",
18     SCROLL_X = "scrollX",
19     SCROLL_Y = "scrollY",
20     TOTAL = "total",
21     BOUNDING_BOX = "boundingBox",
22     CONTENT_BOX = "contentBox",
23     MAX_PAGE_COUNT = 3; // @TODO: Make configurable?
25 /**
26  * Scrollview plugin that adds support for paging
27  *
28  * @class ScrollViewPaginator
29  * @namespace Plugin
30  * @extends Plugin.Base 
31  * @constructor
32  */
33 function PaginatorPlugin() {
34     PaginatorPlugin.superclass.constructor.apply(this, arguments);
37 /**
38  * The identity of the plugin
39  *
40  * @property NAME
41  * @type String
42  * @default 'paginatorPlugin'
43  * @static
44  */
45 PaginatorPlugin.NAME = 'pluginScrollViewPaginator';
47 /**
48  * The namespace on which the plugin will reside
49  *
50  * @property NS
51  * @type String
52  * @default 'pages'
53  * @static
54  */
55 PaginatorPlugin.NS = 'pages';
57 /**
58  * The default attribute configuration for the plugin
59  *
60  * @property ATTRS
61  * @type Object
62  * @static
63  */
64 PaginatorPlugin.ATTRS = {
66     /**
67      * CSS selector for a page inside the scrollview. The scrollview
68      * will snap to the closest page.
69      *
70      * @attribute selector
71      * @type {String}
72      */
73     selector: {
74         value: null
75     },
76     
77     /**
78      * The active page number for a paged scrollview
79      *
80      * @attribute index
81      * @type {Number}
82      * @default 0
83      */
84     index: {
85         value: 0
86     },
87     
88     /**
89      * The active page number for a paged scrollview
90      *
91      * @attribute index
92      * @type {Number}
93      * @default 0
94      */
95     prevIndex: {
96         value: 0
97     },
98     
99     /**
100      * The total number of pages
101      *
102      * @attribute total
103      * @type {Number}
104      * @default 0
105      */
106     total: {
107         value: 0
108     }
111 Y.extend(PaginatorPlugin, Y.Plugin.Base, {
112     
113     optimizeMemory: false,
114     _pageOffsets: null,
115     _pageNodes: null,
116     
117     /**
118      * Designated initializer
119      *
120      * @method initializer
121      */
122     initializer: function(config) { 
123         var paginator = this,
124             optimizeMemory = config.optimizeMemory || optimizeMemory;
125         
126         paginator._host = paginator.get('host');
127         paginator.beforeHostMethod('_flickFrame', paginator._flickFrame);
128         paginator.afterHostMethod('_uiDimensionsChange', paginator._calcOffsets);
129         paginator.afterHostEvent('scrollEnd', paginator._scrollEnded);
130         paginator.afterHostEvent('render', paginator._afterRender);
131         paginator.after('indexChange', paginator._afterIndexChange);
132         paginator.optimizeMemory = optimizeMemory;
133         paginator._pageNodes = new Y.NodeList();
134     },
136     /**
137      * Calculate the page boundary offsets
138      * 
139      * @method _calcOffsets
140      * @protected
141      */
142      _calcOffsets : function() {
143          var host = this._host,
144              cb = host.get(CONTENT_BOX),
145              bb = host.get(BOUNDING_BOX),
146              vert = host._scrollsVertical,
147              size = (vert) ? host._scrollHeight : host._scrollWidth,
148              pageSelector = this.get("selector"),
149              optimizeMemory = this.optimizeMemory,
150              currentIndex = this.get(INDEX),
151              pages,
152              offsets,
153              node;
154          
155          pages = pageSelector ? cb.all(pageSelector) : cb.get("children");
156          this._pageNodes = pages;
157          
158          //Set the total # of pages
159          this.set(TOTAL, pages.size());
161          // Determine the offsets
162          this._pageOffsets = pages.get((vert) ? "offsetTop" : "offsetLeft");
164          if (optimizeMemory) {
165              
166              this.set(PREVINDEX, currentIndex);
168              // Reduce the scroll width to the size of (3) pages (or whatever MAX_PAGE_COUNT is)
169              host._maxScrollX = this._pageOffsets[MAX_PAGE_COUNT-1];
171              // Empty the content-box. @TODO: leave {MAX_PAGE_COUNT} items in?
172              cb.empty(true);
174              // Now, fill it with the first set of items
175              for (var i=0; i < MAX_PAGE_COUNT; i++) {
176                  node = pages.item(currentIndex + i);
177                  cb.append(node);
178              }
179          }
180      },
181     /**
182      * Return the offset value where scrollview should scroll to.
183      * Neccesary because index # doesn't nessecarily map up to location in the DOM because of this._manageDOM()
184      *
185      * @method _getTargetOffset
186      * @param index {Number}
187      * @returns {Number}
188      * @protected
189      */
191     _getTargetOffset: function(index) {
192         var previous = this.get(PREVINDEX),
193             current = this.get(INDEX),
194             total = this.get(TOTAL),
195             forward = (previous < current) ? true : false,
196             pageOffsets = this._pageOffsets,
197             optimizeMemory = this.optimizeMemory,
198             offset, currentPageLocation;
199         
200         // @todo: Clean this up.  Probably do current (currentOffset - previousoffset) instead of assuming they're all the same width
201         if (optimizeMemory) {
202             if (forward) {
203                 if (index > 1) {
204                     currentPageLocation = 2;
205                 }
206                 else {
207                     currentPageLocation = 1;
208                 }
209             }
210             else {
211                 if (current == (total-2)) {
212                     currentPageLocation = 1;
213                 }
214                 else {
215                     currentPageLocation = 0;
216                 }
217             }   
218         }
219         else {
220             currentPageLocation = index;
221         }
222         
223         offset = pageOffsets[currentPageLocation]
224         return offset;
225     },
226     
227     /**
228      * Executed to respond to the flick event, by over-riding the default flickFrame animation. 
229      * This is needed to determine if the next or prev page should be activated.
230      *
231      * @method _flickFrame
232      * @protected
233      */
234     _flickFrame: function() {
235         var host = this._host,
236             velocity = host._currentVelocity,
237             inc = velocity < 0,
238             pageIndex = this.get(INDEX),
239             pageCount = this.get(TOTAL);
240             
241         if (velocity) {
242             if (inc && pageIndex < pageCount-1) {
243                 this.next();
244             } else if (!inc && pageIndex > 0) {
245                 this.prev();
246             }
247         }
249         return this._prevent;
250     },
251     
252     /**
253      * After host render handler
254      *
255      * @method _afterRender
256      * @param {Event.Facade}
257      * @protected
258      */
259     _afterRender: function(e) {
260         var host = this._host;
261         
262         host.get("boundingBox").addClass(host.getClassName("paged"));
263     },
264     
265     /**
266      * scrollEnd handler detects if a page needs to change
267      *
268      * @method _scrollEnded
269      * @param {Event.Facade}
270      * @protected
271      */
272      _scrollEnded: function(e) {
273          var host = this._host,
274              pageIndex = this.get(INDEX),
275              pageCount = this.get(TOTAL),
276              trans = PaginatorPlugin.SNAP_TO_CURRENT,
277              optimizeMemory = this.optimizeMemory;
278              
279          if(e.onGestureMoveEnd && !host._flicking) {
280              if(host._scrolledHalfway) {
281                  if(host._scrolledForward && pageIndex < pageCount-1) {
282                      this.next();
283                  } else if (pageIndex > 0) {
284                      this.prev();
285                  } else {
286                      this.snapToCurrent(trans.duration, trans.easing);
287                  }
288              } else {
289                  this.snapToCurrent(trans.duration, trans.easing);
290              }
291          }
292          
293          if (!e.onGestureMoveEnd){
294              if (optimizeMemory) {
295               this._manageDOM();
296              }
297              
298             this.set(PREVINDEX, pageIndex);
299          }
300      },
301      
302      /**
303       * Manages adding & removing slides from the DOM, to improve performance & memory usage
304       *
305       * @since 3.5.0
306       * @method _manageDOM
307       * @protected
308       */
309      _manageDOM: function(){
310          var newSlide, addSlideMethod, nodeToRemove, 
311              host = this._host,
312              cb = host.get(CONTENT_BOX),
313              currentIndex = this.get(INDEX),
314              previousIndex = this.get(PREVINDEX),
315              total = this.get(TOTAL),
316              isForward = (previousIndex < currentIndex) ? true : false,
317              cbChildren = cb.get('children'),
318              pageNodes = this._pageNodes,
319              targetOffset;
320             
321          if (pageNodes && pageNodes.size() > 0) {
322               if (isForward) {
323                   newSlide = pageNodes.item(currentIndex+1);
324                   addSlideMethod = cb.append;
325               }
326               else {
327                   newSlide = pageNodes.item(currentIndex-1);
328                   addSlideMethod = cb.prepend;
329               }
331               // Append/Prepend the new item to the DOM
332               if (cbChildren.indexOf(newSlide) === -1) {
333                   addSlideMethod.call(cb, newSlide);
334               }
336              // Since we modified the DOM, get an updated reference
337              cbChildren = cb.get('children');
338          }
339          
340          // Are we over the max number of items allowed?
341          if (cbChildren.size() > MAX_PAGE_COUNT) {
342              nodeToRemove = (isForward) ? cb.one('li:first-of-type') : cb.one('li:last-of-type');
343              nodeToRemove.remove();
344              targetOffset = (currentIndex == total ? 2 : 1);
345              host.set('scrollX', this._pageOffsets[targetOffset]); // Center
346          }
347      },
349     /**
350      * index attr change handler
351      *
352      * @method _afterIndexChange
353      * @param {Event.Facade}
354      * @protected
355      */
356     _afterIndexChange: function(e) {
357         if(e.src !== UI) {
358             this._uiIndex(e.newVal);
359         }
360     },
362     /**
363      * Update the UI based on the current page index
364      *
365      * @method _uiIndex
366      * @param index {Number}
367      * @protected
368      */
369     _uiIndex: function(index) {
370         this.scrollTo(index, 350, 'ease-out');
371     },
373     /**
374      * Scroll to the next page in the scrollview, with animation
375      *
376      * @method next
377      */
378     next: function() {
379         var index = this.get(INDEX);
380         if(index < this.get(TOTAL)-1) {
381             this.set(INDEX, index+1);
382         }
383     },
385     /**
386      * Scroll to the previous page in the scrollview, with animation
387      *
388      * @method prev
389      */
390     prev: function() {
391         var index = this.get(INDEX);
392         if(index > 0) {
393             this.set(INDEX, index-1);
394         }
395     },
397     /**
398      * Scroll to a given page in the scrollview, with animation.
399      *
400      * @method scrollTo
401      * @param index {Number} The index of the page to scroll to
402      * @param duration {Number} The number of ms the animation should last
403      * @param easing {String} The timing function to use in the animation
404      */
405     scrollTo: function(index, duration, easing) {
406         var host = this._host,
407             vert = host._scrollsVertical,
408             scrollAxis = (vert) ? SCROLL_Y : SCROLL_X, 
409             scrollVal = this._getTargetOffset(index);
411         host.set(scrollAxis, scrollVal, {
412             duration: duration,
413             easing: easing
414         });
415     },
417     /**
418      * Snaps the scrollview to the currently selected page
419      *
420      * @method snapToCurrent
421      * @param duration {Number} The number of ms the animation should last
422      * @param easing {String} The timing function to use in the animation
423      */
424     snapToCurrent: function(duration, easing) {
425         var host = this._host,
426             vert = host._scrollsVertical;
427             
428         host._killTimer();
429         
430         host.set((vert) ? SCROLL_Y : SCROLL_X, this._getTargetOffset(this.get(INDEX)), {
431             duration: duration,
432             easing: easing
433         });
434     },
436     _prevent: new Y.Do.Prevent()
441  * The default snap to current duration and easing values used on scroll end. 
442  * 
443  * @property SNAP_TO_CURRENT
444  * @static
445  */
446 PaginatorPlugin.SNAP_TO_CURRENT = {
447     duration : 300,
448     easing : 'ease-out'
451 Y.namespace('Plugin').ScrollViewPaginator = PaginatorPlugin;
454 }, '3.5.1' ,{requires:['plugin']});