This is one of my first JavaScript projects. It works (at least in Firefox and Chrome), but I would like to know if this code follows common best practices.
This script is supposed to look for <audio>
tags with class name audioPlayer
and build up a separate interface around each one. It allows for multiple audio files to be played simultaneously. In addition to giving the user the capability to play, pause, stop, and rewind, it allows for seeking via a clickable progress bar and volume adjustment.
Here's the script:
// function to build out player iunterface
var buildPlayer = function(elTag, elClass, destination) {
var newElement = document.createElement(elTag);
newElement.className = elClass;
destination.appendChild(newElement);
return newElement;
}
// format time strings
var formatTime = function(seconds) {
minutes = Math.floor(seconds / 60);
minutes = (minutes >= 10) ? minutes : '0' + minutes;
seconds = Math.floor(seconds % 60);
seconds = (seconds >= 10) ? seconds : '0' + seconds;
return minutes + ':' + seconds;
}
// convert each <audio> element into a player
var processAudioElements = function(audio) {
// remove default playback controls for <audio> element
audio.removeAttribute('controls');
// get ID and source URL
var exerciseId = audio.id;
var source = audio.getElementsByTagName('source')[0];
var audioSrc = source.getAttribute('src');
// put wrapper around <audio> elements
var parent = audio.parentNode;
var playerWrapper = document.createElement('div');
parent.replaceChild(playerWrapper, audio);
playerWrapper.appendChild(audio);
playerWrapper.className = 'playerWrapper';
playerWrapper.id = exerciseId;
// check to see if player should be small version
if (audio.classList.contains('sm')) {
playerWrapper.classList.add('sm');
}
// build remaining elements for player
var fallback = buildPlayer('p', 'fallback', audio)
fallback.innerHTML = 'Audio player not supported...';
var fileloading = buildPlayer('div', 'fileloading', playerWrapper)
fileloading.innerHTML = 'loading';
var playButton = buildPlayer('div', 'playButton', playerWrapper);
playButton.style.display = 'none';
playButton.innerHTML = '<img class="player-icon" src="./images/icons/media-play.svg" />';
var pauseButton = buildPlayer('div', 'pauseButton', playerWrapper);
pauseButton.style.display = 'none';
pauseButton.innerHTML = '<img class="player-icon" src="./images/icons/media-pause.svg" />';
var stopButton = buildPlayer('div', 'stopButton', playerWrapper);
stopButton.style.display = 'none';
stopButton.innerHTML = '<img class="player-icon" src="./images/icons/media-stop.svg" />';
var info = buildPlayer('div', 'info', playerWrapper);
var infocontainer = buildPlayer('div', 'infocontainer', info);
var title = buildPlayer('span', 'title', infocontainer);
title.innerHTML = exerciseId;
var timing = buildPlayer('span', 'timing', infocontainer);
timing.innerHTML = '0:00';
var volume = buildPlayer('div', 'volume', playerWrapper);
var volumearea = buildPlayer('div', 'volumearea', volume);
var volumemask = buildPlayer('div', 'volumemask', volumearea);
var rewindButton = buildPlayer('div', 'rewindButton', playerWrapper);
rewindButton.innerHTML = '<img class="player-icon" src="./images/icons/media-step-backward.svg" />';
var playerprogress = buildPlayer('div', 'playerprogress', playerWrapper);
var bar = buildPlayer('div', 'bar', playerprogress);
// functionality for play buttons
var play = function() {
playButton.style.display = 'none';
pauseButton.style.display = 'block';
audio.play();
};
playButton.addEventListener('click', play, false);
// functionality for pause buttons
var pause = function() {
pauseButton.style.display = 'none';
playButton.style.display = 'block';
audio.pause();
};
pauseButton.addEventListener('click', pause, false);
// functionality for stop buttons
var stop = function() {
playButton.style.display = 'block';
pauseButton.style.display = 'none';
audio.pause();
audio.currentTime = 0;
};
stopButton.addEventListener('click', stop, false);
// functionality for rewind buttons
var rewind = function() {
audio.currentTime = 0;
};
rewindButton.addEventListener('click', rewind, false);
//functionality for volume sliders
var volumeadjust = function(e) {
var volumewidth = volumearea.offsetWidth;
var volumeheight = volumearea.offsetHeight;
var viewportOffset = volumearea.getBoundingClientRect();
var left = viewportOffset.left;
var clickPosition = (e.clientX - left) / volumearea.offsetWidth;
if (clickPosition > 1) {
clickPosition = 1;
} else if (clickPosition < 0) {
clickPosition = 0;
}
function precisionRound(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
clickPosition = precisionRound(clickPosition, 1);
audio.volume = precisionRound(clickPosition, 1);
volumearea.style.background = 'linear-gradient(to right, #999 ' + (precisionRound(clickPosition, 1) * 100) + '%, #fff ' + (precisionRound(clickPosition, 1) * 100) + '%';
}
volumearea.addEventListener('mousedown', function(e){
volumeadjust(e);
volumearea.onmousemove = function(e) {
volumeadjust(e);
}
});
document.addEventListener('mouseup', function(e){
volumearea.onmousemove = null
});
// functionality for progress bar
var jump = function(e) {
// get position of element in viewport
var viewportOffset = this.getBoundingClientRect();
var left = viewportOffset.left;
// calculate the normalized position clicked
var clickPosition = (e.clientX - left) / this.offsetWidth;
var clickTime = clickPosition * audio.duration;
// move the playhead to the correct position
audio.currentTime = clickTime;
}
playerprogress.addEventListener('click', jump, false);
// retrieve and display duration and elapsed time
audio.addEventListener('timeupdate', function() {
bar.style.width = parseInt(((audio.currentTime / audio.duration) * 100), 10) + '%';
});
audio.addEventListener('durationchange', function() {
title.innerHTML = exerciseId + ' - ';
timing.innerHTML = '00:00 / ' + formatTime(audio.duration)
});
audio.addEventListener('timeupdate', function() {
timing.innerHTML = formatTime(audio.currentTime) + ' / ' + formatTime(audio.duration);
});
// when audio is finished playing, rewind to beginning
audio.addEventListener('ended', function() {
audio.currentTime = 0;
bar.style.width = '0%';
playButton.style.display = 'block';
pauseButton.style.display = 'none';
});
// check status for each <audio> element and display playback controls if it's ready
function displayControls() {
fileloading.style.display = 'none';
playButton.style.display = 'block';
stopButton.style.display = 'block';
}
if (audio.paused) {
audio.volume = 0.75;
displayControls();
} else {
audio.volume = 0.75;
audio.addEventListener('canplay', function() {
displayControls();
});
}
}
This is initialized with a script at the bottom of each page:
// collect mp3 audio links and build players
window.onload = function() {
var audioLinks = document.getElementsByClassName('audioPlayer');
for (i = 0; i < audioLinks.length; i++) {
processAudioElements(audioLinks[i]);
}
}
The intention is to take elements like this:
<audio class="audioPlayer" id="Cole-Lewis-1904_1-01-103" controls>
<source src="media/Cole-Lewis-1904_1-01-103-AUD.mp3" type="audio/mpeg"
</audio>
And turn them into this:
<div class="playerWrapper" id="Cole-Lewis-1904_1-01-103">
<audio class="audioPlayer" id="Cole-Lewis-1904_1-01-103">
<source src="media/Cole-Lewis-1904_1-01-103-AUD.mp3" type="audio/mpeg">
<p class="fallback">Audio player not supported.</p>
</audio>
<div class="fileloading" style="display: none;">loading</div>
<div class="playButton" style="display: block;">
<img class="player-icon" src="./images/icons/media-play.svg" />
</div>
<div class="pauseButton" style="display: none;">
<img class="player-icon" src="./images/icons/media-pause.svg" />
</div>
<div class="stopButton" style="display: block;">
<img class="player-icon" src="./images/icons/media-stop.svg" />
</div>
<div class="info">
<div class="infocontainer">
<span class="title">Cole-Lewis-1904_1-01-103 - </span>
<span class="timing">00:00 / 01:04</span>
</div>
</div>
<div class="volume">
<div class="volumearea">
<div class="volumemask"></div>
</div>
</div>
<div class="rewindButton">
<img class="player-icon" src="./images/icons/media-step-backward.svg" />
</div>
<div class="playerprogress">
<div class="bar"></div>
</div>
</div>
The result is then styled with the following CSS:
.playerWrapper {
display: inline-grid;
grid-auto-columns: min-content;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"playpause stop info volume rewind"
"playpause stop progress volume rewind";
line-height: 0;
-webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.65);
-moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.65);
box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.65);
-webkit-border-radius: 4px 4px 4px 4px;
-moz-border-radius: 4px 4px 4px 4px;
border-radius: 4px 4px 4px 4px;
margin-left: 1em;
margin-top: 0.5em;
}
@keyframes activebutton {
0% {background-color: #666;}
50% {background-color: #999;}
100% {background-color: #666;}
}
.playButton, .pauseButton {
grid-area: playpause;
background-color: #444;
-webkit-border-radius: 4px 0px 0px 4px;
-moz-border-radius: 4px 0px 0px 4px;
border-radius: 4px 0px 0px 4px;
}
.pauseButton {
animation-name: activebutton;
animation-duration: 1s;
animation-iteration-count: infinite;
}
.stopButton {
grid-area: stop;
background-color: #444;
}
.info {
grid-area: info;
display: grid;
align-items: center;
width: 300px;
background-color: #444;
}
.infocontainer {
font-size: 0.6em;
font-family: sans-serif;
letter-spacing: 0.1em;
height: 1em;
color: #fff;
line-height: 1;
}
.playerprogress {
grid-area: progress;
background-color: #ccc;
cursor: pointer;
}
.bar {
background:#999;
width:0%;
height: 100%;
}
.volume {
grid-area: volume;
background-color: #444;
}
.volumearea {
width: 2em;
height: 1em;
background: linear-gradient(to right, #999 80%, #fff 20%);
position: relative;
margin: 0.3em;
cursor: pointer;
}
.volumemask {
width: 0em;
height: 0em;
border-top: 1em solid #444;
border-right: 2em solid transparent;
position: absolute;
z-index: 1;
bottom: 0;
}
.rewindButton {
grid-area: rewind;
background-color: #444;
border-radius: 0px 4px 4px 0px;
}
.player-icon {
height: 1em;
margin: 0.3em;
padding: 0em;
}
.playButton:hover, .pauseButton:hover, .stopButton:hover, .rewindButton:hover {
background-color: #666;
cursor: pointer;
}
.volume:hover {
background-color: #666;
}
.volume:hover > .volumearea > .volumemask {
border-top-color: #666;
}
.sm > .info {
display: none;
}
.sm > .playerprogress {
display: none;
}
And the result should look like this:
The progress bar and volume triangle are clickable.
Edit with more specific questions:
I'm a little shaky on how to order things. Is it properly organized to: 1) initiate the script with the
window.onload
call at the bottom of the html body, 2) put the small functions (buildPlayer
andformatTime
) at the top of the .js file? In other words, am I making any glaring newbie errors?Would it be better to put all of the arguments for the various calls to
buildPlayer
in an array and loop through that as opposed to doing each one individually?Am I missing anything essential regarding the loading and playing of the MP3 files? The players seem to work fine but I'm worried there's some common issue I haven't anticipated.