Created
October 4, 2018 22:55
-
-
Save iKlotho/3e1e59482bc8103ac012f0a41e0098f2 to your computer and use it in GitHub Desktop.
bar-ui.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*jslint plusplus: true, white: true, nomen: true */ | |
/*global console, document, navigator, soundManager, window */ | |
(function(window) { | |
/** | |
* SoundManager 2: "Bar UI" player | |
* Copyright (c) 2014, Scott Schiller. All rights reserved. | |
* http://www.schillmania.com/projects/soundmanager2/ | |
* Code provided under BSD license. | |
* http://schillmania.com/projects/soundmanager2/license.txt | |
*/ | |
"use strict"; | |
var Player, | |
players = [], | |
// CSS selector that will get us the top-level DOM node for the player UI. | |
playerSelector = '.sm2-bar-ui', | |
playerOptions, | |
utils; | |
/** | |
* Slightly hackish: event callbacks. | |
* Override globally by setting window.sm2BarPlayers.on = {}, or individually by window.sm2BarPlayers[0].on = {} etc. | |
*/ | |
players.on = { | |
/* | |
play: function(player) { | |
console.log('playing', player); | |
}, | |
finish: function(player) { | |
// each sound | |
console.log('finish', player); | |
}, | |
pause: function(player) { | |
console.log('pause', player); | |
}, | |
error: function(player) { | |
console.log('error', player); | |
} | |
end: function(player) { | |
// end of playlist | |
console.log('end', player); | |
} | |
*/ | |
}; | |
playerOptions = { | |
// useful when multiple players are in use, or other SM2 sounds are active etc. | |
stopOtherSounds: true, | |
// CSS class to let the browser load the URL directly e.g., <a href="foo.mp3" class="sm2-exclude">download foo.mp3</a> | |
excludeClass: 'sm2-exclude' | |
}; | |
soundManager.setup({ | |
// trade-off: higher UI responsiveness (play/progress bar), but may use more CPU. | |
html5PollingInterval: 50, | |
flashVersion: 9 | |
}); | |
soundManager.onready(function() { | |
var nodes, i, j; | |
nodes = utils.dom.getAll(playerSelector); | |
if (nodes && nodes.length) { | |
for (i=0, j=nodes.length; i<j; i++) { | |
players.push(new Player(nodes[i])); | |
} | |
} | |
}); | |
/** | |
* player bits | |
*/ | |
Player = function(playerNode) { | |
var css, dom, extras, playlistController, soundObject, actions, actionData, defaultItem, defaultVolume, firstOpen, exports; | |
css = { | |
disabled: 'disabled', | |
selected: 'selected', | |
active: 'active', | |
legacy: 'legacy', | |
noVolume: 'no-volume', | |
playlistOpen: 'playlist-open' | |
}; | |
dom = { | |
o: null, | |
playlist: null, | |
playlistTarget: null, | |
playlistContainer: null, | |
time: null, | |
player: null, | |
progress: null, | |
progressTrack: null, | |
progressBar: null, | |
duration: null, | |
volume: null | |
}; | |
// prepended to tracks when a sound fails to load/play | |
extras = { | |
loadFailedCharacter: '<span title="Failed to load/play." class="load-error">✖</span>' | |
}; | |
function stopOtherSounds() { | |
if (playerOptions.stopOtherSounds) { | |
soundManager.stopAll(); | |
} | |
} | |
function callback(method) { | |
if (method) { | |
// fire callback, passing current turntable object | |
if (exports.on && exports.on[method]) { | |
exports.on[method](exports); | |
} else if (players.on[method]) { | |
players.on[method](exports); | |
} | |
} | |
} | |
function getTime(msec, useString) { | |
// convert milliseconds to hh:mm:ss, return as object literal or string | |
var nSec = Math.floor(msec/1000), | |
hh = Math.floor(nSec/3600), | |
min = Math.floor(nSec/60) - Math.floor(hh * 60), | |
sec = Math.floor(nSec -(hh*3600) -(min*60)); | |
// if (min === 0 && sec === 0) return null; // return 0:00 as null | |
return (useString ? ((hh ? hh + ':' : '') + (hh && min < 10 ? '0' + min : min) + ':' + ( sec < 10 ? '0' + sec : sec ) ) : { 'min': min, 'sec': sec }); | |
} | |
function setTitle(item) { | |
// given a link, update the "now playing" UI. | |
// if this is an <li> with an inner link, grab and use the text from that. | |
var links = item.getElementsByTagName('a'); | |
if (links.length) { | |
item = links[0]; | |
} | |
// remove any failed character sequence, also | |
dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML.replace(extras.loadFailedCharacter, '') + '</li></ul>'; | |
if (dom.playlistTarget.getElementsByTagName('li')[0].scrollWidth > dom.playlistTarget.offsetWidth) { | |
// this item can use <marquee>, in fact. | |
dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li><marquee>' + item.innerHTML + '</marquee></li></ul>'; | |
} | |
} | |
function makeSound(url) { | |
var sound = soundManager.createSound({ | |
url: url, | |
volume: defaultVolume, | |
whileplaying: function() { | |
var progressMaxLeft = 100, | |
left, | |
width; | |
left = Math.min(progressMaxLeft, Math.max(0, (progressMaxLeft * (this.position / this.durationEstimate)))) + '%'; | |
width = Math.min(100, Math.max(0, (100 * this.position / this.durationEstimate))) + '%'; | |
if (this.duration) { | |
dom.progress.style.left = left; | |
dom.progressBar.style.width = width; | |
// TODO: only write changes | |
dom.time.innerHTML = getTime(this.position, true); | |
} | |
}, | |
onbufferchange: function(isBuffering) { | |
if (isBuffering) { | |
utils.css.add(dom.o, 'buffering'); | |
} else { | |
utils.css.remove(dom.o, 'buffering'); | |
} | |
}, | |
onplay: function() { | |
utils.css.swap(dom.o, 'paused', 'playing'); | |
callback('play'); | |
clearTimeout(AudioBarTimer); | |
}, | |
onpause: function() { | |
utils.css.swap(dom.o, 'playing', 'paused'); | |
callback('pause'); | |
AudioBarTimer = setTimeout(function(){ Close_Audio_Bar(); }, 2000); | |
}, | |
onresume: function() { | |
utils.css.swap(dom.o, 'paused', 'playing'); | |
clearTimeout(AudioBarTimer); | |
}, | |
whileloading: function() { | |
if (!this.isHTML5) { | |
dom.duration.innerHTML = getTime(this.durationEstimate, true); | |
} | |
}, | |
onload: function(ok) { | |
if (ok) { | |
dom.duration.innerHTML = getTime(this.duration, true); | |
} else if (this._iO && this._iO.onerror) { | |
this._iO.onerror(); | |
} | |
}, | |
onerror: function() { | |
// sound failed to load. | |
var item, element, html; | |
item = playlistController.getItem(); | |
if (item) { | |
// note error, delay 2 seconds and advance? | |
// playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML + '</li></ul>'; | |
if (extras.loadFailedCharacter) { | |
dom.playlistTarget.innerHTML = dom.playlistTarget.innerHTML.replace('<li>' ,'<li>' + extras.loadFailedCharacter + ' '); | |
if (playlistController.data.playlist && playlistController.data.playlist[playlistController.data.selectedIndex]) { | |
element = playlistController.data.playlist[playlistController.data.selectedIndex].getElementsByTagName('a')[0]; | |
html = element.innerHTML; | |
if (html.indexOf(extras.loadFailedCharacter) === -1) { | |
element.innerHTML = extras.loadFailedCharacter + ' ' + html; | |
} | |
} | |
} | |
} | |
callback('error'); | |
// load next, possibly with delay. | |
if (navigator.userAgent.match(/mobile/i)) { | |
// mobile will likely block the next play() call if there is a setTimeout() - so don't use one here. | |
actions.next(); | |
} else { | |
if (playlistController.data.timer) { | |
window.clearTimeout(playlistController.data.timer); | |
} | |
playlistController.data.timer = window.setTimeout(actions.next, 2000); | |
} | |
}, | |
onstop: function() { | |
utils.css.remove(dom.o, 'playing'); | |
}, | |
onfinish: function() { | |
var lastIndex, item; | |
utils.css.remove(dom.o, 'playing'); | |
dom.progress.style.left = '0%'; | |
lastIndex = playlistController.data.selectedIndex; | |
callback('finish'); | |
// next track? | |
item = playlistController.getNext(); | |
AudioBarTimer = setTimeout(function(){ Close_Audio_Bar(); }, 2000); | |
// don't play the same item over and over again, if at end of playlist etc. | |
if (item && playlistController.data.selectedIndex !== lastIndex) { | |
playlistController.select(item); | |
setTitle(item); | |
stopOtherSounds(); | |
// play next | |
this.play({ | |
url: playlistController.getURL() | |
}); | |
} else { | |
// end of playlist case | |
// explicitly stop? | |
// this.stop(); | |
callback('end'); | |
} | |
} | |
}); | |
return sound; | |
} | |
function playLink(link) { | |
// if a link is OK, play it. | |
if (soundManager.canPlayURL(link.href)) { | |
// if there's a timer due to failure to play one track, cancel it. | |
// catches case when user may use previous/next after an error. | |
if (playlistController.data.timer) { | |
window.clearTimeout(playlistController.data.timer); | |
playlistController.data.timer = null; | |
} | |
if (!soundObject) { | |
soundObject = makeSound(link.href); | |
} | |
// required to reset pause/play state on iOS so whileplaying() works? odd. | |
soundObject.stop(); | |
playlistController.select(link.parentNode); | |
setTitle(link.parentNode); | |
// reset the UI | |
// TODO: function that also resets/hides timing info. | |
dom.progress.style.left = '0px'; | |
dom.progressBar.style.width = '0px'; | |
stopOtherSounds(); | |
soundObject.play({ | |
url: link.href, | |
position: 0 | |
}); | |
} | |
} | |
function PlaylistController() { | |
var data; | |
data = { | |
// list of nodes? | |
playlist: [], | |
// NOTE: not implemented yet. | |
// shuffledIndex: [], | |
// shuffleMode: false, | |
// selection | |
selectedIndex: 0, | |
loopMode: false, | |
timer: null | |
}; | |
function getPlaylist() { | |
return data.playlist; | |
} | |
function getItem(offset) { | |
var list, | |
item; | |
// given the current selection (or an offset), return the current item. | |
// if currently null, may be end of list case. bail. | |
if (data.selectedIndex === null) { | |
return offset; | |
} | |
list = getPlaylist(); | |
// use offset if provided, otherwise take default selected. | |
offset = (offset !== undefined ? offset : data.selectedIndex); | |
// safety check - limit to between 0 and list length | |
offset = Math.max(0, Math.min(offset, list.length)); | |
item = list[offset]; | |
return item; | |
} | |
function findOffsetFromItem(item) { | |
// given an <li> item, find it in the playlist array and return the index. | |
var list, | |
i, | |
j, | |
offset; | |
offset = -1; | |
list = getPlaylist(); | |
if (list) { | |
for (i=0, j=list.length; i<j; i++) { | |
if (list[i] === item) { | |
offset = i; | |
break; | |
} | |
} | |
} | |
return offset; | |
} | |
function getNext() { | |
// don't increment if null. | |
if (data.selectedIndex !== null) { | |
data.selectedIndex++; | |
} | |
if (data.playlist.length > 1) { | |
if (data.selectedIndex >= data.playlist.length) { | |
if (data.loopMode) { | |
// loop to beginning | |
data.selectedIndex = 0; | |
} else { | |
// no change | |
data.selectedIndex--; | |
// end playback | |
// data.selectedIndex = null; | |
} | |
} | |
} else { | |
data.selectedIndex = null; | |
} | |
return getItem(); | |
} | |
function getPrevious() { | |
data.selectedIndex--; | |
if (data.selectedIndex < 0) { | |
// wrapping around beginning of list? loop or exit. | |
if (data.loopMode) { | |
data.selectedIndex = data.playlist.length - 1; | |
} else { | |
// undo | |
data.selectedIndex++; | |
} | |
} | |
return getItem(); | |
} | |
function resetLastSelected() { | |
// remove UI highlight(s) on selected items. | |
var items, | |
i, j; | |
items = utils.dom.getAll(dom.playlist, '.' + css.selected); | |
for (i=0, j=items.length; i<j; i++) { | |
utils.css.remove(items[i], css.selected); | |
} | |
} | |
function select(item) { | |
var offset, | |
itemTop, | |
itemBottom, | |
containerHeight, | |
scrollTop, | |
itemPadding, | |
liElement; | |
// remove last selected, if any | |
resetLastSelected(); | |
if (item) { | |
liElement = utils.dom.ancestor('li', item); | |
utils.css.add(liElement, css.selected); | |
itemTop = item.offsetTop; | |
itemBottom = itemTop + item.offsetHeight; | |
containerHeight = dom.playlistContainer.offsetHeight; | |
scrollTop = dom.playlist.scrollTop; | |
itemPadding = 8; | |
if (itemBottom > containerHeight + scrollTop) { | |
// bottom-align | |
dom.playlist.scrollTop = itemBottom - containerHeight + itemPadding; | |
} else if (itemTop < scrollTop) { | |
// top-align | |
dom.playlist.scrollTop = item.offsetTop - itemPadding; | |
} | |
} | |
// update selected offset, too. | |
offset = findOffsetFromItem(item); | |
data.selectedIndex = offset; | |
} | |
function playItemByOffset(offset) { | |
var item; | |
offset = (offset || 0); | |
item = getItem(offset); | |
if (item) { | |
playLink(item.getElementsByTagName('a')[0]); | |
} | |
} | |
function getURL() { | |
// return URL of currently-selected item | |
var item, url; | |
item = getItem(); | |
if (item) { | |
url = item.getElementsByTagName('a')[0].href; | |
} | |
return url; | |
} | |
function refreshDOM() { | |
// get / update playlist from DOM | |
if (!dom.playlist) { | |
if (window.console && console.warn) { | |
console.warn('refreshDOM(): playlist node not found?'); | |
} | |
return false; | |
} | |
data.playlist = dom.playlist.getElementsByTagName('li'); | |
} | |
function initDOM() { | |
dom.playlistTarget = utils.dom.get(dom.o, '.sm2-playlist-target'); | |
dom.playlistContainer = utils.dom.get(dom.o, '.sm2-playlist-drawer'); | |
dom.playlist = utils.dom.get(dom.o, '.sm2-playlist-bd'); | |
} | |
function init() { | |
// inherit the default SM2 volume | |
defaultVolume = soundManager.defaultOptions.volume; | |
initDOM(); | |
refreshDOM(); | |
// animate playlist open, if HTML classname indicates so. | |
if (utils.css.has(dom.o, css.playlistOpen)) { | |
// hackish: run this after API has returned | |
window.setTimeout(function() { | |
actions.menu(true); | |
}, 1); | |
} | |
} | |
init(); | |
return { | |
data: data, | |
refresh: refreshDOM, | |
getNext: getNext, | |
getPrevious: getPrevious, | |
getItem: getItem, | |
getURL: getURL, | |
playItemByOffset: playItemByOffset, | |
select: select | |
}; | |
} | |
function isRightClick(e) { | |
// only pay attention to left clicks. old IE differs where there's no e.which, but e.button is 1 on left click. | |
if (e && ((e.which && e.which === 2) || (e.which === undefined && e.button !== 1))) { | |
// http://www.quirksmode.org/js/events_properties.html#button | |
return true; | |
} | |
} | |
function getActionData(target) { | |
// DOM measurements for volume slider | |
if (!target) { | |
return false; | |
} | |
actionData.volume.x = utils.position.getOffX(target); | |
actionData.volume.y = utils.position.getOffY(target); | |
actionData.volume.width = target.offsetWidth; | |
actionData.volume.height = target.offsetHeight; | |
// potentially dangerous: this should, but may not be a percentage-based value. | |
actionData.volume.backgroundSize = parseInt(utils.style.get(target, 'background-size'), 10); | |
// IE gives pixels even if background-size specified as % in CSS. Boourns. | |
if (window.navigator.userAgent.match(/msie|trident/i)) { | |
actionData.volume.backgroundSize = (actionData.volume.backgroundSize / actionData.volume.width) * 100; | |
} | |
} | |
function handleMouseDown(e) { | |
var links, | |
target; | |
target = e.target || e.srcElement; | |
if (isRightClick(e)) { | |
return true; | |
} | |
// normalize to <a>, if applicable. | |
if (target.nodeName.toLowerCase() !== 'a') { | |
links = target.getElementsByTagName('a'); | |
if (links && links.length) { | |
target = target.getElementsByTagName('a')[0]; | |
} | |
} | |
if (utils.css.has(target, 'sm2-volume-control')) { | |
// drag case for volume | |
getActionData(target); | |
utils.events.add(document, 'mousemove', actions.adjustVolume); | |
utils.events.add(document, 'mouseup', actions.releaseVolume); | |
// and apply right away | |
return actions.adjustVolume(e); | |
} | |
} | |
function handleClick(e) { | |
var evt, | |
target, | |
offset, | |
targetNodeName, | |
methodName, | |
href, | |
handled; | |
evt = (e || window.event); | |
target = evt.target || evt.srcElement; | |
if (target && target.nodeName) { | |
targetNodeName = target.nodeName.toLowerCase(); | |
if (targetNodeName !== 'a') { | |
// old IE (IE 8) might return nested elements inside the <a>, eg., <b> etc. Try to find the parent <a>. | |
if (target.parentNode) { | |
do { | |
target = target.parentNode; | |
targetNodeName = target.nodeName.toLowerCase(); | |
} while (targetNodeName !== 'a' && target.parentNode); | |
if (!target) { | |
// something went wrong. bail. | |
return false; | |
} | |
} | |
} | |
if (targetNodeName === 'a') { | |
// yep, it's a link. | |
href = target.href; | |
if (soundManager.canPlayURL(href)) { | |
// not excluded | |
if (!utils.css.has(target, playerOptions.excludeClass)) { | |
// find this in the playlist | |
playLink(target); | |
handled = true; | |
} | |
} else { | |
// is this one of the action buttons, eg., play/pause, volume, etc.? | |
offset = target.href.lastIndexOf('#'); | |
if (offset !== -1) { | |
methodName = target.href.substr(offset+1); | |
if (methodName && actions[methodName]) { | |
handled = true; | |
actions[methodName](e); | |
} | |
} | |
} | |
// fall-through case | |
if (handled) { | |
// prevent browser fall-through | |
return utils.events.preventDefault(evt); | |
} | |
} | |
} | |
} | |
function handleMouse(e) { | |
var target, barX, barWidth, x, newPosition, sound; | |
target = dom.progressTrack; | |
barX = utils.position.getOffX(target); | |
barWidth = target.offsetWidth; | |
x = (e.clientX - barX); | |
newPosition = (x / barWidth); | |
sound = soundObject; | |
if (sound && sound.duration) { | |
sound.setPosition(sound.duration * newPosition); | |
// a little hackish: ensure UI updates immediately with current position, even if audio is buffering and hasn't moved there yet. | |
if (sound._iO && sound._iO.whileplaying) { | |
sound._iO.whileplaying.apply(sound); | |
} | |
} | |
if (e.preventDefault) { | |
e.preventDefault(); | |
} | |
return false; | |
} | |
function releaseMouse(e) { | |
utils.events.remove(document, 'mousemove', handleMouse); | |
utils.css.remove(dom.o, 'grabbing'); | |
utils.events.remove(document, 'mouseup', releaseMouse); | |
utils.events.preventDefault(e); | |
return false; | |
} | |
function init() { | |
// init DOM? | |
if (!playerNode) { | |
console.warn('init(): No playerNode element?'); | |
} | |
dom.o = playerNode; | |
// are we dealing with a crap browser? apply legacy CSS if so. | |
if (window.navigator.userAgent.match(/msie [678]/i)) { | |
utils.css.add(dom.o, css.legacy); | |
} | |
if (window.navigator.userAgent.match(/mobile/i)) { | |
// majority of mobile devices don't let HTML5 audio set volume. | |
utils.css.add(dom.o, css.noVolume); | |
} | |
dom.progress = utils.dom.get(dom.o, '.sm2-progress-ball'); | |
dom.progressTrack = utils.dom.get(dom.o, '.sm2-progress-track'); | |
dom.progressBar = utils.dom.get(dom.o, '.sm2-progress-bar'); | |
dom.volume = utils.dom.get(dom.o, 'a.sm2-volume-control'); | |
// measure volume control dimensions | |
if (dom.volume) { | |
getActionData(dom.volume); | |
} | |
dom.duration = utils.dom.get(dom.o, '.sm2-inline-duration'); | |
dom.time = utils.dom.get(dom.o, '.sm2-inline-time'); | |
playlistController = new PlaylistController(); | |
defaultItem = playlistController.getItem(0); | |
playlistController.select(defaultItem); | |
if (defaultItem) { | |
setTitle(defaultItem); | |
} | |
utils.events.add(dom.o, 'mousedown', handleMouseDown); | |
utils.events.add(dom.o, 'click', handleClick); | |
utils.events.add(dom.progressTrack, 'mousedown', function(e) { | |
if (isRightClick(e)) { | |
return true; | |
} | |
utils.css.add(dom.o, 'grabbing'); | |
utils.events.add(document, 'mousemove', handleMouse); | |
utils.events.add(document, 'mouseup', releaseMouse); | |
return handleMouse(e); | |
}); | |
} | |
// --- | |
actionData = { | |
volume: { | |
x: 0, | |
y: 0, | |
width: 0, | |
height: 0, | |
backgroundSize: 0 | |
} | |
}; | |
actions = { | |
play: function(offsetOrEvent) { | |
//clearTimeout(AudioBarTimer); | |
/** | |
* This is an overloaded function that takes mouse/touch events or offset-based item indices. | |
* Remember, "auto-play" will not work on mobile devices unless this function is called immediately from a touch or click event. | |
* If you have the link but not the offset, you can also pass a fake event object with a target of an <a> inside the playlist - e.g. { target: someMP3Link } | |
*/ | |
var target, | |
href, | |
e; | |
if (offsetOrEvent !== undefined && !isNaN(offsetOrEvent)) { | |
// smells like a number. | |
return playlistController.playItemByOffset(offsetOrEvent); | |
} | |
// DRY things a bit | |
e = offsetOrEvent; | |
if (e && e.target) { | |
target = e.target || e.srcElement; | |
href = target.href; | |
} | |
// haaaack - if null due to no event, OR '#' due to play/pause link, get first link from playlist | |
if (!href || href.indexOf('#') !== -1) { | |
if (dom.playlist.getElementsByTagName('a')[0]) { href = dom.playlist.getElementsByTagName('a')[0].href; } | |
else { return false; } | |
} | |
if (!soundObject) { | |
soundObject = makeSound(href); | |
} | |
// edge case: if the current sound is not playing, stop all others. | |
if (!soundObject.playState) { | |
stopOtherSounds(); | |
} | |
// TODO: if user pauses + unpauses a sound that had an error, try to play next? | |
soundObject.togglePause(); | |
// special case: clear "play next" timeout, if one exists. | |
// edge case: user pauses after a song failed to load. | |
if (soundObject.paused && playlistController.data.timer) { | |
window.clearTimeout(playlistController.data.timer); | |
playlistController.data.timer = null; | |
} | |
}, | |
pause: function() { | |
if (soundObject && soundObject.readyState) { | |
soundObject.pause(); | |
//AudioBarTimer = setTimeout(function(){ Close_Audio_Bar(); }, 2000); | |
} | |
}, | |
resume: function() { | |
if (soundObject && soundObject.readyState) { | |
soundObject.resume(); | |
//clearTimeout(AudioBarTimer); | |
} | |
}, | |
stop: function() { | |
if (soundObject && soundObject.readyState) { | |
soundObject.stop(); | |
//AudioBarTimer = setTimeout(function(){ Close_Audio_Bar(); }, 2000); | |
} | |
// just an alias for pause, really. | |
// don't actually stop because that will mess up some UI state, i.e., dragging the slider. | |
//return actions.pause(); | |
}, | |
playnew: function(/* e */) { | |
//clearTimeout(AudioBarTimer); | |
var href | |
if (dom.playlist.getElementsByTagName('a')[0]) { | |
href = dom.playlist.getElementsByTagName('a')[0].href; | |
} | |
else { return false; } | |
//if (soundObject) { | |
// soundObject = soundManager.destroySound(href); | |
// soundObject = makeSound(href); | |
//} | |
//document.getElementById('CB_Tmp').innerHTML = href; | |
//if (!soundObject) { | |
soundObject = makeSound(href); | |
//} | |
// edge case: if the current sound is not playing, stop all others. | |
if (!soundObject.playState) { | |
stopOtherSounds(); | |
} | |
// TODO: if user pauses + unpauses a sound that had an error, try to play next? | |
//soundObject.togglePause(); //This pauses everything | |
if (playlistController.data.timer) { | |
window.clearTimeout(playlistController.data.timer); | |
playlistController.data.timer = null; | |
} | |
// special case: clear "play next" timeout, if one exists. | |
// edge case: user pauses after a song failed to load. | |
if (soundObject.paused && playlistController.data.timer) { | |
window.clearTimeout(playlistController.data.timer); | |
playlistController.data.timer = null; | |
} | |
}, | |
next: function(/* e */) { | |
var item, lastIndex; | |
// special case: clear "play next" timeout, if one exists. | |
if (playlistController.data.timer) { | |
window.clearTimeout(playlistController.data.timer); | |
playlistController.data.timer = null; | |
} | |
lastIndex = playlistController.data.selectedIndex; | |
item = playlistController.getNext(true); | |
// don't play the same item again | |
if (item && playlistController.data.selectedIndex !== lastIndex) { | |
playLink(item.getElementsByTagName('a')[0]); | |
} | |
}, | |
prev: function(/* e */) { | |
var item, lastIndex; | |
lastIndex = playlistController.data.selectedIndex; | |
item = playlistController.getPrevious(); | |
// don't play the same item again | |
if (item && playlistController.data.selectedIndex !== lastIndex) { | |
playLink(item.getElementsByTagName('a')[0]); | |
} | |
}, | |
shuffle: function(e) { | |
// NOTE: not implemented yet. | |
var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.shuffle')); | |
if (target && !utils.css.has(target, css.disabled)) { | |
utils.css.toggle(target.parentNode, css.active); | |
playlistController.data.shuffleMode = !playlistController.data.shuffleMode; | |
} | |
}, | |
repeat: function(e) { | |
var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.repeat')); | |
if (target && !utils.css.has(target, css.disabled)) { | |
utils.css.toggle(target.parentNode, css.active); | |
playlistController.data.loopMode = !playlistController.data.loopMode; | |
} | |
}, | |
menu: function(ignoreToggle) { | |
var isOpen; | |
isOpen = utils.css.has(dom.o, css.playlistOpen); | |
// hackish: reset scrollTop in default first open case. odd, but some browsers have a non-zero scroll offset the first time the playlist opens. | |
if (playlistController && !playlistController.data.selectedIndex && !firstOpen) { | |
dom.playlist.scrollTop = 0; | |
firstOpen = true; | |
} | |
// sniff out booleans from mouse events, as this is referenced directly by event handlers. | |
if (typeof ignoreToggle !== 'boolean' || !ignoreToggle) { | |
if (!isOpen) { | |
// explicitly set height:0, so the first closed -> open animation runs properly | |
dom.playlistContainer.style.height = '0px'; | |
} | |
isOpen = utils.css.toggle(dom.o, css.playlistOpen); | |
} | |
// playlist | |
dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px'; | |
}, | |
adjustVolume: function(e) { | |
/** | |
* NOTE: this is the mousemove() event handler version. | |
* Use setVolume(50), etc., to assign volume directly. | |
*/ | |
var backgroundMargin, | |
pixelMargin, | |
target, | |
value, | |
volume; | |
value = 0; | |
target = dom.volume; | |
// safety net | |
if (e === undefined) { | |
return false; | |
} | |
if (!e || e.clientX === undefined) { | |
// called directly or with a non-mouseEvent object, etc. | |
// proxy to the proper method. | |
if (arguments.length && window.console && window.console.warn) { | |
console.warn('Bar UI: call setVolume(' + e + ') instead of adjustVolume(' + e + ').'); | |
} | |
return actions.setVolume.apply(this, arguments); | |
} | |
// based on getStyle() result | |
// figure out spacing around background image based on background size, eg. 60% background size. | |
// 60% wide means 20% margin on each side. | |
backgroundMargin = (100 - actionData.volume.backgroundSize) / 2; | |
// relative position of mouse over element | |
value = Math.max(0, Math.min(1, (e.clientX - actionData.volume.x) / actionData.volume.width)); | |
target.style.clip = 'rect(0px, ' + (actionData.volume.width * value) + 'px, ' + actionData.volume.height + 'px, ' + (actionData.volume.width * (backgroundMargin/100)) + 'px)'; | |
// determine logical volume, including background margin | |
pixelMargin = ((backgroundMargin/100) * actionData.volume.width); | |
volume = Math.max(0, Math.min(1, ((e.clientX - actionData.volume.x) - pixelMargin) / (actionData.volume.width - (pixelMargin*2)))) * 100; | |
// set volume | |
if (soundObject) { | |
soundObject.setVolume(volume); | |
} | |
defaultVolume = volume; | |
return utils.events.preventDefault(e); | |
}, | |
releaseVolume: function(/* e */) { | |
utils.events.remove(document, 'mousemove', actions.adjustVolume); | |
utils.events.remove(document, 'mouseup', actions.releaseVolume); | |
}, | |
setVolume: function(volume) { | |
// set volume (0-100) and update volume slider UI. | |
var backgroundSize, | |
backgroundMargin, | |
backgroundOffset, | |
target, | |
from, | |
to; | |
if (volume === undefined || isNaN(volume)) { | |
return; | |
} | |
if (dom.volume) { | |
target = dom.volume; | |
// based on getStyle() result | |
backgroundSize = actionData.volume.backgroundSize; | |
// figure out spacing around background image based on background size, eg. 60% background size. | |
// 60% wide means 20% margin on each side. | |
backgroundMargin = (100 - backgroundSize) / 2; | |
// margin as pixel value relative to width | |
backgroundOffset = actionData.volume.width * (backgroundMargin/100); | |
from = backgroundOffset; | |
to = from + ((actionData.volume.width - (backgroundOffset*2)) * (volume/100)); | |
target.style.clip = 'rect(0px, ' + to + 'px, ' + actionData.volume.height + 'px, ' + from + 'px)'; | |
} | |
// apply volume to sound, as applicable | |
if (soundObject) { | |
soundObject.setVolume(volume); | |
} | |
defaultVolume = volume; | |
} | |
}; | |
init(); | |
// TODO: mixin actions -> exports | |
exports = { | |
// Per-instance events: window.sm2BarPlayers[0].on = { ... } etc. See global players.on example above for reference. | |
on: null, | |
actions: actions, | |
dom: dom, | |
playlistController: playlistController | |
}; | |
return exports; | |
}; | |
// barebones utilities for logic, CSS, DOM, events etc. | |
utils = { | |
array: (function() { | |
function compare(property) { | |
var result; | |
return function(a, b) { | |
if (a[property] < b[property]) { | |
result = -1; | |
} else if (a[property] > b[property]) { | |
result = 1; | |
} else { | |
result = 0; | |
} | |
return result; | |
}; | |
} | |
function shuffle(array) { | |
// Fisher-Yates shuffle algo | |
var i, j, temp; | |
for (i = array.length - 1; i > 0; i--) { | |
j = Math.floor(Math.random() * (i+1)); | |
temp = array[i]; | |
array[i] = array[j]; | |
array[j] = temp; | |
} | |
return array; | |
} | |
return { | |
compare: compare, | |
shuffle: shuffle | |
}; | |
}()), | |
css: (function() { | |
function hasClass(o, cStr) { | |
return (o.className !== undefined ? new RegExp('(^|\\s)' + cStr + '(\\s|$)').test(o.className) : false); | |
} | |
function addClass(o, cStr) { | |
if (!o || !cStr || hasClass(o, cStr)) { | |
return false; // safety net | |
} | |
o.className = (o.className ? o.className + ' ' : '') + cStr; | |
} | |
function removeClass(o, cStr) { | |
if (!o || !cStr || !hasClass(o, cStr)) { | |
return false; | |
} | |
o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), ''); | |
} | |
function swapClass(o, cStr1, cStr2) { | |
var tmpClass = { | |
className: o.className | |
}; | |
removeClass(tmpClass, cStr1); | |
addClass(tmpClass, cStr2); | |
o.className = tmpClass.className; | |
} | |
function toggleClass(o, cStr) { | |
var found, | |
method; | |
found = hasClass(o, cStr); | |
method = (found ? removeClass : addClass); | |
method(o, cStr); | |
// indicate the new state... | |
return !found; | |
} | |
return { | |
has: hasClass, | |
add: addClass, | |
remove: removeClass, | |
swap: swapClass, | |
toggle: toggleClass | |
}; | |
}()), | |
dom: (function() { | |
function getAll(param1, param2) { | |
var node, | |
selector, | |
results; | |
if (arguments.length === 1) { | |
// .selector case | |
node = document.documentElement; | |
// first param is actually the selector | |
selector = param1; | |
} else { | |
// node, .selector | |
node = param1; | |
selector = param2; | |
} | |
// sorry, IE 7 users; IE 8+ required. | |
if (node && node.querySelectorAll) { | |
results = node.querySelectorAll(selector); | |
} | |
return results; | |
} | |
function get(/* parentNode, selector */) { | |
var results = getAll.apply(this, arguments); | |
// hackish: if an array, return the last item. | |
if (results && results.length) { | |
return results[results.length-1]; | |
} | |
// handle "not found" case | |
return results && results.length === 0 ? null : results; | |
} | |
function ancestor(nodeName, element, checkCurrent) { | |
var result; | |
if (!element || !nodeName) { | |
return element; | |
} | |
nodeName = nodeName.toUpperCase(); | |
// return if current node matches. | |
if (checkCurrent && element && element.nodeName === nodeName) { | |
return element; | |
} | |
while (element && element.nodeName !== nodeName && element.parentNode) { | |
element = element.parentNode; | |
} | |
return (element && element.nodeName === nodeName ? element : null); | |
} | |
return { | |
ancestor: ancestor, | |
get: get, | |
getAll: getAll | |
}; | |
}()), | |
position: (function() { | |
function getOffX(o) { | |
// http://www.xs4all.nl/~ppk/js/findpos.html | |
var curleft = 0; | |
if (o.offsetParent) { | |
while (o.offsetParent) { | |
curleft += o.offsetLeft; | |
o = o.offsetParent; | |
} | |
} else if (o.x) { | |
curleft += o.x; | |
} | |
return curleft; | |
} | |
function getOffY(o) { | |
// http://www.xs4all.nl/~ppk/js/findpos.html | |
var curtop = 0; | |
if (o.offsetParent) { | |
while (o.offsetParent) { | |
curtop += o.offsetTop; | |
o = o.offsetParent; | |
} | |
} else if (o.y) { | |
curtop += o.y; | |
} | |
return curtop; | |
} | |
return { | |
getOffX: getOffX, | |
getOffY: getOffY | |
}; | |
}()), | |
style: (function() { | |
function get(node, styleProp) { | |
// http://www.quirksmode.org/dom/getstyles.html | |
var value; | |
if (node.currentStyle) { | |
value = node.currentStyle[styleProp]; | |
} else if (window.getComputedStyle) { | |
value = document.defaultView.getComputedStyle(node, null).getPropertyValue(styleProp); | |
} | |
return value; | |
} | |
return { | |
get: get | |
}; | |
}()), | |
events: (function() { | |
var add, remove, preventDefault; | |
add = function(o, evtName, evtHandler) { | |
// return an object with a convenient detach method. | |
var eventObject = { | |
detach: function() { | |
return remove(o, evtName, evtHandler); | |
} | |
}; | |
if (window.addEventListener) { | |
o.addEventListener(evtName, evtHandler, false); | |
} else { | |
o.attachEvent('on' + evtName, evtHandler); | |
} | |
return eventObject; | |
}; | |
remove = (window.removeEventListener !== undefined ? function(o, evtName, evtHandler) { | |
return o.removeEventListener(evtName, evtHandler, false); | |
} : function(o, evtName, evtHandler) { | |
return o.detachEvent('on' + evtName, evtHandler); | |
}); | |
preventDefault = function(e) { | |
if (e.preventDefault) { | |
e.preventDefault(); | |
} else { | |
e.returnValue = false; | |
e.cancelBubble = true; | |
} | |
return false; | |
}; | |
return { | |
add: add, | |
preventDefault: preventDefault, | |
remove: remove | |
}; | |
}()), | |
features: (function() { | |
var getAnimationFrame, | |
localAnimationFrame, | |
localFeatures, | |
prop, | |
styles, | |
testDiv, | |
transform; | |
testDiv = document.createElement('div'); | |
/** | |
* hat tip: paul irish | |
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/ | |
* https://gist.github.com/838785 | |
*/ | |
localAnimationFrame = (window.requestAnimationFrame | |
|| window.webkitRequestAnimationFrame | |
|| window.mozRequestAnimationFrame | |
|| window.oRequestAnimationFrame | |
|| window.msRequestAnimationFrame | |
|| null); | |
// apply to window, avoid "illegal invocation" errors in Chrome | |
getAnimationFrame = localAnimationFrame ? function() { | |
return localAnimationFrame.apply(window, arguments); | |
} : null; | |
function has(prop) { | |
// test for feature support | |
var result = testDiv.style[prop]; | |
return (result !== undefined ? prop : null); | |
} | |
// note local scope. | |
localFeatures = { | |
transform: { | |
ie: has('-ms-transform'), | |
moz: has('MozTransform'), | |
opera: has('OTransform'), | |
webkit: has('webkitTransform'), | |
w3: has('transform'), | |
prop: null // the normalized property value | |
}, | |
rotate: { | |
has3D: false, | |
prop: null | |
}, | |
getAnimationFrame: getAnimationFrame | |
}; | |
localFeatures.transform.prop = ( | |
localFeatures.transform.w3 || | |
localFeatures.transform.moz || | |
localFeatures.transform.webkit || | |
localFeatures.transform.ie || | |
localFeatures.transform.opera | |
); | |
function attempt(style) { | |
try { | |
testDiv.style[transform] = style; | |
} catch(e) { | |
// that *definitely* didn't work. | |
return false; | |
} | |
// if we can read back the style, it should be cool. | |
return !!testDiv.style[transform]; | |
} | |
if (localFeatures.transform.prop) { | |
// try to derive the rotate/3D support. | |
transform = localFeatures.transform.prop; | |
styles = { | |
css_2d: 'rotate(0deg)', | |
css_3d: 'rotate3d(0,0,0,0deg)' | |
}; | |
if (attempt(styles.css_3d)) { | |
localFeatures.rotate.has3D = true; | |
prop = 'rotate3d'; | |
} else if (attempt(styles.css_2d)) { | |
prop = 'rotate'; | |
} | |
localFeatures.rotate.prop = prop; | |
} | |
testDiv = null; | |
return localFeatures; | |
}()) | |
}; | |
// --- | |
// expose to global | |
window.sm2BarPlayers = players; | |
window.sm2BarPlayerOptions = playerOptions; | |
window.SM2BarPlayer = Player; | |
}(window)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment