1 /** The osmplayer namespace. */
  2 var osmplayer = osmplayer || {};
  3 
  4 /**
  5  * @constructor
  6  * @extends minplayer.display
  7  * @class This class creates the playlist functionality for the minplayer.
  8  *
  9  * @param {object} context The jQuery context.
 10  * @param {object} options This components options.
 11  */
 12 osmplayer.playlist = function(context, options) {
 13 
 14   // Derive from display
 15   minplayer.display.call(this, 'playlist', context, options);
 16 };
 17 
 18 /** Derive from minplayer.display. */
 19 osmplayer.playlist.prototype = new minplayer.display();
 20 
 21 /** Reset the constructor. */
 22 osmplayer.playlist.prototype.constructor = osmplayer.playlist;
 23 
 24 /**
 25  * @see minplayer.plugin#construct
 26  */
 27 osmplayer.playlist.prototype.construct = function() {
 28 
 29   // Make sure we provide default options...
 30   this.options = jQuery.extend({
 31     vertical: true,
 32     playlist: '',
 33     pageLimit: 10,
 34     autoNext: true,
 35     shuffle: false,
 36     loop: false,
 37     hysteresis: 40,
 38     scrollSpeed: 20,
 39     scrollMode: 'auto'
 40   }, this.options);
 41 
 42   // Call the minplayer plugin constructor.
 43   minplayer.display.prototype.construct.call(this);
 44 
 45   /** The nodes within this playlist. */
 46   this.nodes = [];
 47 
 48   // Current page.
 49   this.page = -1;
 50 
 51   // The total amount of nodes.
 52   this.totalItems = 0;
 53 
 54   // The current loaded item index.
 55   this.currentItem = -1;
 56 
 57   // The play playqueue.
 58   this.playqueue = [];
 59 
 60   // The playqueue position.
 61   this.playqueuepos = 0;
 62 
 63   // The current playlist.
 64   this.playlist = this.options.playlist;
 65 
 66   // Create the scroll bar.
 67   this.scroll = null;
 68 
 69   // Create our orientation variable.
 70   this.orient = {
 71     pos: this.options.vertical ? 'y' : 'x',
 72     pagePos: this.options.vertical ? 'pageY' : 'pageX',
 73     offset: this.options.vertical ? 'top' : 'left',
 74     wrapperSize: this.options.vertical ? 'wrapperH' : 'wrapperW',
 75     minScroll: this.options.vertical ? 'minScrollY' : 'minScrollX',
 76     maxScroll: this.options.vertical ? 'maxScrollY' : 'maxScrollX',
 77     size: this.options.vertical ? 'height' : 'width'
 78   };
 79 
 80   // Create the pager.
 81   this.pager = this.create('pager', 'osmplayer');
 82   this.pager.bind('nextPage', (function(playlist) {
 83     return function(event) {
 84       playlist.nextPage();
 85     };
 86   })(this));
 87   this.pager.bind('prevPage', (function(playlist) {
 88     return function(event) {
 89       playlist.prevPage();
 90     };
 91   })(this));
 92 
 93   // Load the "next" item.
 94   if (this.next()) {
 95 
 96     // Get the media.
 97     if (this.options.autoNext) {
 98       this.get('media', function(media) {
 99         media.bind('ended', (function(playlist) {
100           return function(event) {
101             media.options.autoplay = true;
102             playlist.next();
103           };
104         })(this));
105       });
106     }
107   }
108 
109   // Say that we are ready.
110   this.ready();
111 };
112 
113 /**
114  * Wrapper around the scroll scrollTo method.
115  *
116  * @param {number} pos The position you would like to set the list.
117  * @param {boolean} relative If this is a relative position change.
118  */
119 osmplayer.playlist.prototype.scrollTo = function(pos, relative) {
120   if (this.scroll) {
121     this.scroll.options.hideScrollbar = false;
122     if (this.options.vertical) {
123       this.scroll.scrollTo(0, pos, 0, relative);
124     }
125     else {
126       this.scroll.scrollTo(pos, 0, 0, relative);
127     }
128     this.scroll.options.hideScrollbar = true;
129   }
130 };
131 
132 /**
133  * Refresh the scrollbar.
134  */
135 osmplayer.playlist.prototype.refreshScroll = function() {
136 
137   // Make sure that our window has the addEventListener to keep IE happy.
138   if (!window.addEventListener) {
139     setTimeout((function(playlist) {
140       return function() {
141         playlist.refreshScroll.call(playlist);
142       }
143     })(this), 200);
144     return;
145   }
146 
147   // Check the size of the playlist.
148   var list = this.elements.list;
149   var scroll = this.elements.scroll;
150 
151   // Check to see if we should add a scroll bar functionality.
152   if ((!this.scroll) &&
153       (list.length > 0) &&
154       (scroll.length > 0) &&
155       (list[this.orient.size]() > scroll[this.orient.size]())) {
156 
157     // Setup the iScroll component.
158     this.scroll = new iScroll(this.elements.scroll.eq(0)[0], {
159       hScroll: !this.options.vertical,
160       hScrollbar: !this.options.vertical,
161       vScroll: this.options.vertical,
162       vScrollbar: this.options.vertical,
163       hideScrollbar: true
164     });
165 
166     // Use autoScroll for non-touch devices.
167     if ((this.options.scrollMode == 'auto') && !minplayer.hasTouch) {
168 
169       // Bind to the mouse events for autoscrolling.
170       this.elements.list.bind('mousemove', (function(playlist) {
171         return function(event) {
172           event.preventDefault();
173           var offset = playlist.display.offset()[playlist.orient.offset];
174           playlist.mousePos = event[playlist.orient.pagePos];
175           playlist.mousePos -= offset;
176         };
177       })(this)).bind('mouseenter', (function(playlist) {
178         return function(event) {
179           event.preventDefault();
180           playlist.scrolling = true;
181           var setScroll = function() {
182             if (playlist.scrolling) {
183               var scrollSize = playlist.scroll[playlist.orient.wrapperSize];
184               var scrollMid = (scrollSize / 2);
185               var delta = playlist.mousePos - scrollMid;
186               if (Math.abs(delta) > playlist.options.hysteresis) {
187                 var hyst = playlist.options.hysteresis;
188                 hyst *= (delta > 0) ? -1 : 0;
189                 delta = (playlist.options.scrollSpeed * (delta + hyst));
190                 delta /= scrollMid;
191                 var pos = playlist.scroll[playlist.orient.pos] - delta;
192                 var min = playlist.scroll[playlist.orient.minScroll] || 0;
193                 var max = playlist.scroll[playlist.orient.maxScroll];
194                 if (pos >= min) {
195                   playlist.scrollTo(min);
196                 }
197                 else if (pos <= max) {
198                   playlist.scrollTo(max);
199                 }
200                 else {
201                   playlist.scrollTo(delta, true);
202                 }
203               }
204 
205               // Set timeout to try again.
206               setTimeout(setScroll, 30);
207             }
208           };
209           setScroll();
210         };
211       })(this)).bind('mouseleave', (function(playlist) {
212         return function(event) {
213           event.preventDefault();
214           playlist.scrolling = false;
215         };
216       })(this));
217     }
218 
219     // Need to force the width of the list.
220     if (!this.options.vertical) {
221       var listSize = 0;
222       jQuery.each(this.elements.list.children(), function() {
223         listSize += jQuery(this).outerWidth();
224       });
225       this.elements.list.width(listSize);
226     }
227 
228     this.scroll.refresh();
229     this.scroll.scrollTo(0, 0, 200);
230   }
231   else if (this.scroll) {
232 
233     // Disable the scroll bar.
234     this.scroll.disable();
235     this.elements.list
236       .unbind('mousemove')
237       .unbind('mouseenter')
238       .unbind('mouseleave');
239   }
240 };
241 
242 /**
243  * Sets the playlist.
244  *
245  * @param {object} playlist The playlist object.
246  * @param {integer} loadIndex The index of the item to load.
247  */
248 osmplayer.playlist.prototype.set = function(playlist, loadIndex) {
249 
250   // Check to make sure the playlist is an object.
251   if (typeof playlist !== 'object') {
252     this.trigger('error', 'Playlist must be an object to set');
253     return;
254   }
255 
256   // Check to make sure the playlist has correct format.
257   if (!playlist.hasOwnProperty('total_rows')) {
258     this.trigger('error', 'Unknown playlist format.');
259     return;
260   }
261 
262   // Make sure the playlist has some rows.
263   if (playlist.total_rows && playlist.nodes.length) {
264 
265     // Set the total rows.
266     this.totalItems = playlist.total_rows;
267     this.currentItem = 0;
268 
269     // Show or hide the next page if there is or is not a next page.
270     if (((this.page + 1) * this.options.pageLimit) >= this.totalItems) {
271       this.pager.nextPage.hide();
272     }
273     else {
274       this.pager.nextPage.show();
275     }
276 
277     var teaser = null;
278     var numNodes = playlist.nodes.length;
279     this.elements.list.empty();
280     this.nodes = [];
281 
282     // Iterate through all the nodes.
283     for (var index = 0; index < numNodes; index++) {
284 
285       // Create the teaser object.
286       teaser = this.create('teaser', 'osmplayer', this.elements.list);
287       teaser.setNode(playlist.nodes[index]);
288       teaser.bind('nodeLoad', (function(playlist, index) {
289         return function(event, data) {
290           playlist.loadItem(index);
291         };
292       })(this, index));
293 
294       // Add this to our nodes array.
295       this.nodes.push(teaser);
296 
297       // If the index is equal to the loadIndex.
298       if (loadIndex === index) {
299         this.loadItem(index);
300       }
301     }
302 
303     // Refresh the sizes.
304     this.refreshScroll();
305 
306     // Trigger that the playlist has loaded.
307     this.trigger('playlistLoad', playlist);
308   }
309 
310   // Show that we are no longer busy.
311   if (this.elements.playlist_busy) {
312     this.elements.playlist_busy.hide();
313   }
314 };
315 
316 /**
317  * Stores the current playlist state in the playqueue.
318  */
319 osmplayer.playlist.prototype.setQueue = function() {
320 
321   // Add this item to the playqueue.
322   this.playqueue.push({
323     page: this.page,
324     item: this.currentItem
325   });
326 
327   // Store the current playqueue position.
328   this.playqueuepos = this.playqueue.length;
329 };
330 
331 /**
332  * Loads the next item.
333  *
334  * @return {boolean} TRUE if loaded, FALSE if not.
335  */
336 osmplayer.playlist.prototype.next = function() {
337   var item = 0, page = this.page;
338 
339   // See if we are at the front of the playqueue.
340   if (this.playqueuepos >= this.playqueue.length) {
341 
342     // If this is shuffle, then load a random item.
343     if (this.options.shuffle) {
344       item = Math.floor(Math.random() * this.totalItems);
345       page = Math.floor(item / this.options.pageLimit);
346       item = item % this.options.pageLimit;
347       return this.load(page, item);
348     }
349     else {
350 
351       // Otherwise, increment the current item by one.
352       item = (this.currentItem + 1);
353       if (item >= this.nodes.length) {
354         return this.load(page + 1, 0);
355       }
356       else {
357         return this.loadItem(item);
358       }
359     }
360   }
361   else {
362 
363     // Load the next item in the playqueue.
364     this.playqueuepos = this.playqueuepos + 1;
365     var currentQueue = this.playqueue[this.playqueuepos];
366     return this.load(currentQueue.page, currentQueue.item);
367   }
368 };
369 
370 /**
371  * Loads the previous item.
372  *
373  * @return {boolean} TRUE if loaded, FALSE if not.
374  */
375 osmplayer.playlist.prototype.prev = function() {
376 
377   // Move back into the playqueue.
378   this.playqueuepos = this.playqueuepos - 1;
379   this.playqueuepos = (this.playqueuepos < 0) ? 0 : this.playqueuepos;
380   var currentQueue = this.playqueue[this.playqueuepos];
381   if (currentQueue) {
382     return this.load(currentQueue.page, currentQueue.item);
383   }
384   return false;
385 };
386 
387 /**
388  * Loads a playlist node.
389  *
390  * @param {number} index The index of the item you would like to load.
391  * @return {boolean} TRUE if loaded, FALSE if not.
392  */
393 osmplayer.playlist.prototype.loadItem = function(index) {
394   if (index < this.nodes.length) {
395     this.setQueue();
396 
397     // Get the teaser at the current index and deselect it.
398     var teaser = this.nodes[this.currentItem];
399     teaser.select(false);
400     this.currentItem = index;
401 
402     // Get the new teaser and select it.
403     teaser = this.nodes[index];
404     teaser.select(true);
405     this.trigger('nodeLoad', teaser.node);
406     return true;
407   }
408 
409   return false;
410 };
411 
412 /**
413  * Loads the next page.
414  *
415  * @param {integer} loadIndex The index of the item to load.
416  * @return {boolean} TRUE if loaded, FALSE if not.
417  */
418 osmplayer.playlist.prototype.nextPage = function(loadIndex) {
419   return this.load(this.page + 1, loadIndex);
420 };
421 
422 /**
423  * Loads the previous page.
424  *
425  * @param {integer} loadIndex The index of the item to load.
426  * @return {boolean} TRUE if loaded, FALSE if not.
427  */
428 osmplayer.playlist.prototype.prevPage = function(loadIndex) {
429   return this.load(this.page - 1, loadIndex);
430 };
431 
432 /**
433  * Loads a playlist.
434  *
435  * @param {integer} page The page to load.
436  * @param {integer} loadIndex The index of the item to load.
437  * @return {boolean} TRUE if loaded, FALSE if not.
438  */
439 osmplayer.playlist.prototype.load = function(page, loadIndex) {
440 
441   // If the playlist and pages are the same, then no need to load.
442   if ((this.playlist == this.options.playlist) && (page == this.page)) {
443     return this.loadItem(loadIndex);
444   }
445 
446   // Set the new playlist.
447   this.playlist = this.options.playlist;
448 
449   // Return if there aren't any playlists to play.
450   if (!this.playlist) {
451     return false;
452   }
453 
454   // Determine if we need to loop.
455   var maxPages = Math.floor(this.totalItems / this.options.pageLimit);
456   if (page > maxPages) {
457     if (this.options.loop) {
458       page = 0;
459       loadIndex = 0;
460     }
461     else {
462       return false;
463     }
464   }
465 
466   // Say that we are busy.
467   if (this.elements.playlist_busy) {
468     this.elements.playlist_busy.show();
469   }
470 
471   // Normalize the page.
472   page = page || 0;
473   page = (page < 0) ? 0 : page;
474 
475   // Set the queue.
476   this.setQueue();
477 
478   // Set the new page.
479   this.page = page;
480 
481   // Hide or show the page based on if we are on the first page.
482   if (this.page == 0) {
483     this.pager.prevPage.hide();
484   }
485   else {
486     this.pager.prevPage.show();
487   }
488 
489   // If the playlist is an object, then go ahead and set it.
490   if (typeof this.playlist == 'object') {
491     this.set(this.playlist, loadIndex);
492     if (this.playlist.endpoint) {
493       this.playlist = this.options.playlist = this.playlist.endpoint;
494     }
495     return true;
496   }
497 
498   // Get the highest priority parser.
499   var parser = osmplayer.parser['default'];
500   for (var name in osmplayer.parser) {
501     if (osmplayer.parser.hasOwnProperty(name)) {
502       if (osmplayer.parser[name].valid(this.playlist)) {
503         if (osmplayer.parser[name].priority > parser.priority) {
504           parser = osmplayer.parser[name];
505         }
506       }
507     }
508   }
509 
510   // The start index.
511   var start = this.page * this.options.pageLimit;
512 
513   // Get the feed from the parser.
514   var feed = parser.getFeed(
515     this.playlist,
516     start,
517     this.options.pageLimit
518   );
519 
520   // Build our request.
521   var request = {
522     type: 'GET',
523     url: feed,
524     success: (function(playlist) {
525       return function(data) {
526         playlist.set(parser.parse(data), loadIndex);
527       };
528     })(this),
529     error: (function(playlist) {
530       return function(XMLHttpRequest, textStatus, errorThrown) {
531         if (playlist.elements.playlist_busy) {
532           playlist.elements.playlist_busy.hide();
533         }
534         playlist.trigger('error', textStatus);
535       }
536     })(this)
537   };
538 
539   // Set the data if applicable.
540   var dataType = '';
541   if (dataType = parser.getType()) {
542     request.dataType = dataType;
543   }
544 
545   // Perform an ajax callback.
546   jQuery.ajax(request);
547 
548   // Return that we did something.
549   return true;
550 };
551