@techiepi/green-audio-player
Version:
Green Audio Player
529 lines (470 loc) • 23.1 kB
JavaScript
class GreenAudioPlayer {
constructor(player, options) {
this.audioPlayer = typeof player === 'string' ? document.querySelector(player) : player;
const opts = options || {};
const audioElement = this.audioPlayer.innerHTML;
this.audioPlayer.classList.add('green-audio-player');
this.audioPlayer.innerHTML = GreenAudioPlayer.getTemplate() + audioElement;
this.isDevice = /ipad|iphone|ipod|android/i.test(window.navigator.userAgent.toLowerCase()) && !window.MSStream;
this.playPauseBtn = this.audioPlayer.querySelector('.play-pause-btn');
this.loading = this.audioPlayer.querySelector('.loading');
this.sliders = this.audioPlayer.querySelectorAll('.slider');
this.progress = this.audioPlayer.querySelector('.controls__progress');
this.volumeBtn = this.audioPlayer.querySelector('.volume__button');
this.volumeControls = this.audioPlayer.querySelector('.volume__controls');
this.volumeProgress = this.volumeControls.querySelector('.volume__progress');
this.player = this.audioPlayer.querySelector('audio');
this.currentTime = this.audioPlayer.querySelector('.controls__current-time');
this.totalTime = this.audioPlayer.querySelector('.controls__total-time');
this.speaker = this.audioPlayer.querySelector('.volume__speaker');
this.span = this.audioPlayer.querySelectorAll('.message__offscreen');
this.svg = this.audioPlayer.getElementsByTagName('svg');
this.img = this.audioPlayer.getElementsByTagName('img');
this.draggableClasses = ['pin'];
this.currentlyDragged = null;
this.stopOthersOnPlay = opts.stopOthersOnPlay || false;
this.enableKeystrokes = opts.enableKeystrokes || false;
this.showTooltips = opts.showTooltips || false;
const self = this;
this.labels = {
volume: {
open: 'Open Volume Controls',
close: 'Close Volume Controls',
},
pause: 'Pause',
play: 'Play',
};
if (!this.enableKeystrokes) {
for (let i = 0; i < this.span.length; i++) {
this.span[i].outerHTML = '';
}
} else {
window.addEventListener('keydown', this.pressKb.bind(self), false);
window.addEventListener('keyup', this.unPressKb.bind(self), false);
this.sliders[0].setAttribute('tabindex', 0);
this.sliders[1].setAttribute('tabindex', 0);
for (let j = 0; j < this.svg.length; j++) {
this.svg[j].setAttribute('tabindex', 0);
this.svg[j].setAttribute('focusable', true);
}
for (let k = 0; k < this.img.length; k++) {
this.img[k].setAttribute('tabindex', 0);
}
}
if (this.showTooltips) {
this.playPauseBtn.setAttribute('title', this.labels.play);
this.volumeBtn.setAttribute('title', this.labels.volume.open);
}
if (opts.outlineControls || false) {
this.audioPlayer.classList.add('player-accessible');
}
this.initEvents();
this.directionAware();
this.overcomeIosLimitations();
if ('autoplay' in this.player.attributes) {
const promise = this.player.play();
if (promise !== undefined) {
promise.then(() => {
const playPauseButton = self.player.parentElement.querySelector('.play-pause-btn__icon');
playPauseButton.attributes.d.value = 'M0 0h6v24H0zM12 0h6v24h-6z';
self.playPauseBtn.setAttribute('aria-label', self.labels.pause);
self.hasSetAttribute(self.playPauseBtn, 'title', self.labels.pause);
}).catch(() => {
// eslint-disable-next-line no-console
console.error('Green Audio Player Error: Autoplay has been prevented, because it is not allowed by this browser.');
});
}
}
if ('preload' in this.player.attributes && this.player.attributes.preload.value === 'none') {
this.playPauseBtn.style.visibility = 'visible';
this.loading.style.visibility = 'hidden';
}
}
static init(options) {
const players = document.querySelectorAll(options.selector);
players.forEach((player) => {
/* eslint-disable no-new */
new GreenAudioPlayer(player, options);
});
}
static getTemplate() {
return `
<div class="holder">
<div class="loading">
<div class="loading__spinner"></div>
</div>
<div class="play-pause-btn" aria-label="Play" role="button">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="24" viewBox="0 0 18 24">
<path fill="#566574" fill-rule="evenodd" d="M18 12L0 24V0" class="play-pause-btn__icon"/>
</svg>
</div>
</div>
<div class="controls">
<span class="controls__current-time" aria-live="off" role="timer">00:00</span>
<div class="controls__slider slider" data-direction="horizontal">
<div class="controls__progress gap-progress" aria-label="Time Slider" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" role="slider">
<div class="pin progress__pin" data-method="rewind"></div>
</div>
</div>
<span class="controls__total-time">00:00</span>
</div>
<div class="volume">
<div class="volume__button" aria-label="Open Volume Controls" role="button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="volume__speaker" fill="#566574" fill-rule="evenodd" d="M14.667 0v2.747c3.853 1.146 6.666 4.72 6.666 8.946 0 4.227-2.813 7.787-6.666 8.934v2.76C20 22.173 24 17.4 24 11.693 24 5.987 20 1.213 14.667 0zM18 11.693c0-2.36-1.333-4.386-3.333-5.373v10.707c2-.947 3.333-2.987 3.333-5.334zm-18-4v8h5.333L12 22.36V1.027L5.333 7.693H0z"/>
</svg>
<span class="message__offscreen">Press Enter or Space to show volume slider.</span>
</div>
<div class="volume__controls hidden">
<div class="volume__slider slider" data-direction="vertical">
<div class="volume__progress gap-progress" aria-label="Volume Slider" aria-valuemin="0" aria-valuemax="100" aria-valuenow="81" role="slider">
<div class="pin volume__pin" data-method="changeVolume"></div>
</div>
<span class="message__offscreen">Use Up/Down Arrow keys to increase or decrease volume.</span>
</div>
</div>
</div>
`;
}
initEvents() {
const self = this;
self.audioPlayer.addEventListener('mousedown', (event) => {
if (self.isDraggable(event.target)) {
self.currentlyDragged = event.target;
const handleMethod = self.currentlyDragged.dataset.method;
const listener = self[handleMethod].bind(self);
window.addEventListener('mousemove', listener, false);
if (self.currentlyDragged.parentElement.parentElement === self.sliders[0]) {
self.paused = self.player.paused;
if (self.paused === false) self.togglePlay();
}
window.addEventListener('mouseup', () => {
if (self.currentlyDragged !== false
&& self.currentlyDragged.parentElement.parentElement === self.sliders[0]
&& self.paused !== self.player.paused) {
self.togglePlay();
}
self.currentlyDragged = false;
window.removeEventListener('mousemove', listener, false);
}, false);
}
});
// for mobile touches
self.audioPlayer.addEventListener('touchstart', (event) => {
if (self.isDraggable(event.target)) {
[self.currentlyDragged] = event.targetTouches;
const handleMethod = self.currentlyDragged.target.dataset.method;
const listener = self[handleMethod].bind(self);
window.addEventListener('touchmove', listener, false);
if (self.currentlyDragged.parentElement.parentElement === self.sliders[0]) {
self.paused = self.player.paused;
if (self.paused === false) self.togglePlay();
}
window.addEventListener('touchend', () => {
if (self.currentlyDragged !== false
&& self.currentlyDragged.parentElement.parentElement === self.sliders[0]
&& self.paused !== self.player.paused) {
self.togglePlay();
}
self.currentlyDragged = false;
window.removeEventListener('touchmove', listener, false);
}, false);
event.preventDefault();
}
});
this.playPauseBtn.addEventListener('click', this.togglePlay.bind(self));
this.player.addEventListener('timeupdate', this.updateProgress.bind(self));
this.player.addEventListener('volumechange', this.updateVolume.bind(self));
this.player.volume = 0.81;
this.player.addEventListener('loadedmetadata', () => {
self.totalTime.textContent = GreenAudioPlayer.formatTime(self.player.duration);
});
this.player.addEventListener('seeking', this.showLoadingIndicator.bind(self));
this.player.addEventListener('seeked', this.hideLoadingIndicator.bind(self));
this.player.addEventListener('canplay', this.hideLoadingIndicator.bind(self));
this.player.addEventListener('ended', () => {
GreenAudioPlayer.pausePlayer(self.player, 'ended');
self.player.currentTime = 0;
self.playPauseBtn.setAttribute('aria-label', self.labels.play);
self.hasSetAttribute(self.playPauseBtn, 'title', self.labels.play);
});
this.volumeBtn.addEventListener('click', this.showHideVolume.bind(self));
window.addEventListener('resize', self.directionAware.bind(self));
window.addEventListener('scroll', self.directionAware.bind(self));
for (let i = 0; i < this.sliders.length; i++) {
const pin = this.sliders[i].querySelector('.pin');
this.sliders[i].addEventListener('click', self[pin.dataset.method].bind(self));
}
}
overcomeIosLimitations() {
const self = this;
if (this.isDevice) {
// iOS does not support "canplay" event
this.player.addEventListener('loadedmetadata', this.hideLoadingIndicator.bind(self));
// iOS does not let "volume" property to be set programmatically
this.audioPlayer.querySelector('.volume').style.display = 'none';
this.audioPlayer.querySelector('.controls').style.marginRight = '0';
}
}
isDraggable(el) {
let canDrag = false;
if (typeof el.classList === 'undefined') return false; // fix for IE 11 not supporting classList on SVG elements
for (let i = 0; i < this.draggableClasses.length; i++) {
if (el.classList.contains(this.draggableClasses[i])) {
canDrag = true;
}
}
return canDrag;
}
inRange(event) {
const touch = ('touches' in event); // instanceof TouchEvent may also be used
const rangeBox = this.getRangeBox(event);
const sliderPositionAndDimensions = rangeBox.getBoundingClientRect();
const { dataset: { direction } } = rangeBox;
let min = null;
let max = null;
if (direction === 'horizontal') {
min = sliderPositionAndDimensions.x;
max = min + sliderPositionAndDimensions.width;
const clientX = touch ? event.touches[0].clientX : event.clientX;
if (clientX < min || clientX > max) return false;
} else {
min = sliderPositionAndDimensions.top;
max = min + sliderPositionAndDimensions.height;
const clientY = touch ? event.touches[0].clientY : event.clientY;
if (clientY < min || clientY > max) return false;
}
return true;
}
updateProgress() {
const current = this.player.currentTime;
const percent = (current / this.player.duration) * 100;
this.progress.setAttribute('aria-valuenow', percent);
this.progress.style.width = `${percent}%`;
this.currentTime.textContent = GreenAudioPlayer.formatTime(current);
}
updateVolume() {
this.volumeProgress.setAttribute('aria-valuenow', this.player.volume * 100);
this.volumeProgress.style.height = `${this.player.volume * 100}%`;
if (this.player.volume >= 0.5) {
this.speaker.attributes.d.value = 'M14.667 0v2.747c3.853 1.146 6.666 4.72 6.666 8.946 0 4.227-2.813 7.787-6.666 8.934v2.76C20 22.173 24 17.4 24 11.693 24 5.987 20 1.213 14.667 0zM18 11.693c0-2.36-1.333-4.386-3.333-5.373v10.707c2-.947 3.333-2.987 3.333-5.334zm-18-4v8h5.333L12 22.36V1.027L5.333 7.693H0z';
} else if (this.player.volume < 0.5 && this.player.volume > 0.05) {
this.speaker.attributes.d.value = 'M0 7.667v8h5.333L12 22.333V1L5.333 7.667M17.333 11.373C17.333 9.013 16 6.987 14 6v10.707c2-.947 3.333-2.987 3.333-5.334z';
} else if (this.player.volume <= 0.05) {
this.speaker.attributes.d.value = 'M0 7.667v8h5.333L12 22.333V1L5.333 7.667';
}
}
getRangeBox(event) {
let rangeBox = event.target;
const el = this.currentlyDragged;
if (event.type === 'click' && this.isDraggable(event.target)) {
rangeBox = event.target.parentElement.parentElement;
}
if (event.type === 'mousemove') {
rangeBox = el.parentElement.parentElement;
}
if (event.type === 'touchmove') {
rangeBox = el.target.parentElement.parentElement;
}
return rangeBox;
}
getCoefficient(event) {
const touch = ('touches' in event); // instanceof TouchEvent may also be used
const slider = this.getRangeBox(event);
const sliderPositionAndDimensions = slider.getBoundingClientRect();
let K = 0;
if (slider.dataset.direction === 'horizontal') {
// if event is touch
const clientX = touch ? event.touches[0].clientX : event.clientX;
const offsetX = clientX - sliderPositionAndDimensions.left;
const { width } = sliderPositionAndDimensions;
K = offsetX / width;
} else if (slider.dataset.direction === 'vertical') {
const { height } = sliderPositionAndDimensions;
const clientY = touch ? event.touches[0].clientY : event.clientY;
const offsetY = clientY - sliderPositionAndDimensions.top;
K = 1 - offsetY / height;
}
return K;
}
rewind(event) {
if (this.player.seekable && this.player.seekable.length) { // no seek if not (pre)loaded
if (this.inRange(event)) {
this.player.currentTime = this.player.duration * this.getCoefficient(event);
}
}
}
showVolume() {
if (this.volumeBtn.getAttribute('aria-attribute') === this.labels.volume.open) {
this.volumeControls.classList.remove('hidden');
this.volumeBtn.classList.add('open');
this.volumeBtn.setAttribute('aria-label', this.labels.volume.close);
this.hasSetAttribute(this.volumeBtn, 'title', this.labels.volume.close);
}
}
showHideVolume() {
this.volumeControls.classList.toggle('hidden');
if (this.volumeBtn.getAttribute('aria-label') === this.labels.volume.open) {
this.volumeBtn.setAttribute('aria-label', this.labels.volume.close);
this.hasSetAttribute(this.volumeBtn, 'title', this.labels.volume.close);
this.volumeBtn.classList.add('open');
} else {
this.volumeBtn.setAttribute('aria-label', this.labels.volume.open);
this.hasSetAttribute(this.volumeBtn, 'title', this.labels.volume.open);
this.volumeBtn.classList.remove('open');
}
}
changeVolume(event) {
if (this.inRange(event)) {
this.player.volume = Math.round(this.getCoefficient(event) * 50) / 50;
}
}
static formatTime(time) {
const min = Math.floor(time / 60);
const sec = Math.floor(time % 60);
return `${(min < 10) ? `0${min}` : min}:${(sec < 10) ? `0${sec}` : sec}`;
}
preloadNone() {
const self = this;
if (!this.player.duration) {
self.playPauseBtn.style.visibility = 'hidden';
self.loading.style.visibility = 'visible';
}
}
togglePlay() {
this.preloadNone();
if (this.player.paused) {
if (this.stopOthersOnPlay) {
GreenAudioPlayer.stopOtherPlayers();
}
GreenAudioPlayer.playPlayer(this.player);
this.playPauseBtn.setAttribute('aria-label', this.labels.pause);
this.hasSetAttribute(this.playPauseBtn, 'title', this.labels.pause);
} else {
GreenAudioPlayer.pausePlayer(this.player, 'toggle');
this.playPauseBtn.setAttribute('aria-label', this.labels.play);
this.hasSetAttribute(this.playPauseBtn, 'title', this.labels.play);
}
}
hasSetAttribute(el, a, v) {
if (this.showTooltips) {
if (el.hasAttribute(a)) {
el.setAttribute(a, v);
}
}
}
setCurrentTime(time) {
const pos = this.player.currentTime;
const end = Math.floor(this.player.duration);
if (pos + time < 0 && pos === 0) {
this.player.currentTime = this.player.currentTime;
} else if (pos + time < 0) {
this.player.currentTime = 0;
} else if (pos + time > end) {
this.player.currentTime = end;
} else {
this.player.currentTime += time;
}
}
setVolume(volume) {
if (this.isDevice) return;
const vol = this.player.volume;
if (vol + volume >= 0 && vol + volume < 1) {
this.player.volume += volume;
} else if (vol + volume <= 0) {
this.player.volume = 0;
} else {
this.player.volume = 1;
}
}
unPressKb(event) {
const evt = event || window.event;
if (this.seeking && (evt.keyCode === 37 || evt.keyCode === 39)) {
this.togglePlay();
this.seeking = false;
}
}
pressKb(event) {
const evt = event || window.event;
switch (evt.keyCode) {
case 13: // Enter
case 32: // Spacebar
if (document.activeElement.parentNode === this.playPauseBtn) {
this.togglePlay();
} else if (document.activeElement.parentNode === this.volumeBtn
|| document.activeElement === this.sliders[1]) {
if (document.activeElement === this.sliders[1]) {
try { // IE 11 not supporting programmatic focus on svg elements
this.volumeBtn.children[0].focus();
} catch (error) {
this.volumeBtn.focus();
}
}
this.showHideVolume();
}
break;
case 37: case 39: // horizontal Arrows
if (document.activeElement === this.sliders[0]) {
if (evt.keyCode === 37) {
this.setCurrentTime(-5);
} else {
this.setCurrentTime(+5);
}
if (!this.player.paused && this.player.seeking) {
this.togglePlay();
this.seeking = true;
}
}
break;
case 38: case 40: // vertical Arrows
if (document.activeElement.parentNode === this.volumeBtn
|| document.activeElement === this.sliders[1]) {
if (evt.keyCode === 38) {
this.setVolume(0.05);
} else {
this.setVolume(-0.05);
}
}
if (document.activeElement.parentNode === this.volumeBtn) {
this.showVolume();
}
break;
default: break;
}
}
static pausePlayer(player) {
const playPauseButton = player.parentElement.querySelector('.play-pause-btn__icon');
playPauseButton.attributes.d.value = 'M18 12L0 24V0';
player.pause();
}
static playPlayer(player) {
const playPauseButton = player.parentElement.querySelector('.play-pause-btn__icon');
playPauseButton.attributes.d.value = 'M0 0h6v24H0zM12 0h6v24h-6z';
player.play();
}
static stopOtherPlayers() {
const players = document.querySelectorAll('.green-audio-player audio');
for (let i = 0; i < players.length; i++) {
GreenAudioPlayer.pausePlayer(players[i]);
}
}
showLoadingIndicator() {
this.playPauseBtn.style.visibility = 'hidden';
this.loading.style.visibility = 'visible';
}
hideLoadingIndicator() {
this.playPauseBtn.style.visibility = 'visible';
this.loading.style.visibility = 'hidden';
}
directionAware() {
this.volumeControls.classList.remove('top', 'middle', 'bottom');
if (window.innerHeight < 250) {
this.volumeControls.classList.add('middle');
} else if (this.audioPlayer.getBoundingClientRect().top < 180) {
this.volumeControls.classList.add('bottom');
} else {
this.volumeControls.classList.add('top');
}
}
}
export default GreenAudioPlayer;