(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., download foo.mp3 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: '' }; 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 = ''; if (extras.loadFailedCharacter) { dom.playlistTarget.innerHTML = dom.playlistTarget.innerHTML.replace('
  • ', '
  • ' + 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
  • 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 , 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 , eg., etc. Try to find the parent . 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 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));