aboutsummaryrefslogtreecommitdiff
path: root/public/dist/bar-ui.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/dist/bar-ui.js')
-rw-r--r--public/dist/bar-ui.js1677
1 files changed, 1677 insertions, 0 deletions
diff --git a/public/dist/bar-ui.js b/public/dist/bar-ui.js
new file mode 100644
index 0000000..b05e3e1
--- /dev/null
+++ b/public/dist/bar-ui.js
@@ -0,0 +1,1677 @@
+(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
+ */
+
+ /* global console, document, navigator, soundManager, window */
+
+ '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;
+
+ /**
+ * The following are player object event callback examples.
+ * Override globally by setting window.sm2BarPlayers.on = {}, or individually by window.sm2BarPlayers[0].on = {} etc.
+ * soundObject is provided for whileplaying() etc., but playback control should be done via the player object.
+ */
+ players.on = {
+ /*
+ play: function(player, soundObject) {
+ console.log('playing', player);
+ },
+ whileplaying: function(player, soundObject) {
+ console.log('whileplaying', player, soundObject);
+ },
+ finish: function(player, soundObject) {
+ // each sound
+ console.log('finish', player);
+ },
+ pause: function(player, soundObject) {
+ console.log('pause', player);
+ },
+ error: function(player, soundObject) {
+ console.log('error', player);
+ },
+ end: function(player, soundObject) {
+ // 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, oSound) {
+ if (method) {
+ // fire callback, passing current player and sound objects
+ if (exports.on && exports.on[method]) {
+ exports.on[method](exports, oSound);
+ } else if (players.on[method]) {
+ players.on[method](exports, oSound);
+ }
+ }
+ }
+
+ 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 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);
+
+ }
+
+ callback('whileplaying', this);
+
+ },
+
+ 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', this);
+ },
+
+ onpause: function() {
+ utils.css.swap(dom.o, 'playing', 'paused');
+ callback('pause', this);
+ },
+
+ onresume: function() {
+ utils.css.swap(dom.o, 'paused', 'playing');
+ },
+
+ 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', this);
+
+ // 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', this);
+
+ // next track?
+ item = playlistController.getNext();
+
+ // don't play the same item over and over again, if at end of playlist (excluding single item case.)
+ if (item && (playlistController.data.selectedIndex !== lastIndex || (playlistController.data.playlist.length === 1 && playlistController.data.loopMode))) {
+
+ playlistController.select(item);
+
+ stopOtherSounds();
+
+ // play next
+ this.play({
+ url: playlistController.getURL()
+ });
+
+ } else {
+
+ // end of playlist case
+
+ // explicitly stop?
+ // this.stop();
+
+ callback('end', this);
+
+ }
+
+ }
+
+ });
+
+ 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(liElement);
+
+ 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;
+ }
+
+ 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 initPlaylistController() {
+
+ // 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);
+ }
+
+ }
+
+ initPlaylistController();
+
+ 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;
+ }
+
+ return false;
+
+ }
+
+ function getActionData(target) {
+
+ // DOM measurements for volume slider
+
+ if (!target) {
+ return;
+ }
+
+ 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;
+ }
+
+ // 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, 'touchmove', actions.adjustVolume);
+ utils.events.add(document, 'mouseup', actions.releaseVolume);
+ utils.events.add(document, 'touchend', actions.releaseVolume);
+
+ // and apply right away
+ actions.adjustVolume(e);
+
+ }
+
+ }
+
+ function handleMouse(e) {
+
+ var target, barX, barWidth, x, clientX, newPosition, sound;
+
+ target = dom.progressTrack;
+
+ barX = utils.position.getOffX(target);
+ barWidth = target.offsetWidth;
+ clientX = utils.events.getClientX(e);
+
+ x = (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.events.remove(document, 'touchmove', handleMouse);
+
+ utils.css.remove(dom.o, 'grabbing');
+
+ utils.events.remove(document, 'mouseup', releaseMouse);
+ utils.events.remove(document, 'touchend', releaseMouse);
+
+ utils.events.preventDefault(e);
+
+ return false;
+
+ }
+
+ function handleProgressMouseDown(e) {
+
+ if (isRightClick(e)) {
+ return;
+ }
+
+ utils.css.add(dom.o, 'grabbing');
+
+ utils.events.add(document, 'mousemove', handleMouse);
+ utils.events.add(document, 'touchmove', handleMouse);
+ utils.events.add(document, 'mouseup', releaseMouse);
+ utils.events.add(document, 'touchend', releaseMouse);
+
+ handleMouse(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);
+ }
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+ function init() {
+
+ // init DOM?
+
+ if (!playerNode && window.console && console.warn) {
+ 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);
+
+ utils.events.add(dom.o, 'mousedown', handleMouseDown);
+ utils.events.add(dom.o, 'touchstart', handleMouseDown);
+ utils.events.add(dom.o, 'click', handleClick);
+ utils.events.add(dom.progressTrack, 'mousedown', handleProgressMouseDown);
+ utils.events.add(dom.progressTrack, 'touchstart', handleProgressMouseDown);
+
+ }
+
+ // ---
+
+ actionData = {
+
+ volume: {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ backgroundSize: 0
+ }
+
+ };
+
+ actions = {
+
+ play: function(offsetOrEvent) {
+
+ /**
+ * 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.
+ playlistController.playItemByOffset(offsetOrEvent);
+ return;
+ }
+
+ // 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) {
+ href = dom.playlist.getElementsByTagName('a')[0].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();
+
+ // 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();
+ }
+
+ },
+
+ resume: function() {
+
+ if (soundObject && soundObject.readyState) {
+ soundObject.resume();
+ }
+
+ },
+
+ stop: function() {
+
+ // 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();
+
+ },
+
+ 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;
+ }
+
+ // normalize between mouse and touch events
+ var clientX = utils.events.getClientX(e);
+
+ if (!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, (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, ((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, 'touchmove', actions.adjustVolume);
+ utils.events.remove(document, 'mouseup', actions.releaseVolume);
+ utils.events.remove(document, 'touchend', 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; // safety net
+ }
+ o.className = (o.className ? o.className + ' ' : '') + cStr;
+
+ }
+
+ function removeClass(o, cStr) {
+
+ if (!o || !cStr || !hasClass(o, cStr)) {
+ return;
+ }
+ 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) {
+
+ 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, getClientX;
+
+ 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;
+ };
+
+ getClientX = function(e) {
+ // normalize between desktop (mouse) and touch (mobile/tablet/?) events.
+ // note pageX for touch, which normalizes zoom/scroll/pan vs. clientX.
+ return (e && (e.clientX || (e.touches && e.touches[0] && e.touches[0].pageX)));
+ };
+
+ return {
+ add: add,
+ preventDefault: preventDefault,
+ remove: remove,
+ getClientX: getClientX
+ };
+
+ }()),
+
+ 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(propName) {
+
+ // test for feature support
+ return (testDiv.style[propName] !== undefined ? propName : 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));