1/*
2 * Playlist Object for the jPlayer Plugin
3 * http://www.jplayer.org
4 *
5 * Copyright (c) 2009 - 2014 Happyworm Ltd
6 * Licensed under the MIT license.
7 * http://www.opensource.org/licenses/MIT
8 *
9 * Author: Mark J Panaghiston
10 * Version: 2.4.1
11 * Date: 19th November 2014
12 *
13 * Requires:
14 *  - jQuery 1.7.0+
15 *  - jPlayer 2.8.2+
16 */
17
18/*global jPlayerPlaylist:true */
19
20(function($, undefined) {
21
22	jPlayerPlaylist = function(cssSelector, playlist, options) {
23		var self = this;
24
25		this.current = 0;
26		this.loop = false; // Flag used with the jPlayer repeat event
27		this.shuffled = false;
28		this.removing = false; // Flag is true during remove animation, disabling the remove() method until complete.
29
30		this.cssSelector = $.extend({}, this._cssSelector, cssSelector); // Object: Containing the css selectors for jPlayer and its cssSelectorAncestor
31		this.options = $.extend(true, {
32			keyBindings: {
33				next: {
34					key: 221, // ]
35					fn: function() {
36						self.next();
37					}
38				},
39				previous: {
40					key: 219, // [
41					fn: function() {
42						self.previous();
43					}
44				},
45				shuffle: {
46					key: 83, // s
47					fn: function() {
48						self.shuffle();
49					}
50				}
51			},
52			stateClass: {
53				shuffled: "jp-state-shuffled"
54			}
55		}, this._options, options); // Object: The jPlayer constructor options for this playlist and the playlist options
56
57		this.playlist = []; // Array of Objects: The current playlist displayed (Un-shuffled or Shuffled)
58		this.original = []; // Array of Objects: The original playlist
59
60		this._initPlaylist(playlist); // Copies playlist to this.original. Then mirrors this.original to this.playlist. Creating two arrays, where the element pointers match. (Enables pointer comparison.)
61
62		// Setup the css selectors for the extra interface items used by the playlist.
63		this.cssSelector.details = this.cssSelector.cssSelectorAncestor + " .jp-details"; // Note that jPlayer controls the text in the title element.
64		this.cssSelector.playlist = this.cssSelector.cssSelectorAncestor + " .jp-playlist";
65		this.cssSelector.next = this.cssSelector.cssSelectorAncestor + " .jp-next";
66		this.cssSelector.previous = this.cssSelector.cssSelectorAncestor + " .jp-previous";
67		this.cssSelector.shuffle = this.cssSelector.cssSelectorAncestor + " .jp-shuffle";
68		this.cssSelector.shuffleOff = this.cssSelector.cssSelectorAncestor + " .jp-shuffle-off";
69
70		// Override the cssSelectorAncestor given in options
71		this.options.cssSelectorAncestor = this.cssSelector.cssSelectorAncestor;
72
73		// Override the default repeat event handler
74		this.options.repeat = function(event) {
75			self.loop = event.jPlayer.options.loop;
76		};
77
78		// Create a ready event handler to initialize the playlist
79		$(this.cssSelector.jPlayer).bind($.jPlayer.event.ready, function() {
80			self._init();
81		});
82
83		// Create an ended event handler to move to the next item
84		$(this.cssSelector.jPlayer).bind($.jPlayer.event.ended, function() {
85			self.next();
86		});
87
88		// Create a play event handler to pause other instances
89		$(this.cssSelector.jPlayer).bind($.jPlayer.event.play, function() {
90			$(this).jPlayer("pauseOthers");
91		});
92
93		// Create a resize event handler to show the title in full screen mode.
94		$(this.cssSelector.jPlayer).bind($.jPlayer.event.resize, function(event) {
95			if(event.jPlayer.options.fullScreen) {
96				$(self.cssSelector.details).show();
97			} else {
98				$(self.cssSelector.details).hide();
99			}
100		});
101
102		// Create click handlers for the extra buttons that do playlist functions.
103		$(this.cssSelector.previous).click(function(e) {
104			e.preventDefault();
105			self.previous();
106			self.blur(this);
107		});
108
109		$(this.cssSelector.next).click(function(e) {
110			e.preventDefault();
111			self.next();
112			self.blur(this);
113		});
114
115		$(this.cssSelector.shuffle).click(function(e) {
116			e.preventDefault();
117			if(self.shuffled && $(self.cssSelector.jPlayer).jPlayer("option", "useStateClassSkin")) {
118				self.shuffle(false);
119			} else {
120				self.shuffle(true);
121			}
122			self.blur(this);
123		});
124		$(this.cssSelector.shuffleOff).click(function(e) {
125			e.preventDefault();
126			self.shuffle(false);
127			self.blur(this);
128		}).hide();
129
130		// Put the title in its initial display state
131		if(!this.options.fullScreen) {
132			$(this.cssSelector.details).hide();
133		}
134
135		// Remove the empty <li> from the page HTML. Allows page to be valid HTML, while not interfereing with display animations
136		$(this.cssSelector.playlist + " ul").empty();
137
138		// Create .on() handlers for the playlist items along with the free media and remove controls.
139		this._createItemHandlers();
140
141		// Instance jPlayer
142		$(this.cssSelector.jPlayer).jPlayer(this.options);
143	};
144
145	jPlayerPlaylist.prototype = {
146		_cssSelector: { // static object, instanced in constructor
147			jPlayer: "#jquery_jplayer_1",
148			cssSelectorAncestor: "#jp_container_1"
149		},
150		_options: { // static object, instanced in constructor
151			playlistOptions: {
152				autoPlay: false,
153				loopOnPrevious: false,
154				shuffleOnLoop: true,
155				enableRemoveControls: false,
156				displayTime: 'slow',
157				addTime: 'fast',
158				removeTime: 'fast',
159				shuffleTime: 'slow',
160				itemClass: "jp-playlist-item",
161				freeGroupClass: "jp-free-media",
162				freeItemClass: "jp-playlist-item-free",
163				removeItemClass: "jp-playlist-item-remove"
164			}
165		},
166		option: function(option, value) { // For changing playlist options only
167			if(value === undefined) {
168				return this.options.playlistOptions[option];
169			}
170
171			this.options.playlistOptions[option] = value;
172
173			switch(option) {
174				case "enableRemoveControls":
175					this._updateControls();
176					break;
177				case "itemClass":
178				case "freeGroupClass":
179				case "freeItemClass":
180				case "removeItemClass":
181					this._refresh(true); // Instant
182					this._createItemHandlers();
183					break;
184			}
185			return this;
186		},
187		_init: function() {
188			var self = this;
189			this._refresh(function() {
190				if(self.options.playlistOptions.autoPlay) {
191					self.play(self.current);
192				} else {
193					self.select(self.current);
194				}
195			});
196		},
197		_initPlaylist: function(playlist) {
198			this.current = 0;
199			this.shuffled = false;
200			this.removing = false;
201			this.original = $.extend(true, [], playlist); // Copy the Array of Objects
202			this._originalPlaylist();
203		},
204		_originalPlaylist: function() {
205			var self = this;
206			this.playlist = [];
207			// Make both arrays point to the same object elements. Gives us 2 different arrays, each pointing to the same actual object. ie., Not copies of the object.
208			$.each(this.original, function(i) {
209				self.playlist[i] = self.original[i];
210			});
211		},
212		_refresh: function(instant) {
213			/* instant: Can be undefined, true or a function.
214			 *	undefined -> use animation timings
215			 *	true -> no animation
216			 *	function -> use animation timings and excute function at half way point.
217			 */
218			var self = this;
219
220			if(instant && !$.isFunction(instant)) {
221				$(this.cssSelector.playlist + " ul").empty();
222				$.each(this.playlist, function(i) {
223					$(self.cssSelector.playlist + " ul").append(self._createListItem(self.playlist[i]));
224				});
225				this._updateControls();
226			} else {
227				var displayTime = $(this.cssSelector.playlist + " ul").children().length ? this.options.playlistOptions.displayTime : 0;
228
229				$(this.cssSelector.playlist + " ul").slideUp(displayTime, function() {
230					var $this = $(this);
231					$(this).empty();
232
233					$.each(self.playlist, function(i) {
234						$this.append(self._createListItem(self.playlist[i]));
235					});
236					self._updateControls();
237					if($.isFunction(instant)) {
238						instant();
239					}
240					if(self.playlist.length) {
241						$(this).slideDown(self.options.playlistOptions.displayTime);
242					} else {
243						$(this).show();
244					}
245				});
246			}
247		},
248		_createListItem: function(media) {
249			var self = this;
250
251			// Wrap the <li> contents in a <div>
252			var listItem = "<li><div>";
253
254			// Create remove control
255			listItem += "<a href='javascript:;' class='" + this.options.playlistOptions.removeItemClass + "'>&times;</a>";
256
257			// Create links to free media
258			if(media.free) {
259				var first = true;
260				listItem += "<span class='" + this.options.playlistOptions.freeGroupClass + "'>(";
261				$.each(media, function(property,value) {
262					if($.jPlayer.prototype.format[property]) { // Check property is a media format.
263						if(first) {
264							first = false;
265						} else {
266							listItem += " | ";
267						}
268						listItem += "<a class='" + self.options.playlistOptions.freeItemClass + "' href='" + value + "' tabindex='-1'>" + property + "</a>";
269					}
270				});
271				listItem += ")</span>";
272			}
273
274			// The title is given next in the HTML otherwise the float:right on the free media corrupts in IE6/7
275			listItem += "<a href='javascript:;' class='" + this.options.playlistOptions.itemClass + "' tabindex='0'>" + media.title + (media.artist ? " <span class='jp-artist'>by " + media.artist + "</span>" : "") + "</a>";
276			listItem += "</div></li>";
277
278			return listItem;
279		},
280		_createItemHandlers: function() {
281			var self = this;
282			// Create live handlers for the playlist items
283			$(this.cssSelector.playlist).off("click", "a." + this.options.playlistOptions.itemClass).on("click", "a." + this.options.playlistOptions.itemClass, function(e) {
284				e.preventDefault();
285				var index = $(this).parent().parent().index();
286				if(self.current !== index) {
287					self.play(index);
288				} else {
289					$(self.cssSelector.jPlayer).jPlayer("play");
290				}
291				self.blur(this);
292			});
293
294			// Create live handlers that disable free media links to force access via right click
295			$(this.cssSelector.playlist).off("click", "a." + this.options.playlistOptions.freeItemClass).on("click", "a." + this.options.playlistOptions.freeItemClass, function(e) {
296				e.preventDefault();
297				$(this).parent().parent().find("." + self.options.playlistOptions.itemClass).click();
298				self.blur(this);
299			});
300
301			// Create live handlers for the remove controls
302			$(this.cssSelector.playlist).off("click", "a." + this.options.playlistOptions.removeItemClass).on("click", "a." + this.options.playlistOptions.removeItemClass, function(e) {
303				e.preventDefault();
304				var index = $(this).parent().parent().index();
305				self.remove(index);
306				self.blur(this);
307			});
308		},
309		_updateControls: function() {
310			if(this.options.playlistOptions.enableRemoveControls) {
311				$(this.cssSelector.playlist + " ." + this.options.playlistOptions.removeItemClass).show();
312			} else {
313				$(this.cssSelector.playlist + " ." + this.options.playlistOptions.removeItemClass).hide();
314			}
315
316			if(this.shuffled) {
317				$(this.cssSelector.jPlayer).jPlayer("addStateClass", "shuffled");
318			} else {
319				$(this.cssSelector.jPlayer).jPlayer("removeStateClass", "shuffled");
320			}
321			if($(this.cssSelector.shuffle).length && $(this.cssSelector.shuffleOff).length) {
322				if(this.shuffled) {
323					$(this.cssSelector.shuffleOff).show();
324					$(this.cssSelector.shuffle).hide();
325				} else {
326					$(this.cssSelector.shuffleOff).hide();
327					$(this.cssSelector.shuffle).show();
328				}
329			}
330		},
331		_highlight: function(index) {
332			if(this.playlist.length && index !== undefined) {
333				$(this.cssSelector.playlist + " .jp-playlist-current").removeClass("jp-playlist-current");
334				$(this.cssSelector.playlist + " li:nth-child(" + (index + 1) + ")").addClass("jp-playlist-current").find(".jp-playlist-item").addClass("jp-playlist-current");
335				// $(this.cssSelector.details + " li").html("<span class='jp-title'>" + this.playlist[index].title + "</span>" + (this.playlist[index].artist ? " <span class='jp-artist'>by " + this.playlist[index].artist + "</span>" : ""));
336			}
337		},
338		setPlaylist: function(playlist) {
339			this._initPlaylist(playlist);
340			this._init();
341		},
342		add: function(media, playNow) {
343			$(this.cssSelector.playlist + " ul").append(this._createListItem(media)).find("li:last-child").hide().slideDown(this.options.playlistOptions.addTime);
344			this._updateControls();
345			this.original.push(media);
346			this.playlist.push(media); // Both array elements share the same object pointer. Comforms with _initPlaylist(p) system.
347
348			if(playNow) {
349				this.play(this.playlist.length - 1);
350			} else {
351				if(this.original.length === 1) {
352					this.select(0);
353				}
354			}
355		},
356		remove: function(index) {
357			var self = this;
358
359			if(index === undefined) {
360				this._initPlaylist([]);
361				this._refresh(function() {
362					$(self.cssSelector.jPlayer).jPlayer("clearMedia");
363				});
364				return true;
365			} else {
366
367				if(this.removing) {
368					return false;
369				} else {
370					index = (index < 0) ? self.original.length + index : index; // Negative index relates to end of array.
371					if(0 <= index && index < this.playlist.length) {
372						this.removing = true;
373
374						$(this.cssSelector.playlist + " li:nth-child(" + (index + 1) + ")").slideUp(this.options.playlistOptions.removeTime, function() {
375							$(this).remove();
376
377							if(self.shuffled) {
378								var item = self.playlist[index];
379								$.each(self.original, function(i) {
380									if(self.original[i] === item) {
381										self.original.splice(i, 1);
382										return false; // Exit $.each
383									}
384								});
385								self.playlist.splice(index, 1);
386							} else {
387								self.original.splice(index, 1);
388								self.playlist.splice(index, 1);
389							}
390
391							if(self.original.length) {
392								if(index === self.current) {
393									self.current = (index < self.original.length) ? self.current : self.original.length - 1; // To cope when last element being selected when it was removed
394									self.select(self.current);
395								} else if(index < self.current) {
396									self.current--;
397								}
398							} else {
399								$(self.cssSelector.jPlayer).jPlayer("clearMedia");
400								self.current = 0;
401								self.shuffled = false;
402								self._updateControls();
403							}
404
405							self.removing = false;
406						});
407					}
408					return true;
409				}
410			}
411		},
412		select: function(index) {
413			index = (index < 0) ? this.original.length + index : index; // Negative index relates to end of array.
414			if(0 <= index && index < this.playlist.length) {
415				this.current = index;
416				this._highlight(index);
417				$(this.cssSelector.jPlayer).jPlayer("setMedia", this.playlist[this.current]);
418			} else {
419				this.current = 0;
420			}
421		},
422		play: function(index) {
423			index = (index < 0) ? this.original.length + index : index; // Negative index relates to end of array.
424			if(0 <= index && index < this.playlist.length) {
425				if(this.playlist.length) {
426					this.select(index);
427					$(this.cssSelector.jPlayer).jPlayer("play");
428				}
429			} else if(index === undefined) {
430				$(this.cssSelector.jPlayer).jPlayer("play");
431			}
432		},
433		pause: function() {
434			$(this.cssSelector.jPlayer).jPlayer("pause");
435		},
436		next: function() {
437			var index = (this.current + 1 < this.playlist.length) ? this.current + 1 : 0;
438
439			if(this.loop) {
440				// See if we need to shuffle before looping to start, and only shuffle if more than 1 item.
441				if(index === 0 && this.shuffled && this.options.playlistOptions.shuffleOnLoop && this.playlist.length > 1) {
442					this.shuffle(true, true); // playNow
443				} else {
444					this.play(index);
445				}
446			} else {
447				// The index will be zero if it just looped round
448				if(index > 0) {
449					this.play(index);
450				}
451			}
452		},
453		previous: function() {
454			var index = (this.current - 1 >= 0) ? this.current - 1 : this.playlist.length - 1;
455
456			if(this.loop && this.options.playlistOptions.loopOnPrevious || index < this.playlist.length - 1) {
457				this.play(index);
458			}
459		},
460		shuffle: function(shuffled, playNow) {
461			var self = this;
462
463			if(shuffled === undefined) {
464				shuffled = !this.shuffled;
465			}
466
467			if(shuffled || shuffled !== this.shuffled) {
468
469				$(this.cssSelector.playlist + " ul").slideUp(this.options.playlistOptions.shuffleTime, function() {
470					self.shuffled = shuffled;
471					if(shuffled) {
472						self.playlist.sort(function() {
473							return 0.5 - Math.random();
474						});
475					} else {
476						self._originalPlaylist();
477					}
478					self._refresh(true); // Instant
479
480					if(playNow || !$(self.cssSelector.jPlayer).data("jPlayer").status.paused) {
481						self.play(0);
482					} else {
483						self.select(0);
484					}
485
486					$(this).slideDown(self.options.playlistOptions.shuffleTime);
487				});
488			}
489		},
490		blur: function(that) {
491			if($(this.cssSelector.jPlayer).jPlayer("option", "autoBlur")) {
492				$(that).blur();
493			}
494		}
495	};
496})(jQuery);
497