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.ubind(this.uuid + ':nextPage', (function(playlist) {
 83     return function(event) {
 84       playlist.nextPage();
 85     };
 86   })(this));
 87   this.pager.ubind(this.uuid + ':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('player', function(player) {
 99         player.ubind(this.uuid + ':player_ended', (function(playlist) {
100           return function(event) {
101             player.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   // Destroy the scroll bar first.
152   if (this.scroll) {
153     this.scroll.scrollTo(0, 0);
154     this.scroll.destroy();
155     this.scroll = null;
156     this.elements.list
157         .unbind('mousemove')
158         .unbind('mouseenter')
159         .unbind('mouseleave');
160   }
161 
162   // Need to force the width of the list.
163   if (!this.options.vertical) {
164     var listSize = 0;
165     jQuery.each(this.elements.list.children(), function() {
166       listSize += jQuery(this).outerWidth();
167     });
168     this.elements.list.width(listSize);
169   }
170 
171   // Check to see if we should add a scroll bar functionality.
172   if ((list.length > 0) &&
173       (scroll.length > 0) &&
174       (list[this.orient.size]() > scroll[this.orient.size]())) {
175 
176     // Setup the iScroll component.
177     this.scroll = new iScroll(this.elements.scroll.eq(0)[0], {
178       hScroll: !this.options.vertical,
179       hScrollbar: !this.options.vertical,
180       vScroll: this.options.vertical,
181       vScrollbar: this.options.vertical,
182       hideScrollbar: (this.options.scrollMode !== 'none')
183     });
184 
185     // Use autoScroll for non-touch devices.
186     if ((this.options.scrollMode == 'auto') && !minplayer.hasTouch) {
187 
188       // Bind to the mouse events for autoscrolling.
189       this.elements.list.bind('mousemove', (function(playlist) {
190         return function(event) {
191           event.preventDefault();
192           var offset = playlist.display.offset()[playlist.orient.offset];
193           playlist.mousePos = event[playlist.orient.pagePos];
194           playlist.mousePos -= offset;
195         };
196       })(this)).bind('mouseenter', (function(playlist) {
197         return function(event) {
198           event.preventDefault();
199           playlist.scrolling = true;
200           var setScroll = function() {
201             if (playlist.scrolling) {
202               var scrollSize = playlist.scroll[playlist.orient.wrapperSize];
203               var scrollMid = (scrollSize / 2);
204               var delta = playlist.mousePos - scrollMid;
205               if (Math.abs(delta) > playlist.options.hysteresis) {
206                 var hyst = playlist.options.hysteresis;
207                 hyst *= (delta > 0) ? -1 : 0;
208                 delta = (playlist.options.scrollSpeed * (delta + hyst));
209                 delta /= scrollMid;
210                 var pos = playlist.scroll[playlist.orient.pos] - delta;
211                 var min = playlist.scroll[playlist.orient.minScroll] || 0;
212                 var max = playlist.scroll[playlist.orient.maxScroll];
213                 if (pos >= min) {
214                   playlist.scrollTo(min);
215                 }
216                 else if (pos <= max) {
217                   playlist.scrollTo(max);
218                 }
219                 else {
220                   playlist.scrollTo(delta, true);
221                 }
222               }
223 
224               // Set timeout to try again.
225               setTimeout(setScroll, 30);
226             }
227           };
228           setScroll();
229         };
230       })(this)).bind('mouseleave', (function(playlist) {
231         return function(event) {
232           event.preventDefault();
233           playlist.scrolling = false;
234         };
235       })(this));
236     }
237 
238     this.scroll.refresh();
239     this.scroll.scrollTo(0, 0, 200);
240   }
241 };
242 
243 /**
244  * Sets the playlist.
245  *
246  * @param {object} playlist The playlist object.
247  * @param {integer} loadIndex The index of the item to load.
248  */
249 osmplayer.playlist.prototype.set = function(playlist, loadIndex) {
250 
251   // Check to make sure the playlist is an object.
252   if (typeof playlist !== 'object') {
253     this.trigger('error', 'Playlist must be an object to set');
254     return;
255   }
256 
257   // Check to make sure the playlist has correct format.
258   if (!playlist.hasOwnProperty('total_rows')) {
259     this.trigger('error', 'Unknown playlist format.');
260     return;
261   }
262 
263   // Make sure the playlist has some rows.
264   if (playlist.total_rows && playlist.nodes.length) {
265 
266     // Set the total rows.
267     this.totalItems = playlist.total_rows;
268     this.currentItem = 0;
269 
270     // Show or hide the next page if there is or is not a next page.
271     if (((this.page + 1) * this.options.pageLimit) >= this.totalItems) {
272       this.pager.nextPage.hide();
273     }
274     else {
275       this.pager.nextPage.show();
276     }
277 
278     var teaser = null;
279     var numNodes = playlist.nodes.length;
280     this.elements.list.empty();
281     this.nodes = [];
282 
283     // Iterate through all the nodes.
284     for (var index = 0; index < numNodes; index++) {
285 
286       // Create the teaser object.
287       teaser = this.create('teaser', 'osmplayer', this.elements.list);
288       teaser.setNode(playlist.nodes[index]);
289       teaser.ubind(this.uuid + ':nodeLoad', (function(playlist, index) {
290         return function(event, data) {
291           playlist.loadItem(index);
292         };
293       })(this, index));
294 
295       // Add this to our nodes array.
296       this.nodes.push(teaser);
297 
298       // If the index is equal to the loadIndex.
299       if (loadIndex === index) {
300         this.loadItem(index);
301       }
302     }
303 
304     // Refresh the sizes.
305     this.refreshScroll();
306 
307     // Trigger that the playlist has loaded.
308     this.trigger('playlistLoad', playlist);
309   }
310 
311   // Show that we are no longer busy.
312   if (this.elements.playlist_busy) {
313     this.elements.playlist_busy.hide();
314   }
315 };
316 
317 /**
318  * Stores the current playlist state in the playqueue.
319  */
320 osmplayer.playlist.prototype.setQueue = function() {
321 
322   // Add this item to the playqueue.
323   this.playqueue.push({
324     page: this.page,
325     item: this.currentItem
326   });
327 
328   // Store the current playqueue position.
329   this.playqueuepos = this.playqueue.length;
330 };
331 
332 /**
333  * Loads the next item.
334  *
335  * @return {boolean} TRUE if loaded, FALSE if not.
336  */
337 osmplayer.playlist.prototype.next = function() {
338   var item = 0, page = this.page;
339 
340   // See if we are at the front of the playqueue.
341   if (this.playqueuepos >= this.playqueue.length) {
342 
343     // If this is shuffle, then load a random item.
344     if (this.options.shuffle) {
345       item = Math.floor(Math.random() * this.totalItems);
346       page = Math.floor(item / this.options.pageLimit);
347       item = item % this.options.pageLimit;
348       return this.load(page, item);
349     }
350     else {
351 
352       // Otherwise, increment the current item by one.
353       item = (this.currentItem + 1);
354       if (item >= this.nodes.length) {
355         return this.load(page + 1, 0);
356       }
357       else {
358         return this.loadItem(item);
359       }
360     }
361   }
362   else {
363 
364     // Load the next item in the playqueue.
365     this.playqueuepos = this.playqueuepos + 1;
366     var currentQueue = this.playqueue[this.playqueuepos];
367     return this.load(currentQueue.page, currentQueue.item);
368   }
369 };
370 
371 /**
372  * Loads the previous item.
373  *
374  * @return {boolean} TRUE if loaded, FALSE if not.
375  */
376 osmplayer.playlist.prototype.prev = function() {
377 
378   // Move back into the playqueue.
379   this.playqueuepos = this.playqueuepos - 1;
380   this.playqueuepos = (this.playqueuepos < 0) ? 0 : this.playqueuepos;
381   var currentQueue = this.playqueue[this.playqueuepos];
382   if (currentQueue) {
383     return this.load(currentQueue.page, currentQueue.item);
384   }
385   return false;
386 };
387 
388 /**
389  * Loads a playlist node.
390  *
391  * @param {number} index The index of the item you would like to load.
392  * @return {boolean} TRUE if loaded, FALSE if not.
393  */
394 osmplayer.playlist.prototype.loadItem = function(index) {
395   if (index < this.nodes.length) {
396     this.setQueue();
397 
398     // Get the teaser at the current index and deselect it.
399     var teaser = this.nodes[this.currentItem];
400     teaser.select(false);
401     this.currentItem = index;
402 
403     // Get the new teaser and select it.
404     teaser = this.nodes[index];
405     teaser.select(true);
406     this.trigger('nodeLoad', teaser.node);
407     return true;
408   }
409 
410   return false;
411 };
412 
413 /**
414  * Loads the next page.
415  *
416  * @param {integer} loadIndex The index of the item to load.
417  * @return {boolean} TRUE if loaded, FALSE if not.
418  */
419 osmplayer.playlist.prototype.nextPage = function(loadIndex) {
420   return this.load(this.page + 1, loadIndex);
421 };
422 
423 /**
424  * Loads the previous page.
425  *
426  * @param {integer} loadIndex The index of the item to load.
427  * @return {boolean} TRUE if loaded, FALSE if not.
428  */
429 osmplayer.playlist.prototype.prevPage = function(loadIndex) {
430   return this.load(this.page - 1, loadIndex);
431 };
432 
433 /**
434  * Loads a playlist.
435  *
436  * @param {integer} page The page to load.
437  * @param {integer} loadIndex The index of the item to load.
438  * @return {boolean} TRUE if loaded, FALSE if not.
439  */
440 osmplayer.playlist.prototype.load = function(page, loadIndex) {
441 
442   // If the playlist and pages are the same, then no need to load.
443   if ((this.playlist == this.options.playlist) && (page == this.page)) {
444     return this.loadItem(loadIndex);
445   }
446 
447   // Set the new playlist.
448   this.playlist = this.options.playlist;
449 
450   // Return if there aren't any playlists to play.
451   if (!this.playlist) {
452     return false;
453   }
454 
455   // Determine if we need to loop.
456   var maxPages = Math.floor(this.totalItems / this.options.pageLimit);
457   if (page > maxPages) {
458     if (this.options.loop) {
459       page = 0;
460       loadIndex = 0;
461     }
462     else {
463       return false;
464     }
465   }
466 
467   // Say that we are busy.
468   if (this.elements.playlist_busy) {
469     this.elements.playlist_busy.show();
470   }
471 
472   // Normalize the page.
473   page = page || 0;
474   page = (page < 0) ? 0 : page;
475 
476   // Set the queue.
477   this.setQueue();
478 
479   // Set the new page.
480   this.page = page;
481 
482   // Hide or show the page based on if we are on the first page.
483   if (this.page == 0) {
484     this.pager.prevPage.hide();
485   }
486   else {
487     this.pager.prevPage.show();
488   }
489 
490   // If the playlist is an object, then go ahead and set it.
491   if (typeof this.playlist == 'object') {
492     this.set(this.playlist, loadIndex);
493     if (this.playlist.endpoint) {
494       this.playlist = this.options.playlist = this.playlist.endpoint;
495     }
496     return true;
497   }
498 
499   // Get the highest priority parser.
500   var parser = osmplayer.parser['default'];
501   for (var name in osmplayer.parser) {
502     if (osmplayer.parser.hasOwnProperty(name)) {
503       if (osmplayer.parser[name].valid(this.playlist)) {
504         if (osmplayer.parser[name].priority > parser.priority) {
505           parser = osmplayer.parser[name];
506         }
507       }
508     }
509   }
510 
511   // The start index.
512   var start = this.page * this.options.pageLimit;
513 
514   // Get the feed from the parser.
515   var feed = parser.getFeed(
516     this.playlist,
517     start,
518     this.options.pageLimit
519   );
520 
521   // Build our request.
522   var request = {
523     type: 'GET',
524     url: feed,
525     success: (function(playlist) {
526       return function(data) {
527         playlist.set(parser.parse(data), loadIndex);
528       };
529     })(this),
530     error: (function(playlist) {
531       return function(XMLHttpRequest, textStatus, errorThrown) {
532         if (playlist.elements.playlist_busy) {
533           playlist.elements.playlist_busy.hide();
534         }
535         playlist.trigger('error', textStatus);
536       }
537     })(this)
538   };
539 
540   // Set the data if applicable.
541   var dataType = '';
542   if (dataType = parser.getType()) {
543     request.dataType = dataType;
544   }
545 
546   // Perform an ajax callback.
547   jQuery.ajax(request);
548 
549   // Return that we did something.
550   return true;
551 };
552