Lately I've been working on an update of an audio player that I created last year as visible on github:
xinnks / xns-audio-player
A simple customizable web music player powered by vue & HTMLAudioElement
xns-audio-player
A persistent audio player powered by vue and some visuals from tailwindcss, v-tooltip, v-progress, vue-ionicons & xns-seek-bar
Adding playlists
To add a new playlist call the addPlaylist() method from within a method or the mounted hook
...
this.addPlaylist({
title: 'Playlist 1',
songs: this.demoPlaylist
})
...
Where demoPlaylist
is an array of song objects in the following format
{ audio: "link_to_audio_file.mp3", artist: "Artist's name", title: "Song title", album: "album name", cover: "link_to_album_or_song_cover_image.jpg"}
Project setup
npm install
Compiles and hot-reloads for development
npm run serve
Compiles and minifies for production
npm run build
.
In short it's an audio player that's based to work with vue, the idea being, it should support persistent playback on route changes in a javascript environment, in this case Vue.
Like on a couple other projects, I always start with an idea then execute it with more bloated code and plugins than favorable. Then I usually proceed with cutting back on the plugins in favor of custom components, re-inventing the wheel so to speak, but with the target of reducing code size and hopefully increasing performance by reducing dependencies.
So, amongst the plugins I decided to cut off from the project, was a slider component that I used to convey audio playback position and seeking to the UI, which brings us to this article. I decided to share this because I think it might be useful to someone out there who at first might assume creating such functionality on their project is a complicated task, well, no it's not.
Let's get down to business.
Our objective is to achieve this 👇
Since this project is based on Vue, I'll be using code snippets from the component itself which is in a Vue environment, but likewise, you can apply the same concept on any Javascript environment as we will be utilizing Javascript event listeners.
After setting up the Vue project environment (here for beginners) we will start by creating our seekable component and name it 'SeekProgress.vue'
Our template will contain only two div blocks, a wrapper which will be setting the dimensions for our component, and it's child which will be an absolute positioned div covering the parent based on the percentage of the total width.
<template>
<div id="app">
<div class="progress-wrapper">
<div class="progress"></div>
</div>
</div>
</template>
<script>
export default {
name: 'SeekProgress',
}
</script>
<style lang="scss">
.progress-wrapper{
display: block;
height: 200px;
margin: 200px 20px;
position: relative;
background: #e1e1e1;
.progress{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
background: teal;
}
}
</style>
Next, we will add a reference to our wrapper block by using a $ref, whereas we'd utilize the div's id or class to reference it in but not limited to vanilla js.
<template>
<div id="app">
<div ref="listenTo" class="progress-wrapper">
<div class="progress"></div>
</div>
</div>
</template>
After listening to the Javascript events we'll be modifying the '.progress' div's width by adding inline styling to it, passing the width percentage.
<template>
<div id="app">
<div ref="listenTo" class="progress-wrapper">
<div :style="'width:'+progress+'%'" class="progress"></div>
</div>
</div>
</template>
Then, we will listen to the events on the wrapper and react accordingly.
<script>
export default {
name: 'SeekProgress',
data(){
return {
progress: 0,
wrapperWidth: 0
}
},
mounted(){
this.$refs.listenTo.addEventListener("click", this.getClickPosition, false)
},
methods: {
getClickPosition(e){
e = e || window.e
// get target element
let target = e.target || e.srcElement
if(target.nodeType == 3) target = target.parentNode // fix for a safari bug
this.wrapperWidth = this.wrapperWidth || target.offsetWidth // set initial wrapper width
// get the seek width
let seekWidth = e.offsetX
// change seek position
this.progress = (seekWidth / this.wrapperWidth) * 100
},
}
}
</script>
A breakdown of the script above:
progress: a variable that sets the width of the progress div in percent.
wrapperWidth: a variable that stores the dynamic width of the wrapper div, which we derive our progress percent from.
getClickPosition(): A callback function executed when a click event is performed on the wrapper div block.
On the getClickPosition() function we make sure we get the object that the event is based on, in our case the wrapper block; doing this just after error proofing different browser types on acquiring this object. After, we set our initial wrapper width and then obtain the position where the event occurred based on the horizontal offset from the left side of our component.
Next we get the percent of this offset to the total block width and store it in 'progress'.
It is important to make sure that the wrapperWidth variable will be modified when the window is resized, otherwise we end up with not interesting results when we interact with our component after resizes.
We'll add a window resize listener that will be doing just that.
<script>
...
//add a listener that will listen to window resize and modify progress width accordingly
window.addEventListener('resize', this.windowResize, false)
...
...
windowResize(e){
let prog = this
setTimeout(()=>{
prog.wrapperWidth = prog.$refs.listenTo.offsetWidth
}, 200)
}
}
...
}
</script>
That's all... right!?
If your target is just modifying the progress on mere clicks and not including drags, that's it really. But if you want to have smooth drag seeks you'll need to listen to a couple more events.
Our friends "mousedown", "mousemove" and "mouseup" will help us with that.
<script>
...
mounted(){
...
this.$refs.listenTo.addEventListener("mousedown", this.detectMouseDown, false)
this.$refs.listenTo.addEventListener("mouseup", this.detectMouseUp, false)
...
},
methods: {
...
detectMouseDown(e){
e.preventDefault() // prevent browser from moving objects, following links etc
// start listening to mouse movements
this.$refs.listenTo.addEventListener("mousemove", this.getClickPosition, false)
},
detectMouseUp(e){
// stop listening to mouse movements
this.$refs.listenTo.removeEventListener("mousemove", this.getClickPosition, false)
},
...
}
}
</script>
We start by listening to a mousedown event inside which we commence listening to mousemove events and updating our progress accordingly by utilizing our first callback function getClickPosition(); what to note here is the e.preventDefault() which tells the browser not to drag stuff on the screen.
When the mouse is released and we hear a mouseup event, we stop listening to the mousemove event and voila! we have added drag capability to our progress component.
Compiling the code above, we then have:
<template>
<div id="app">
<div ref="listenTo" class="progress-wrapper">
<div :style="'width:'+progress+'%'" class="progress"></div>
</div>
</div>
</template>
<script>
export default {
name: 'SeekProgress',
data(){
return {
progress: 0,
wrapperWidth: 0
}
},
mounted(){
this.$refs.listenTo.addEventListener("click", this.getClickPosition, false)
this.$refs.listenTo.addEventListener("mousedown", this.detectMouseDown, false)
this.$refs.listenTo.addEventListener("mouseup", this.detectMouseUp, false)
//add a listener that will listen to window resize and modify progress width accordingly
window.addEventListener('resize', this.windowResize, false)
},
methods: {
getClickPosition(e){
e = e || window.e
// get target element
let target = e.target || e.srcElement
if(target.nodeType == 3) target = target.parentNode // fix for a safari bug
this.wrapperWidth = this.wrapperWidth || target.offsetWidth // set initial progressbar width
// get the seek width
let seekWidth = e.offsetX
// change seek position
this.progress = (seekWidth / this.wrapperWidth) * 100
},
detectMouseDown(e){
e.preventDefault() // prevent browser from moving objects, following links etc
// start listening to mouse movements
this.$refs.listenTo.addEventListener("mousemove", this.getClickPosition, false)
},
detectMouseUp(e){
// stop listening to mouse movements
this.$refs.listenTo.removeEventListener("mousemove", this.getClickPosition, false)
},
windowResize(e){
let prog = this
setTimeout(()=>{
prog.wrapperWidth = prog.$refs.listenTo.offsetWidth
}, 200)
}
}
}
</script>
<style lang="scss">
.progress-wrapper{
display: block;
height: 200px;
margin: 200px 20px;
position: relative;
background: #e1e1e1;
.progress{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
background: teal;
}
}
</style>
Now go out there and build stuff!
Update
So I went ahead and bundled this code up into a Vue plugin.
It's available for use both within the Vue environment and the browser:
xinnks / xns-seek-bar
A seekable progress bar component for Vue.js
xns-seek-bar
A seekable progress bar component for Vue.js
install
$ npm i xns-seek-bar
Import & initiate plugin on your entry js file
import XnsSeekBar from 'xns-seek-bar'
Vue.use(XnsSeekBar)
In Browser
// Latest update
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xns-seek-bar/dist/index.umd.js"></script>
Example
<xns-seek-bar :bar-color="'#ffdd00'" :current-value="33" :total-value="100"></xns-seek-bar>
Options
Option | Type | Required | Default |
---|---|---|---|
currentValue | Number | false | 0 |
totalValue | Number | false | 300 |
listen | Boolean | false | true |
barHeight | Number | false | 0.5 |
barColor | String (Hex) | false | false |
barShadeColor | String (Hex) | false | false |
intensity | Number (0.1 - 1)) | false | 0 |
Options Details
listen : Enable touch / tap.
Events
seekedTo Returns a Number representing value of seeked position.
Here's a demo pen:
Â
Â
Top comments (0)