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 + "'>×</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