1/*jslint plusplus: true, white: true, nomen: true */ 2/*global console, document, navigator, soundManager, window */ 3 4(function(window) { 5 6 /** 7 * SoundManager 2: "Bar UI" player 8 * Copyright (c) 2014, Scott Schiller. All rights reserved. 9 * http://www.schillmania.com/projects/soundmanager2/ 10 * Code provided under BSD license. 11 * http://schillmania.com/projects/soundmanager2/license.txt 12 */ 13 14 "use strict"; 15 16 var Player, 17 players = [], 18 // CSS selector that will get us the top-level DOM node for the player UI. 19 playerSelector = '.sm2-bar-ui', 20 playerOptions, 21 utils; 22 23 /** 24 * Slightly hackish: event callbacks. 25 * Override globally by setting window.sm2BarPlayers.on = {}, or individually by window.sm2BarPlayers[0].on = {} etc. 26 */ 27 players.on = { 28 /* 29 play: function(player) { 30 console.log('playing', player); 31 }, 32 pause: function(player) { 33 console.log('pause', player); 34 }, 35 error: function(player) { 36 console.log('error', player); 37 }, 38 stop: function(player) { 39 console.log('stop', player); 40 } 41 */ 42 }; 43 44 playerOptions = { 45 // useful when multiple players are in use, or other SM2 sounds are active etc. 46 stopOtherSounds: true, 47 // CSS class to let the browser load the URL directly e.g., <a href="foo.mp3" class="sm2-exclude">download foo.mp3</a> 48 excludeClass: 'sm2-exclude' 49 }; 50 51 soundManager.setup({ 52 // trade-off: higher UI responsiveness (play/progress bar), but may use more CPU. 53 html5PollingInterval: 50, 54 flashVersion: 9 55 }); 56 57 soundManager.onready(function() { 58 59 var nodes, i, j; 60 61 nodes = utils.dom.getAll(playerSelector); 62 63 if (nodes && nodes.length) { 64 for (i=0, j=nodes.length; i<j; i++) { 65 players.push(new Player(nodes[i])); 66 } 67 } 68 69 }); 70 71 /** 72 * player bits 73 */ 74 75 Player = function(playerNode) { 76 77 var css, dom, extras, playlistController, soundObject, actions, actionData, defaultItem, defaultVolume, firstOpen, exports; 78 79 css = { 80 disabled: 'disabled', 81 selected: 'selected', 82 active: 'active', 83 legacy: 'legacy', 84 noVolume: 'no-volume', 85 playlistOpen: 'playlist-open' 86 }; 87 88 dom = { 89 o: null, 90 playlist: null, 91 playlistTarget: null, 92 playlistContainer: null, 93 time: null, 94 player: null, 95 progress: null, 96 progressTrack: null, 97 progressBar: null, 98 duration: null, 99 volume: null 100 }; 101 102 // prepended to tracks when a sound fails to load/play 103 extras = { 104 loadFailedCharacter: '<span title="Failed to load/play." class="load-error">â</span>' 105 }; 106 107 function stopOtherSounds() { 108 109 if (playerOptions.stopOtherSounds) { 110 soundManager.stopAll(); 111 } 112 113 } 114 115 function callback(method) { 116 if (method) { 117 // fire callback, passing current turntable object 118 if (exports.on && exports.on[method]) { 119 exports.on[method](exports); 120 } else if (players.on[method]) { 121 players.on[method](exports); 122 } 123 } 124 } 125 126 function getTime(millisec, useString) { 127 128 // convert milliseconds to hh:mm:ss, return as object literal or string 129 var seconds = (millisec / 1000).toFixed(0); 130 var minutes = Math.floor(seconds / 60); 131 var hours = ""; 132 if (minutes > 59) { 133 hours = Math.floor(minutes / 60); 134 hours = (hours >= 10) ? hours : "0" + hours; 135 minutes = minutes - (hours * 60); 136 minutes = (minutes >= 10) ? minutes : "0" + minutes; 137 } 138 139 seconds = Math.floor(seconds % 60); 140 seconds = (seconds >= 10) ? seconds : "0" + seconds; 141 if (hours != "") { 142 return hours + ":" + minutes + ":" + seconds; 143 } 144 return minutes + ":" + seconds; 145 } 146 147 function setTitle(item) { 148 149 // given a link, update the "now playing" UI. 150 151 // if this is an <li> with an inner link, grab and use the text from that. 152 var links = item.getElementsByTagName('a'); 153 154 if (links.length) { 155 item = links[0]; 156 } 157 158 // remove any failed character sequence, also 159 dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML.replace(extras.loadFailedCharacter, '') + '</li></ul>'; 160 161 if (dom.playlistTarget.getElementsByTagName('li')[0].scrollWidth > dom.playlistTarget.offsetWidth) { 162 // this item can use <marquee>, in fact. 163 dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li><marquee>' + item.innerHTML + '</marquee></li></ul>'; 164 } 165 166 } 167 168 function makeSound(url) { 169 170 var sound = soundManager.createSound({ 171 172 url: url, 173 174 volume: defaultVolume, 175 176 whileplaying: function() { 177 var progressMaxLeft = 100, 178 left, 179 width; 180 181 left = Math.min(progressMaxLeft, Math.max(0, (progressMaxLeft * (this.position / this.durationEstimate)))) + '%'; 182 width = Math.min(100, Math.max(0, (100 * this.position / this.durationEstimate))) + '%'; 183 184 if (this.duration) { 185 186 dom.progress.style.left = left; 187 dom.progressBar.style.width = width; 188 189 // TODO: only write changes 190 dom.time.innerHTML = getTime(this.position, true); 191 192 } 193 194 }, 195 196 onbufferchange: function(isBuffering) { 197 if (isBuffering) { 198 utils.css.add(dom.o, 'buffering'); 199 } else { 200 utils.css.remove(dom.o, 'buffering'); 201 } 202 203 }, 204 205 onplay: function() { 206 jQuery(".loader").show(); 207 callback('play'); 208 utils.css.swap(dom.o, 'paused', 'playing'); 209 jQuery(".loader").hide(); 210 }, 211 212 onpause: function() { 213 utils.css.swap(dom.o, 'playing', 'paused'); 214 callback('pause'); 215 }, 216 217 onresume: function() { 218 utils.css.swap(dom.o, 'paused', 'playing'); 219 }, 220 221 whileloading: function() {
222 console.log(this.id); 223 if (!this.isHTML5) { 224 dom.duration.innerHTML = getTime(this.durationEstimate, true); 225 } 226 227 }, 228 229 onload: function(ok) { 230 jQuery(".loader").show(); 231 if (ok) { 232 jQuery(".playercontent").data('sound-id', this.id); 233 jQuery(".loader").hide(); 234 dom.duration.innerHTML = getTime(this.duration, true); 235 236 } else if (this._iO && this._iO.onerror) { 237 jQuery(".loader").hide(); 238 this._iO.onerror(); 239 240 } 241 242 }, 243 244 onerror: function() { 245 246 // sound failed to load. 247 var item, element, html; 248 249 item = playlistController.getItem(); 250 251 if (item) { 252 253 // note error, delay 2 seconds and advance? 254 // playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML + '</li></ul>'; 255 256 if (extras.loadFailedCharacter) { 257 dom.playlistTarget.innerHTML = dom.playlistTarget.innerHTML.replace('<li>' ,'<li>' + extras.loadFailedCharacter + ' '); 258 if (playlistController.data.playlist && playlistController.data.playlist[playlistController.data.selectedIndex]) { 259 element = playlistController.data.playlist[playlistController.data.selectedIndex].getElementsByTagName('a')[0]; 260 html = element.innerHTML; 261 if (html.indexOf(extras.loadFailedCharacter) === -1) { 262 element.innerHTML = extras.loadFailedCharacter + ' ' + html; 263 } 264 } 265 } 266 267 } 268 269 callback('error'); 270 271 // load next, possibly with delay. 272 273 if (navigator.userAgent.match(/mobile/i)) { 274 // mobile will likely block the next play() call if there is a setTimeout() - so don't use one here. 275 actions.next(); 276 } else { 277 if (playlistController.data.timer) { 278 window.clearTimeout(playlistController.data.timer); 279 } 280 playlistController.data.timer = window.setTimeout(actions.next, 2000); 281 } 282 283 }, 284 285 onstop: function() { 286 287 utils.css.remove(dom.o, 'playing'); 288 callback('stop'); 289 290 }, 291 292 onfinish: function() { 293 294 var lastIndex, item; 295 296 utils.css.remove(dom.o, 'playing'); 297 298 dom.progress.style.left = '0%'; 299 300 lastIndex = playlistController.data.selectedIndex; 301 302 // next track? 303 item = playlistController.getNext(); 304 305 // don't play the same item over and over again, if at end of playlist etc. 306 if (item && playlistController.data.selectedIndex !== lastIndex) { 307 308 playlistController.select(item); 309 310 setTitle(item); 311 312 stopOtherSounds(); 313 314 // play next 315 this.play({ 316 url: playlistController.getURL() 317 }); 318 319 }/* else { 320 // explicitly stop? 321 // this.stop(); 322 }*/ 323 324 } 325 326 }); 327sound.load; 328 return sound; 329 330 } 331 332 function playLink(link) { 333 334 // if a link is OK, play it. 335 336 if (soundManager.canPlayURL(link.href)) { 337 338 // if there's a timer due to failure to play one track, cancel it. 339 // catches case when user may use previous/next after an error. 340 if (playlistController.data.timer) { 341 window.clearTimeout(playlistController.data.timer); 342 playlistController.data.timer = null; 343 } 344 345 if (!soundObject) { 346 soundObject = makeSound(link.href); 347 } 348 349 // required to reset pause/play state on iOS so whileplaying() works? odd. 350 soundObject.stop(); 351 352 playlistController.select(link.parentNode); 353 354 // TODO: ancestor('li') 355 setTitle(link.parentNode); 356 357 // reset the UI 358 // TODO: function that also resets/hides timing info. 359 dom.progress.style.left = '0px'; 360 dom.progressBar.style.width = '0px'; 361 362 stopOtherSounds(); 363 364 soundObject.play({ 365 url: link.href, 366 position: 0 367 }); 368 369 } 370 371 } 372 373 function PlaylistController() { 374 375 var data; 376 377 data = { 378 379 // list of nodes? 380 playlist: [], 381 382 // shuffledIndex: [], 383 384 // selection 385 selectedIndex: 0, 386 387 // shuffleMode: false, 388 389 loopMode: false, 390 391 timer: null 392 393 }; 394 395 function getPlaylist() { 396 397 return data.playlist; 398 399 } 400 401 function getItem(offset) { 402 403 var list, 404 item; 405 406 // given the current selection (or an offset), return the current item. 407 408 // if currently null, may be end of list case. bail. 409 if (data.selectedIndex === null) { 410 return offset; 411 } 412 413 list = getPlaylist(); 414 415 // use offset if provided, otherwise take default selected. 416 offset = (offset !== undefined ? offset : data.selectedIndex); 417 418 // safety check - limit to between 0 and list length 419 offset = Math.max(0, Math.min(offset, list.length)); 420 421 item = list[offset]; 422 423 return item; 424 425 } 426 427 function findOffsetFromItem(item) { 428 429 // given an <li> item, find it in the playlist array and return the index. 430 var list, 431 i, 432 j, 433 offset; 434 435 offset = -1; 436 437 list = getPlaylist(); 438 439 if (list) { 440 441 for (i=0, j=list.length; i<j; i++) { 442 if (list[i] === item) { 443 offset = i; 444 break; 445 } 446 } 447 448 } 449 450 return offset; 451 452 } 453 454 function getNext() { 455 456 // don't increment if null. 457 if (data.selectedIndex !== null) { 458 data.selectedIndex++; 459 } 460 461 if (data.playlist.length > 1) { 462 463 if (data.selectedIndex >= data.playlist.length) { 464 465 if (data.loopMode) { 466 467 // loop to beginning 468 data.selectedIndex = 0; 469 470 } else { 471 472 // no change 473 data.selectedIndex--; 474 475 // end playback 476 // data.selectedIndex = null; 477 478 } 479 480 } 481 482 } else { 483 484 data.selectedIndex = null; 485 486 } 487 488 return getItem(); 489 490 } 491 492 function getPrevious() { 493 494 data.selectedIndex--; 495 496 if (data.selectedIndex < 0) { 497 // wrapping around beginning of list? loop or exit. 498 if (data.loopMode) { 499 data.selectedIndex = data.playlist.length - 1; 500 } else { 501 // undo 502 data.selectedIndex++; 503 } 504 } 505 506 return getItem(); 507 508 } 509 510 function resetLastSelected() { 511 512 // remove UI highlight(s) on selected items. 513 var items, 514 i, j; 515 516 items = utils.dom.getAll(dom.playlist, '.' + css.selected); 517 518 for (i=0, j=items.length; i<j; i++) { 519 utils.css.remove(items[i], css.selected); 520 } 521 522 } 523 524 function select(item) { 525 526 var offset, 527 itemTop, 528 itemBottom, 529 containerHeight, 530 scrollTop, 531 itemPadding; 532 533 // remove last selected, if any 534 resetLastSelected(); 535 536 if (item) { 537 538 utils.css.add(item, css.selected); 539 540 itemTop = item.offsetTop; 541 itemBottom = itemTop + item.offsetHeight; 542 containerHeight = dom.playlistContainer.offsetHeight; 543 scrollTop = dom.playlist.scrollTop; 544 itemPadding = 8; 545 546 if (itemBottom > containerHeight + scrollTop) { 547 // bottom-align 548 dom.playlist.scrollTop = itemBottom - containerHeight + itemPadding; 549 } else if (itemTop < scrollTop) { 550 // top-align 551 dom.playlist.scrollTop = item.offsetTop - itemPadding; 552 } 553 554 } 555 556 // update selected offset, too. 557 offset = findOffsetFromItem(item); 558 559 data.selectedIndex = offset; 560 561 } 562 563 function playItemByOffset(offset) { 564 565 var item; 566 567 offset = (offset || 0); 568 569 item = getItem(offset); 570 571 if (item) { 572 playLink(item.getElementsByTagName('a')[0]); 573 } 574 575 } 576 577 function getURL() { 578 579 // return URL of currently-selected item 580 var item, url; 581 582 item = getItem(); 583 584 if (item) { 585 url = item.getElementsByTagName('a')[0].href; 586 } 587 588 return url; 589 590 } 591 592 function refreshDOM() { 593 594 // get / update playlist from DOM 595 596 if (!dom.playlist) { 597 if (window.console && console.warn) { 598 console.warn('refreshDOM(): playlist node not found?'); 599 } 600 return false;
601 } 602 603 data.playlist = dom.playlist.getElementsByTagName('li'); 604 605 } 606 607 function initDOM() { 608 609 dom.playlistTarget = utils.dom.get(dom.o, '.sm2-playlist-target'); 610 dom.playlistContainer = utils.dom.get(dom.o, '.sm2-playlist-drawer'); 611 dom.playlist = utils.dom.get(dom.o, '.sm2-playlist-bd'); 612 613 } 614 615 function init() { 616 617 // inherit the default SM2 volume 618 defaultVolume = soundManager.defaultOptions.volume; 619 620 initDOM(); 621 refreshDOM(); 622 623 // animate playlist open, if HTML classname indicates so. 624 if (utils.css.has(dom.o, css.playlistOpen)) { 625 // hackish: run this after API has returned 626 window.setTimeout(function() { 627 actions.menu(true); 628 }, 1); 629 } 630 631 } 632 633 init(); 634 635 return { 636 data: data, 637 refresh: refreshDOM, 638 getNext: getNext, 639 getPrevious: getPrevious, 640 getItem: getItem, 641 getURL: getURL, 642 playItemByOffset: playItemByOffset, 643 select: select 644 }; 645 646 } 647 648 function isRightClick(e) { 649 650 // only pay attention to left clicks. old IE differs where there's no e.which, but e.button is 1 on left click. 651 if (e && ((e.which && e.which === 2) || (e.which === undefined && e.button !== 1))) { 652 // http://www.quirksmode.org/js/events_properties.html#button 653 return true; 654 } 655 656 } 657 658 function getActionData(target) { 659 660 // DOM measurements for volume slider 661 662 if (!target) { 663 return false; 664 } 665 666 actionData.volume.x = utils.position.getOffX(target); 667 actionData.volume.y = utils.position.getOffY(target); 668 669 actionData.volume.width = target.offsetWidth; 670 actionData.volume.height = target.offsetHeight; 671 672 // potentially dangerous: this should, but may not be a percentage-based value. 673 actionData.volume.backgroundSize = parseInt(utils.style.get(target, 'background-size'), 10); 674 675 // IE gives pixels even if background-size specified as % in CSS. Boourns. 676 if (window.navigator.userAgent.match(/msie|trident/i)) { 677 actionData.volume.backgroundSize = (actionData.volume.backgroundSize / actionData.volume.width) * 100; 678 } 679 680 } 681 682 function handleMouseDown(e) { 683 684 var links, 685 target; 686 687 target = e.target || e.srcElement; 688 689 if (isRightClick(e)) { 690 return true; 691 } 692 693 // normalize to <a>, if applicable. 694 if (target.nodeName.toLowerCase() !== 'a') { 695 696 links = target.getElementsByTagName('a'); 697 if (links && links.length) { 698 target = target.getElementsByTagName('a')[0]; 699 } 700 701 } 702 703 if (utils.css.has(target, 'sm2-volume-control')) { 704 705 // drag case for volume 706 707 getActionData(target); 708 709 utils.events.add(document, 'mousemove', actions.adjustVolume); 710 utils.events.add(document, 'mouseup', actions.releaseVolume); 711 712 // and apply right away 713 return actions.adjustVolume(e); 714 715 } 716 717 } 718 719 function handleClick(e) { 720 721 var evt, 722 target, 723 offset, 724 targetNodeName, 725 methodName, 726 href, 727 handled; 728 729 evt = (e || window.event); 730 731 target = evt.target || evt.srcElement; 732 733 if (target && target.nodeName) { 734 735 targetNodeName = target.nodeName.toLowerCase(); 736 737 if (targetNodeName !== 'a') { 738 739 // old IE (IE 8) might return nested elements inside the <a>, eg., <b> etc. Try to find the parent <a>. 740 741 if (target.parentNode) { 742 743 do { 744 target = target.parentNode; 745 targetNodeName = target.nodeName.toLowerCase(); 746 } while (targetNodeName !== 'a' && target.parentNode); 747 748 if (!target) { 749 // something went wrong. bail. 750 return false; 751 } 752 753 } 754 755 } 756 757 if (targetNodeName === 'a') { 758 759 // yep, it's a link. 760 761 href = target.href; 762 763 if (soundManager.canPlayURL(href)) { 764 765 // not excluded 766 if (!utils.css.has(target, playerOptions.excludeClass)) { 767 768 // find this in the playlist 769 770 playLink(target); 771 772 handled = true; 773 774 } 775 776 } else { 777 778 // is this one of the action buttons, eg., play/pause, volume, etc.? 779 offset = target.href.lastIndexOf('#'); 780 781 if (offset !== -1) { 782 783 methodName = target.href.substr(offset+1); 784 785 if (methodName && actions[methodName]) { 786 handled = true; 787 actions[methodName](e); 788 } 789 790 } 791 792 } 793 794 // fall-through case 795 796 if (handled) { 797 // prevent browser fall-through 798 return utils.events.preventDefault(evt); 799 } 800 801 } 802 803 } 804 805 } 806 807 function handleMouse(e) { 808 809 var target, barX, barWidth, x, newPosition, sound; 810 811 target = dom.progressTrack; 812 813 barX = utils.position.getOffX(target); 814 barWidth = target.offsetWidth; 815 816 x = (e.clientX - barX); 817 818 newPosition = (x / barWidth); 819 820 sound = soundObject; 821 822 if (sound && sound.duration) { 823 824 sound.setPosition(sound.duration * newPosition); 825 826 // a little hackish: ensure UI updates immediately with current position, even if audio is buffering and hasn't moved there yet. 827 if (sound._iO && sound._iO.whileplaying) { 828 sound._iO.whileplaying.apply(sound); 829 } 830 831 } 832 833 if (e.preventDefault) { 834 e.preventDefault(); 835 } 836 837 return false;
838 839 } 840 841 function releaseMouse(e) { 842 843 utils.events.remove(document, 'mousemove', handleMouse); 844 845 utils.css.remove(dom.o, 'grabbing'); 846 847 utils.events.remove(document, 'mouseup', releaseMouse); 848 849 utils.events.preventDefault(e); 850 851 return false; 852 853 } 854 855 function init() { 856 857 // init DOM? 858 859 if (!playerNode) { 860 console.warn('init(): No playerNode element?'); 861 } 862 863 dom.o = playerNode; 864 865 // are we dealing with a crap browser? apply legacy CSS if so. 866 if (window.navigator.userAgent.match(/msie [678]/i)) { 867 utils.css.add(dom.o, css.legacy); 868 } 869 870 if (window.navigator.userAgent.match(/mobile/i)) { 871 // majority of mobile devices don't let HTML5 audio set volume. 872 utils.css.add(dom.o, css.noVolume); 873 } 874 875 dom.progress = utils.dom.get(dom.o, '.sm2-progress-ball'); 876 877 dom.progressTrack = utils.dom.get(dom.o, '.sm2-progress-track'); 878 879 dom.progressBar = utils.dom.get(dom.o, '.sm2-progress-bar'); 880 881 dom.volume = utils.dom.get(dom.o, 'a.sm2-volume-control'); 882 883 // measure volume control dimensions 884 if (dom.volume) { 885 getActionData(dom.volume); 886 } 887 888 dom.duration = utils.dom.get(dom.o, '.sm2-inline-duration'); 889 890 dom.time = utils.dom.get(dom.o, '.sm2-inline-time'); 891 892 playlistController = new PlaylistController(); 893 894 defaultItem = playlistController.getItem(0); 895 896 playlistController.select(defaultItem); 897 898 setTitle(defaultItem); 899 900 utils.events.add(dom.o, 'mousedown', handleMouseDown); 901 902 utils.events.add(dom.o, 'click', handleClick); 903 904 utils.events.add(dom.progressTrack, 'mousedown', function(e) { 905 906 if (isRightClick(e)) { 907 return true; 908 } 909 910 utils.css.add(dom.o, 'grabbing'); 911 utils.events.add(document, 'mousemove', handleMouse); 912 utils.events.add(document, 'mouseup', releaseMouse); 913 914 return handleMouse(e); 915 916 }); 917 918 } 919 920 // --- 921 922 actionData = { 923 924 volume: { 925 x: 0, 926 y: 0, 927 width: 0, 928 height: 0, 929 backgroundSize: 0 930 } 931 932 }; 933 934 actions = { 935 936 play: function(offsetOrEvent) { 937 938 /** 939 * This is an overloaded function that takes mouse/touch events or offset-based item indices. 940 * Remember, "auto-play" will not work on mobile devices unless this function is called immediately from a touch or click event. 941 * 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 } 942 */ 943 944 var target, 945 href, 946 e; 947 948 if (offsetOrEvent !== undefined && !isNaN(offsetOrEvent)) { 949 // smells like a number. 950 return playlistController.playItemByOffset(offsetOrEvent); 951 } 952 953 // DRY things a bit 954 e = offsetOrEvent; 955 956 if (e && e.target) { 957 958 target = e.target || e.srcElement; 959 960 href = target.href; 961 962 } 963 964 // haaaack - if null due to no event, OR '#' due to play/pause link, get first link from playlist 965 if (!href || href.indexOf('#') !== -1) { 966 href = dom.playlist.getElementsByTagName('a')[0].href; 967 } 968 969 if (!soundObject) { 970 soundObject = makeSound(href); 971 } 972 973 // edge case: if the current sound is not playing, stop all others. 974 if (!soundObject.playState) { 975 stopOtherSounds(); 976 } 977 978 // TODO: if user pauses + unpauses a sound that had an error, try to play next? 979 soundObject.togglePause(); 980 981 // special case: clear "play next" timeout, if one exists. 982 // edge case: user pauses after a song failed to load. 983 if (soundObject.paused && playlistController.data.timer) { 984 window.clearTimeout(playlistController.data.timer); 985 playlistController.data.timer = null; 986 } 987 988 }, 989 990 pause: function() { 991 992 if (soundObject && soundObject.readyState) { 993 soundObject.pause(); 994 } 995 996 }, 997 998 resume: function() { 999 1000 if (soundObject && soundObject.readyState) { 1001 soundObject.resume(); 1002 } 1003 1004 }, 1005 1006 stop: function() { 1007 1008 // just an alias for pause, really. 1009 // don't actually stop because that will mess up some UI state, i.e., dragging the slider. 1010 return actions.pause(); 1011 1012 }, 1013 1014 next: function(/* e */) { 1015 1016 var item, lastIndex; 1017 1018 // special case: clear "play next" timeout, if one exists. 1019 if (playlistController.data.timer) { 1020 window.clearTimeout(playlistController.data.timer); 1021 playlistController.data.timer = null; 1022 } 1023 1024 lastIndex = playlistController.data.selectedIndex; 1025 1026 item = playlistController.getNext(true); 1027 1028 // don't play the same item again 1029 if (item && playlistController.data.selectedIndex !== lastIndex) { 1030 playLink(item.getElementsByTagName('a')[0]); 1031 } 1032 1033 }, 1034 1035 prev: function(/* e */) { 1036 1037 var item, lastIndex; 1038 1039 lastIndex = playlistController.data.selectedIndex; 1040 1041 item = playlistController.getPrevious(); 1042 1043 // don't play the same item again 1044 if (item && playlistController.data.selectedIndex !== lastIndex) { 1045 playLink(item.getElementsByTagName('a')[0]); 1046 } 1047 1048 }, 1049 1050 shuffle: function(e) { 1051 1052 // NOTE: not implemented yet. 1053 1054 var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.shuffle')); 1055 1056 if (target && !utils.css.has(target, css.disabled)) { 1057 utils.css.toggle(target.parentNode, css.active); 1058 playlistController.data.shuffleMode = !playlistController.data.shuffleMode; 1059 } 1060 1061 }, 1062 1063 repeat: function(e) { 1064 1065 var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.repeat')); 1066 1067 if (target && !utils.css.has(target, css.disabled)) { 1068 utils.css.toggle(target.parentNode, css.active); 1069 playlistController.data.loopMode = !playlistController.data.loopMode; 1070 } 1071 1072 }, 1073 1074 menu: function(ignoreToggle) { 1075 1076 var isOpen; 1077 1078 isOpen = utils.css.has(dom.o, css.playlistOpen); 1079 1080 // hackish: reset scrollTop in default first open case. odd, but some browsers have a non-zero scroll offset the first time the playlist opens. 1081 if (playlistController && !playlistController.data.selectedIndex && !firstOpen) { 1082 dom.playlist.scrollTop = 0; 1083 firstOpen = true; 1084 } 1085 1086 // sniff out booleans from mouse events, as this is referenced directly by event handlers. 1087 if (typeof ignoreToggle !== 'boolean' || !ignoreToggle) { 1088 1089 if (!isOpen) { 1090 jQuery(".sm2-bar-ui .sm2-inline-element:first-of-type").css('border-bottom-left-radius', '0px'); 1091 } 1092 if (isOpen) { 1093 jQuery(".sm2-bar-ui .sm2-inline-element:first-of-type").css('border-bottom-left-radius', '6px'); 1094 } 1095 if (!isOpen) { 1096 // explicitly set height:0, so the first closed -> open animation runs properly 1097 dom.playlistContainer.style.height = '0px'; 1098 } 1099 1100 isOpen = utils.css.toggle(dom.o, css.playlistOpen); 1101 1102 } 1103 1104 // playlist 1105 dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px'; 1106 1107 }, 1108 1109 adjustVolume: function(e) { 1110 1111 /** 1112 * NOTE: this is the mousemove() event handler version. 1113 * Use setVolume(50), etc., to assign volume directly. 1114 */ 1115 1116 var backgroundMargin, 1117 pixelMargin, 1118 target, 1119 value, 1120 volume; 1121 1122 value = 0; 1123 1124 target = dom.volume; 1125 1126 // safety net 1127 if (e === undefined) { 1128 return false;
1129 } 1130 1131 if (!e || e.clientX === undefined) { 1132 // called directly or with a non-mouseEvent object, etc. 1133 // proxy to the proper method. 1134 if (arguments.length && window.console && window.console.warn) { 1135 console.warn('Bar UI: call setVolume(' + e + ') instead of adjustVolume(' + e + ').'); 1136 } 1137 return actions.setVolume.apply(this, arguments); 1138 } 1139 1140 // based on getStyle() result 1141 // figure out spacing around background image based on background size, eg. 60% background size. 1142 // 60% wide means 20% margin on each side. 1143 backgroundMargin = (100 - actionData.volume.backgroundSize) / 2; 1144 1145 // relative position of mouse over element 1146 value = Math.max(0, Math.min(1, (e.clientX - actionData.volume.x) / actionData.volume.width)); 1147 1148 target.style.clip = 'rect(0px, ' + (actionData.volume.width * value) + 'px, ' + actionData.volume.height + 'px, ' + (actionData.volume.width * (backgroundMargin/100)) + 'px)'; 1149 1150 // determine logical volume, including background margin 1151 pixelMargin = ((backgroundMargin/100) * actionData.volume.width); 1152 1153 volume = Math.max(0, Math.min(1, ((e.clientX - actionData.volume.x) - pixelMargin) / (actionData.volume.width - (pixelMargin*2)))) * 100; 1154 1155 // set volume 1156 if (soundObject) { 1157 soundObject.setVolume(volume); 1158 } 1159 1160 defaultVolume = volume; 1161 1162 return utils.events.preventDefault(e); 1163 1164 }, 1165 1166 releaseVolume: function(/* e */) { 1167 1168 utils.events.remove(document, 'mousemove', actions.adjustVolume); 1169 utils.events.remove(document, 'mouseup', actions.releaseVolume); 1170 1171 }, 1172 1173 setVolume: function(volume) { 1174 1175 // set volume (0-100) and update volume slider UI. 1176 1177 var backgroundSize, 1178 backgroundMargin, 1179 backgroundOffset, 1180 target, 1181 from, 1182 to; 1183 1184 if (volume === undefined || isNaN(volume)) { 1185 return; 1186 } 1187 1188 if (dom.volume) { 1189 1190 target = dom.volume; 1191 1192 // based on getStyle() result 1193 backgroundSize = actionData.volume.backgroundSize; 1194 1195 // figure out spacing around background image based on background size, eg. 60% background size. 1196 // 60% wide means 20% margin on each side. 1197 backgroundMargin = (100 - backgroundSize) / 2; 1198 1199 // margin as pixel value relative to width 1200 backgroundOffset = actionData.volume.width * (backgroundMargin/100); 1201 1202 from = backgroundOffset; 1203 to = from + ((actionData.volume.width - (backgroundOffset*2)) * (volume/100)); 1204 1205 target.style.clip = 'rect(0px, ' + to + 'px, ' + actionData.volume.height + 'px, ' + from + 'px)'; 1206 1207 } 1208 1209 // apply volume to sound, as applicable 1210 if (soundObject) { 1211 soundObject.setVolume(volume); 1212 } 1213 1214 defaultVolume = volume; 1215 1216 } 1217 1218 }; 1219 1220 init(); 1221 1222 // TODO: mixin actions -> exports 1223 1224 exports = { 1225 // Per-instance events: window.sm2BarPlayers[0].on = { ... } etc. See global players.on example above for reference. 1226 on: null, 1227 actions: actions, 1228 dom: dom, 1229 playlistController: playlistController 1230 }; 1231 soundObject = makeSound(playlistController.getURL()); 1232 1233 return exports; 1234 1235 }; 1236 1237 // barebones utilities for logic, CSS, DOM, events etc. 1238 1239 utils = { 1240 1241 array: (function() { 1242 1243 function compare(property) { 1244 1245 var result; 1246 1247 return function(a, b) { 1248 1249 if (a[property] < b[property]) { 1250 result = -1; 1251 } else if (a[property] > b[property]) { 1252 result = 1; 1253 } else { 1254 result = 0; 1255 } 1256 return result; 1257 }; 1258 1259 } 1260 1261 function shuffle(array) { 1262 1263 // Fisher-Yates shuffle algo 1264 1265 var i, j, temp; 1266 1267 for (i = array.length - 1; i > 0; i--) { 1268 j = Math.floor(Math.random() * (i+1)); 1269 temp = array[i]; 1270 array[i] = array[j]; 1271 array[j] = temp; 1272 } 1273 1274 return array; 1275 1276 } 1277 1278 return { 1279 compare: compare, 1280 shuffle: shuffle 1281 }; 1282 1283 }()), 1284 1285 css: (function() { 1286 1287 function hasClass(o, cStr) { 1288 1289 return (o.className !== undefined ? new RegExp('(^|\\s)' + cStr + '(\\s|$)').test(o.className) : false); 1290 1291 } 1292 1293 function addClass(o, cStr) { 1294 1295 if (!o || !cStr || hasClass(o, cStr)) { 1296 return false; // safety net 1297 } 1298 o.className = (o.className ? o.className + ' ' : '') + cStr; 1299 1300 } 1301 1302 function removeClass(o, cStr) { 1303 1304 if (!o || !cStr || !hasClass(o, cStr)) { 1305 return false;
1306 } 1307 o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), ''); 1308 1309 } 1310 1311 function swapClass(o, cStr1, cStr2) { 1312 1313 var tmpClass = { 1314 className: o.className 1315 }; 1316 1317 removeClass(tmpClass, cStr1); 1318 addClass(tmpClass, cStr2); 1319 1320 o.className = tmpClass.className; 1321 1322 } 1323 1324 function toggleClass(o, cStr) { 1325 1326 var found, 1327 method; 1328 1329 found = hasClass(o, cStr); 1330 1331 method = (found ? removeClass : addClass); 1332 1333 method(o, cStr); 1334 1335 // indicate the new state... 1336 return !found; 1337 1338 } 1339 1340 return { 1341 has: hasClass, 1342 add: addClass, 1343 remove: removeClass, 1344 swap: swapClass, 1345 toggle: toggleClass 1346 }; 1347 1348 }()), 1349 1350 dom: (function() { 1351 1352 function getAll(param1, param2) { 1353 1354 var node, 1355 selector, 1356 results; 1357 1358 if (arguments.length === 1) { 1359 1360 // .selector case 1361 node = document.documentElement; 1362 // first param is actually the selector 1363 selector = param1; 1364 1365 } else { 1366 1367 // node, .selector 1368 node = param1; 1369 selector = param2; 1370 1371 } 1372 1373 // sorry, IE 7 users; IE 8+ required. 1374 if (node && node.querySelectorAll) { 1375 1376 results = node.querySelectorAll(selector); 1377 1378 } 1379 1380 return results; 1381 1382 } 1383 1384 function get(/* parentNode, selector */) { 1385 1386 var results = getAll.apply(this, arguments); 1387 1388 // hackish: if an array, return the last item. 1389 if (results && results.length) { 1390 return results[results.length-1]; 1391 } 1392 1393 // handle "not found" case 1394 return results && results.length === 0 ? null : results; 1395 1396 } 1397 1398 return { 1399 get: get, 1400 getAll: getAll 1401 }; 1402 1403 }()), 1404 1405 position: (function() { 1406 1407 function getOffX(o) { 1408 1409 // http://www.xs4all.nl/~ppk/js/findpos.html 1410 var curleft = 0; 1411 1412 if (o.offsetParent) { 1413 1414 while (o.offsetParent) { 1415 1416 curleft += o.offsetLeft; 1417 1418 o = o.offsetParent; 1419 1420 } 1421 1422 } else if (o.x) { 1423 1424 curleft += o.x; 1425 1426 } 1427 1428 return curleft; 1429 1430 } 1431 1432 function getOffY(o) { 1433 1434 // http://www.xs4all.nl/~ppk/js/findpos.html 1435 var curtop = 0; 1436 1437 if (o.offsetParent) { 1438 1439 while (o.offsetParent) { 1440 1441 curtop += o.offsetTop; 1442 1443 o = o.offsetParent; 1444 1445 } 1446 1447 } else if (o.y) { 1448 1449 curtop += o.y; 1450 1451 } 1452 1453 return curtop; 1454 1455 } 1456 1457 return { 1458 getOffX: getOffX, 1459 getOffY: getOffY 1460 }; 1461 1462 }()), 1463 1464 style: (function() { 1465 1466 function get(node, styleProp) { 1467 1468 // http://www.quirksmode.org/dom/getstyles.html 1469 var value; 1470 1471 if (node.currentStyle) { 1472 1473 value = node.currentStyle[styleProp]; 1474 1475 } else if (window.getComputedStyle) { 1476 1477 value = document.defaultView.getComputedStyle(node, null).getPropertyValue(styleProp); 1478 1479 } 1480 1481 return value; 1482 1483 } 1484 1485 return { 1486 get: get 1487 }; 1488 1489 }()), 1490 1491 events: (function() { 1492 1493 var add, remove, preventDefault; 1494 1495 add = function(o, evtName, evtHandler) { 1496 // return an object with a convenient detach method. 1497 var eventObject = { 1498 detach: function() { 1499 return remove(o, evtName, evtHandler); 1500 } 1501 }; 1502 if (window.addEventListener) { 1503 o.addEventListener(evtName, evtHandler, false); 1504 } else { 1505 o.attachEvent('on' + evtName, evtHandler); 1506 } 1507 return eventObject; 1508 }; 1509 1510 remove = (window.removeEventListener !== undefined ? function(o, evtName, evtHandler) { 1511 return o.removeEventListener(evtName, evtHandler, false); 1512 } : function(o, evtName, evtHandler) { 1513 return o.detachEvent('on' + evtName, evtHandler); 1514 }); 1515 1516 preventDefault = function(e) { 1517 if (e.preventDefault) { 1518 e.preventDefault(); 1519 } else { 1520 e.returnValue = false; 1521 e.cancelBubble = true; 1522 } 1523 return false;
1524 }; 1525 1526 return { 1527 add: add, 1528 preventDefault: preventDefault, 1529 remove: remove 1530 }; 1531 1532 }()), 1533 1534 features: (function() { 1535 1536 var getAnimationFrame, 1537 localAnimationFrame, 1538 localFeatures, 1539 prop, 1540 styles, 1541 testDiv, 1542 transform; 1543 1544 testDiv = document.createElement('div'); 1545 1546 /** 1547 * hat tip: paul irish 1548 * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 1549 * https://gist.github.com/838785 1550 */ 1551 1552 localAnimationFrame = (window.requestAnimationFrame 1553 || window.webkitRequestAnimationFrame 1554 || window.mozRequestAnimationFrame 1555 || window.oRequestAnimationFrame 1556 || window.msRequestAnimationFrame 1557 || null); 1558 1559 // apply to window, avoid "illegal invocation" errors in Chrome 1560 getAnimationFrame = localAnimationFrame ? function() { 1561 return localAnimationFrame.apply(window, arguments); 1562 } : null; 1563 1564 function has(prop) { 1565 1566 // test for feature support 1567 var result = testDiv.style[prop]; 1568 1569 return (result !== undefined ? prop : null); 1570 1571 } 1572 1573 // note local scope. 1574 localFeatures = { 1575 1576 transform: { 1577 ie: has('-ms-transform'), 1578 moz: has('MozTransform'), 1579 opera: has('OTransform'), 1580 webkit: has('webkitTransform'), 1581 w3: has('transform'), 1582 prop: null // the normalized property value 1583 }, 1584 1585 rotate: { 1586 has3D: false, 1587 prop: null 1588 }, 1589 1590 getAnimationFrame: getAnimationFrame 1591 1592 }; 1593 1594 localFeatures.transform.prop = ( 1595 localFeatures.transform.w3 || 1596 localFeatures.transform.moz || 1597 localFeatures.transform.webkit || 1598 localFeatures.transform.ie || 1599 localFeatures.transform.opera 1600 ); 1601 1602 function attempt(style) { 1603 1604 try { 1605 testDiv.style[transform] = style; 1606 } catch(e) { 1607 // that *definitely* didn't work. 1608 return false; 1609 } 1610 // if we can read back the style, it should be cool. 1611 return !!testDiv.style[transform]; 1612 1613 } 1614 1615 if (localFeatures.transform.prop) { 1616 1617 // try to derive the rotate/3D support. 1618 transform = localFeatures.transform.prop; 1619 styles = { 1620 css_2d: 'rotate(0deg)', 1621 css_3d: 'rotate3d(0,0,0,0deg)' 1622 }; 1623 1624 if (attempt(styles.css_3d)) { 1625 localFeatures.rotate.has3D = true; 1626 prop = 'rotate3d'; 1627 } else if (attempt(styles.css_2d)) { 1628 prop = 'rotate'; 1629 } 1630 1631 localFeatures.rotate.prop = prop; 1632 1633 } 1634 1635 testDiv = null; 1636 1637 return localFeatures; 1638 1639 }()) 1640 1641 }; 1642 1643 // --- 1644 1645 // expose to global 1646 window.sm2BarPlayers = players; 1647 window.sm2BarPlayerOptions = playerOptions; 1648 window.SM2BarPlayer = Player; 1649//if (playervars.autoplay == 1){ 1650// 1651//} 1652}(window));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.