497 lines
16 KiB
JavaScript
497 lines
16 KiB
JavaScript
|
/*
|
||
|
* Playlist Object for the jPlayer Plugin
|
||
|
* http://www.jplayer.org
|
||
|
*
|
||
|
* Copyright (c) 2009 - 2014 Happyworm Ltd
|
||
|
* Licensed under the MIT license.
|
||
|
* http://www.opensource.org/licenses/MIT
|
||
|
*
|
||
|
* Author: Mark J Panaghiston
|
||
|
* Version: 2.4.1
|
||
|
* Date: 19th November 2014
|
||
|
*
|
||
|
* Requires:
|
||
|
* - jQuery 1.7.0+
|
||
|
* - jPlayer 2.8.2+
|
||
|
*/
|
||
|
|
||
|
/*global jPlayerPlaylist:true */
|
||
|
|
||
|
(function($, undefined) {
|
||
|
|
||
|
jPlayerPlaylist = function(cssSelector, playlist, options) {
|
||
|
var self = this;
|
||
|
|
||
|
this.current = 0;
|
||
|
this.loop = false; // Flag used with the jPlayer repeat event
|
||
|
this.shuffled = false;
|
||
|
this.removing = false; // Flag is true during remove animation, disabling the remove() method until complete.
|
||
|
|
||
|
this.cssSelector = $.extend({}, this._cssSelector, cssSelector); // Object: Containing the css selectors for jPlayer and its cssSelectorAncestor
|
||
|
this.options = $.extend(true, {
|
||
|
keyBindings: {
|
||
|
next: {
|
||
|
key: 221, // ]
|
||
|
fn: function() {
|
||
|
self.next();
|
||
|
}
|
||
|
},
|
||
|
previous: {
|
||
|
key: 219, // [
|
||
|
fn: function() {
|
||
|
self.previous();
|
||
|
}
|
||
|
},
|
||
|
shuffle: {
|
||
|
key: 83, // s
|
||
|
fn: function() {
|
||
|
self.shuffle();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
stateClass: {
|
||
|
shuffled: "jp-state-shuffled"
|
||
|
}
|
||
|
}, this._options, options); // Object: The jPlayer constructor options for this playlist and the playlist options
|
||
|
|
||
|
this.playlist = []; // Array of Objects: The current playlist displayed (Un-shuffled or Shuffled)
|
||
|
this.original = []; // Array of Objects: The original playlist
|
||
|
|
||
|
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.)
|
||
|
|
||
|
// Setup the css selectors for the extra interface items used by the playlist.
|
||
|
this.cssSelector.details = this.cssSelector.cssSelectorAncestor + " .jp-details"; // Note that jPlayer controls the text in the title element.
|
||
|
this.cssSelector.playlist = this.cssSelector.cssSelectorAncestor + " .jp-playlist";
|
||
|
this.cssSelector.next = this.cssSelector.cssSelectorAncestor + " .jp-next";
|
||
|
this.cssSelector.previous = this.cssSelector.cssSelectorAncestor + " .jp-previous";
|
||
|
this.cssSelector.shuffle = this.cssSelector.cssSelectorAncestor + " .jp-shuffle";
|
||
|
this.cssSelector.shuffleOff = this.cssSelector.cssSelectorAncestor + " .jp-shuffle-off";
|
||
|
|
||
|
// Override the cssSelectorAncestor given in options
|
||
|
this.options.cssSelectorAncestor = this.cssSelector.cssSelectorAncestor;
|
||
|
|
||
|
// Override the default repeat event handler
|
||
|
this.options.repeat = function(event) {
|
||
|
self.loop = event.jPlayer.options.loop;
|
||
|
};
|
||
|
|
||
|
// Create a ready event handler to initialize the playlist
|
||
|
$(this.cssSelector.jPlayer).bind($.jPlayer.event.ready, function() {
|
||
|
self._init();
|
||
|
});
|
||
|
|
||
|
// Create an ended event handler to move to the next item
|
||
|
$(this.cssSelector.jPlayer).bind($.jPlayer.event.ended, function() {
|
||
|
self.next();
|
||
|
});
|
||
|
|
||
|
// Create a play event handler to pause other instances
|
||
|
$(this.cssSelector.jPlayer).bind($.jPlayer.event.play, function() {
|
||
|
$(this).jPlayer("pauseOthers");
|
||
|
});
|
||
|
|
||
|
// Create a resize event handler to show the title in full screen mode.
|
||
|
$(this.cssSelector.jPlayer).bind($.jPlayer.event.resize, function(event) {
|
||
|
if(event.jPlayer.options.fullScreen) {
|
||
|
$(self.cssSelector.details).show();
|
||
|
} else {
|
||
|
$(self.cssSelector.details).hide();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Create click handlers for the extra buttons that do playlist functions.
|
||
|
$(this.cssSelector.previous).click(function(e) {
|
||
|
e.preventDefault();
|
||
|
self.previous();
|
||
|
self.blur(this);
|
||
|
});
|
||
|
|
||
|
$(this.cssSelector.next).click(function(e) {
|
||
|
e.preventDefault();
|
||
|
self.next();
|
||
|
self.blur(this);
|
||
|
});
|
||
|
|
||
|
$(this.cssSelector.shuffle).click(function(e) {
|
||
|
e.preventDefault();
|
||
|
if(self.shuffled && $(self.cssSelector.jPlayer).jPlayer("option", "useStateClassSkin")) {
|
||
|
self.shuffle(false);
|
||
|
} else {
|
||
|
self.shuffle(true);
|
||
|
}
|
||
|
self.blur(this);
|
||
|
});
|
||
|
$(this.cssSelector.shuffleOff).click(function(e) {
|
||
|
e.preventDefault();
|
||
|
self.shuffle(false);
|
||
|
self.blur(this);
|
||
|
}).hide();
|
||
|
|
||
|
// Put the title in its initial display state
|
||
|
if(!this.options.fullScreen) {
|
||
|
$(this.cssSelector.details).hide();
|
||
|
}
|
||
|
|
||
|
// Remove the empty <li> from the page HTML. Allows page to be valid HTML, while not interfereing with display animations
|
||
|
$(this.cssSelector.playlist + " ul").empty();
|
||
|
|
||
|
// Create .on() handlers for the playlist items along with the free media and remove controls.
|
||
|
this._createItemHandlers();
|
||
|
|
||
|
// Instance jPlayer
|
||
|
$(this.cssSelector.jPlayer).jPlayer(this.options);
|
||
|
};
|
||
|
|
||
|
jPlayerPlaylist.prototype = {
|
||
|
_cssSelector: { // static object, instanced in constructor
|
||
|
jPlayer: "#jquery_jplayer_1",
|
||
|
cssSelectorAncestor: "#jp_container_1"
|
||
|
},
|
||
|
_options: { // static object, instanced in constructor
|
||
|
playlistOptions: {
|
||
|
autoPlay: false,
|
||
|
loopOnPrevious: false,
|
||
|
shuffleOnLoop: true,
|
||
|
enableRemoveControls: false,
|
||
|
displayTime: 'slow',
|
||
|
addTime: 'fast',
|
||
|
removeTime: 'fast',
|
||
|
shuffleTime: 'slow',
|
||
|
itemClass: "jp-playlist-item",
|
||
|
freeGroupClass: "jp-free-media",
|
||
|
freeItemClass: "jp-playlist-item-free",
|
||
|
removeItemClass: "jp-playlist-item-remove"
|
||
|
}
|
||
|
},
|
||
|
option: function(option, value) { // For changing playlist options only
|
||
|
if(value === undefined) {
|
||
|
return this.options.playlistOptions[option];
|
||
|
}
|
||
|
|
||
|
this.options.playlistOptions[option] = value;
|
||
|
|
||
|
switch(option) {
|
||
|
case "enableRemoveControls":
|
||
|
this._updateControls();
|
||
|
break;
|
||
|
case "itemClass":
|
||
|
case "freeGroupClass":
|
||
|
case "freeItemClass":
|
||
|
case "removeItemClass":
|
||
|
this._refresh(true); // Instant
|
||
|
this._createItemHandlers();
|
||
|
break;
|
||
|
}
|
||
|
return this;
|
||
|
},
|
||
|
_init: function() {
|
||
|
var self = this;
|
||
|
this._refresh(function() {
|
||
|
if(self.options.playlistOptions.autoPlay) {
|
||
|
self.play(self.current);
|
||
|
} else {
|
||
|
self.select(self.current);
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
_initPlaylist: function(playlist) {
|
||
|
this.current = 0;
|
||
|
this.shuffled = false;
|
||
|
this.removing = false;
|
||
|
this.original = $.extend(true, [], playlist); // Copy the Array of Objects
|
||
|
this._originalPlaylist();
|
||
|
},
|
||
|
_originalPlaylist: function() {
|
||
|
var self = this;
|
||
|
this.playlist = [];
|
||
|
// 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.
|
||
|
$.each(this.original, function(i) {
|
||
|
self.playlist[i] = self.original[i];
|
||
|
});
|
||
|
},
|
||
|
_refresh: function(instant) {
|
||
|
/* instant: Can be undefined, true or a function.
|
||
|
* undefined -> use animation timings
|
||
|
* true -> no animation
|
||
|
* function -> use animation timings and excute function at half way point.
|
||
|
*/
|
||
|
var self = this;
|
||
|
|
||
|
if(instant && !$.isFunction(instant)) {
|
||
|
$(this.cssSelector.playlist + " ul").empty();
|
||
|
$.each(this.playlist, function(i) {
|
||
|
$(self.cssSelector.playlist + " ul").append(self._createListItem(self.playlist[i]));
|
||
|
});
|
||
|
this._updateControls();
|
||
|
} else {
|
||
|
var displayTime = $(this.cssSelector.playlist + " ul").children().length ? this.options.playlistOptions.displayTime : 0;
|
||
|
|
||
|
$(this.cssSelector.playlist + " ul").slideUp(displayTime, function() {
|
||
|
var $this = $(this);
|
||
|
$(this).empty();
|
||
|
|
||
|
$.each(self.playlist, function(i) {
|
||
|
$this.append(self._createListItem(self.playlist[i]));
|
||
|
});
|
||
|
self._updateControls();
|
||
|
if($.isFunction(instant)) {
|
||
|
instant();
|
||
|
}
|
||
|
if(self.playlist.length) {
|
||
|
$(this).slideDown(self.options.playlistOptions.displayTime);
|
||
|
} else {
|
||
|
$(this).show();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
_createListItem: function(media) {
|
||
|
var self = this;
|
||
|
|
||
|
// Wrap the <li> contents in a <div>
|
||
|
var listItem = "<li><div>";
|
||
|
|
||
|
// Create remove control
|
||
|
listItem += "<a href='javascript:;' class='" + this.options.playlistOptions.removeItemClass + "'>×</a>";
|
||
|
|
||
|
// Create links to free media
|
||
|
if(media.free) {
|
||
|
var first = true;
|
||
|
listItem += "<span class='" + this.options.playlistOptions.freeGroupClass + "'>(";
|
||
|
$.each(media, function(property,value) {
|
||
|
if($.jPlayer.prototype.format[property]) { // Check property is a media format.
|
||
|
if(first) {
|
||
|
first = false;
|
||
|
} else {
|
||
|
listItem += " | ";
|
||
|
}
|
||
|
listItem += "<a class='" + self.options.playlistOptions.freeItemClass + "' href='" + value + "' tabindex='-1'>" + property + "</a>";
|
||
|
}
|
||
|
});
|
||
|
listItem += ")</span>";
|
||
|
}
|
||
|
|
||
|
// The title is given next in the HTML otherwise the float:right on the free media corrupts in IE6/7
|
||
|
listItem += "<a href='javascript:;' class='" + this.options.playlistOptions.itemClass + "' tabindex='0'>" + media.title + (media.artist ? " <span class='jp-artist'>by " + media.artist + "</span>" : "") + "</a>";
|
||
|
listItem += "</div></li>";
|
||
|
|
||
|
return listItem;
|
||
|
},
|
||
|
_createItemHandlers: function() {
|
||
|
var self = this;
|
||
|
// Create live handlers for the playlist items
|
||
|
$(this.cssSelector.playlist).off("click", "a." + this.options.playlistOptions.itemClass).on("click", "a." + this.options.playlistOptions.itemClass, function(e) {
|
||
|
e.preventDefault();
|
||
|
var index = $(this).parent().parent().index();
|
||
|
if(self.current !== index) {
|
||
|
self.play(index);
|
||
|
} else {
|
||
|
$(self.cssSelector.jPlayer).jPlayer("play");
|
||
|
}
|
||
|
self.blur(this);
|
||
|
});
|
||
|
|
||
|
// Create live handlers that disable free media links to force access via right click
|
||
|
$(this.cssSelector.playlist).off("click", "a." + this.options.playlistOptions.freeItemClass).on("click", "a." + this.options.playlistOptions.freeItemClass, function(e) {
|
||
|
e.preventDefault();
|
||
|
$(this).parent().parent().find("." + self.options.playlistOptions.itemClass).click();
|
||
|
self.blur(this);
|
||
|
});
|
||
|
|
||
|
// Create live handlers for the remove controls
|
||
|
$(this.cssSelector.playlist).off("click", "a." + this.options.playlistOptions.removeItemClass).on("click", "a." + this.options.playlistOptions.removeItemClass, function(e) {
|
||
|
e.preventDefault();
|
||
|
var index = $(this).parent().parent().index();
|
||
|
self.remove(index);
|
||
|
self.blur(this);
|
||
|
});
|
||
|
},
|
||
|
_updateControls: function() {
|
||
|
if(this.options.playlistOptions.enableRemoveControls) {
|
||
|
$(this.cssSelector.playlist + " ." + this.options.playlistOptions.removeItemClass).show();
|
||
|
} else {
|
||
|
$(this.cssSelector.playlist + " ." + this.options.playlistOptions.removeItemClass).hide();
|
||
|
}
|
||
|
|
||
|
if(this.shuffled) {
|
||
|
$(this.cssSelector.jPlayer).jPlayer("addStateClass", "shuffled");
|
||
|
} else {
|
||
|
$(this.cssSelector.jPlayer).jPlayer("removeStateClass", "shuffled");
|
||
|
}
|
||
|
if($(this.cssSelector.shuffle).length && $(this.cssSelector.shuffleOff).length) {
|
||
|
if(this.shuffled) {
|
||
|
$(this.cssSelector.shuffleOff).show();
|
||
|
$(this.cssSelector.shuffle).hide();
|
||
|
} else {
|
||
|
$(this.cssSelector.shuffleOff).hide();
|
||
|
$(this.cssSelector.shuffle).show();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
_highlight: function(index) {
|
||
|
if(this.playlist.length && index !== undefined) {
|
||
|
$(this.cssSelector.playlist + " .jp-playlist-current").removeClass("jp-playlist-current");
|
||
|
$(this.cssSelector.playlist + " li:nth-child(" + (index + 1) + ")").addClass("jp-playlist-current").find(".jp-playlist-item").addClass("jp-playlist-current");
|
||
|
// $(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>" : ""));
|
||
|
}
|
||
|
},
|
||
|
setPlaylist: function(playlist) {
|
||
|
this._initPlaylist(playlist);
|
||
|
this._init();
|
||
|
},
|
||
|
add: function(media, playNow) {
|
||
|
$(this.cssSelector.playlist + " ul").append(this._createListItem(media)).find("li:last-child").hide().slideDown(this.options.playlistOptions.addTime);
|
||
|
this._updateControls();
|
||
|
this.original.push(media);
|
||
|
this.playlist.push(media); // Both array elements share the same object pointer. Comforms with _initPlaylist(p) system.
|
||
|
|
||
|
if(playNow) {
|
||
|
this.play(this.playlist.length - 1);
|
||
|
} else {
|
||
|
if(this.original.length === 1) {
|
||
|
this.select(0);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
remove: function(index) {
|
||
|
var self = this;
|
||
|
|
||
|
if(index === undefined) {
|
||
|
this._initPlaylist([]);
|
||
|
this._refresh(function() {
|
||
|
$(self.cssSelector.jPlayer).jPlayer("clearMedia");
|
||
|
});
|
||
|
return true;
|
||
|
} else {
|
||
|
|
||
|
if(this.removing) {
|
||
|
return false;
|
||
|
} else {
|
||
|
index = (index < 0) ? self.original.length + index : index; // Negative index relates to end of array.
|
||
|
if(0 <= index && index < this.playlist.length) {
|
||
|
this.removing = true;
|
||
|
|
||
|
$(this.cssSelector.playlist + " li:nth-child(" + (index + 1) + ")").slideUp(this.options.playlistOptions.removeTime, function() {
|
||
|
$(this).remove();
|
||
|
|
||
|
if(self.shuffled) {
|
||
|
var item = self.playlist[index];
|
||
|
$.each(self.original, function(i) {
|
||
|
if(self.original[i] === item) {
|
||
|
self.original.splice(i, 1);
|
||
|
return false; // Exit $.each
|
||
|
}
|
||
|
});
|
||
|
self.playlist.splice(index, 1);
|
||
|
} else {
|
||
|
self.original.splice(index, 1);
|
||
|
self.playlist.splice(index, 1);
|
||
|
}
|
||
|
|
||
|
if(self.original.length) {
|
||
|
if(index === self.current) {
|
||
|
self.current = (index < self.original.length) ? self.current : self.original.length - 1; // To cope when last element being selected when it was removed
|
||
|
self.select(self.current);
|
||
|
} else if(index < self.current) {
|
||
|
self.current--;
|
||
|
}
|
||
|
} else {
|
||
|
$(self.cssSelector.jPlayer).jPlayer("clearMedia");
|
||
|
self.current = 0;
|
||
|
self.shuffled = false;
|
||
|
self._updateControls();
|
||
|
}
|
||
|
|
||
|
self.removing = false;
|
||
|
});
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
select: function(index) {
|
||
|
index = (index < 0) ? this.original.length + index : index; // Negative index relates to end of array.
|
||
|
if(0 <= index && index < this.playlist.length) {
|
||
|
this.current = index;
|
||
|
this._highlight(index);
|
||
|
$(this.cssSelector.jPlayer).jPlayer("setMedia", this.playlist[this.current]);
|
||
|
} else {
|
||
|
this.current = 0;
|
||
|
}
|
||
|
},
|
||
|
play: function(index) {
|
||
|
index = (index < 0) ? this.original.length + index : index; // Negative index relates to end of array.
|
||
|
if(0 <= index && index < this.playlist.length) {
|
||
|
if(this.playlist.length) {
|
||
|
this.select(index);
|
||
|
$(this.cssSelector.jPlayer).jPlayer("play");
|
||
|
}
|
||
|
} else if(index === undefined) {
|
||
|
$(this.cssSelector.jPlayer).jPlayer("play");
|
||
|
}
|
||
|
},
|
||
|
pause: function() {
|
||
|
$(this.cssSelector.jPlayer).jPlayer("pause");
|
||
|
},
|
||
|
next: function() {
|
||
|
var index = (this.current + 1 < this.playlist.length) ? this.current + 1 : 0;
|
||
|
|
||
|
if(this.loop) {
|
||
|
// See if we need to shuffle before looping to start, and only shuffle if more than 1 item.
|
||
|
if(index === 0 && this.shuffled && this.options.playlistOptions.shuffleOnLoop && this.playlist.length > 1) {
|
||
|
this.shuffle(true, true); // playNow
|
||
|
} else {
|
||
|
this.play(index);
|
||
|
}
|
||
|
} else {
|
||
|
// The index will be zero if it just looped round
|
||
|
if(index > 0) {
|
||
|
this.play(index);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
previous: function() {
|
||
|
var index = (this.current - 1 >= 0) ? this.current - 1 : this.playlist.length - 1;
|
||
|
|
||
|
if(this.loop && this.options.playlistOptions.loopOnPrevious || index < this.playlist.length - 1) {
|
||
|
this.play(index);
|
||
|
}
|
||
|
},
|
||
|
shuffle: function(shuffled, playNow) {
|
||
|
var self = this;
|
||
|
|
||
|
if(shuffled === undefined) {
|
||
|
shuffled = !this.shuffled;
|
||
|
}
|
||
|
|
||
|
if(shuffled || shuffled !== this.shuffled) {
|
||
|
|
||
|
$(this.cssSelector.playlist + " ul").slideUp(this.options.playlistOptions.shuffleTime, function() {
|
||
|
self.shuffled = shuffled;
|
||
|
if(shuffled) {
|
||
|
self.playlist.sort(function() {
|
||
|
return 0.5 - Math.random();
|
||
|
});
|
||
|
} else {
|
||
|
self._originalPlaylist();
|
||
|
}
|
||
|
self._refresh(true); // Instant
|
||
|
|
||
|
if(playNow || !$(self.cssSelector.jPlayer).data("jPlayer").status.paused) {
|
||
|
self.play(0);
|
||
|
} else {
|
||
|
self.select(0);
|
||
|
}
|
||
|
|
||
|
$(this).slideDown(self.options.playlistOptions.shuffleTime);
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
blur: function(that) {
|
||
|
if($(this.cssSelector.jPlayer).jPlayer("option", "autoBlur")) {
|
||
|
$(that).blur();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
})(jQuery);
|