I'm converting a player in html into a Vue component.
Half of the component is already created, only the time control slider is missing.
Here is the html player code (Lines with multiple tabs are already implemented in the Vue component):
var audioPlayer = document.querySelector('.green-audio-player');
var playPause = audioPlayer.querySelector('#playPause');
var playpauseBtn = audioPlayer.querySelector('.play-pause-btn');
var loading = audioPlayer.querySelector('.loading');
var progress = audioPlayer.querySelector('.progress');
var sliders = audioPlayer.querySelectorAll('.slider');
var player = audioPlayer.querySelector('audio');
var currentTime = audioPlayer.querySelector('.current-time');
var totalTime = audioPlayer.querySelector('.total-time');
var speaker = audioPlayer.querySelector('#speaker');
var draggableClasses = ['pin'];
var currentlyDragged = null;
window.addEventListener('mousedown', function(event) {
if(!isDraggable(event.target)) return false;
currentlyDragged = event.target;
let handleMethod = currentlyDragged.dataset.method;
this.addEventListener('mousemove', window[handleMethod], false);
window.addEventListener('mouseup', () => {
currentlyDragged = false;
window.removeEventListener('mousemove', window[handleMethod], false);
}, false);
});
playpauseBtn.addEventListener('click', togglePlay);
player.addEventListener('timeupdate', updateProgress);
player.addEventListener('loadedmetadata', () => {
totalTime.textContent = formatTime(player.duration);
});
player.addEventListener('canplay', makePlay);
player.addEventListener('ended', function(){
playPause.attributes.d.value = "M18 12L0 24V0";
player.currentTime = 0;
});
sliders.forEach(slider => {
let pin = slider.querySelector('.pin');
slider.addEventListener('click', window[pin.dataset.method]);
});
function isDraggable(el) {
let canDrag = false;
let classes = Array.from(el.classList);
draggableClasses.forEach(draggable => {
if(classes.indexOf(draggable) !== -1)
canDrag = true;
})
return canDrag;
}
function inRange(event) {
let rangeBox = getRangeBox(event);
let rect = rangeBox.getBoundingClientRect();
let direction = rangeBox.dataset.direction;
if(direction == 'horizontal') {
var min = rangeBox.offsetLeft;
var max = min + rangeBox.offsetWidth;
if(event.clientX < min || event.clientX > max) return false;
} else {
var min = rect.top;
var max = min + rangeBox.offsetHeight;
if(event.clientY < min || event.clientY > max) return false;
}
return true;
}
function updateProgress() {
var current = player.currentTime;
var percent = (current / player.duration) * 100;
progress.style.width = percent + '%';
currentTime.textContent = formatTime(current);
}
function getRangeBox(event) {
let rangeBox = event.target;
let el = currentlyDragged;
if(event.type == 'click' && isDraggable(event.target)) {
rangeBox = event.target.parentElement.parentElement;
}
if(event.type == 'mousemove') {
rangeBox = el.parentElement.parentElement;
}
return rangeBox;
}
function getCoefficient(event) {
let slider = getRangeBox(event);
let rect = slider.getBoundingClientRect();
let K = 0;
if(slider.dataset.direction == 'horizontal') {
let offsetX = event.clientX - slider.offsetLeft;
let width = slider.clientWidth;
K = offsetX / width;
} else if(slider.dataset.direction == 'vertical') {
let height = slider.clientHeight;
var offsetY = event.clientY - rect.top;
K = 1 - offsetY / height;
}
return K;
}
function rewind(event) {
if(inRange(event)) {
player.currentTime = player.duration * getCoefficient(event);
}
}
function formatTime(time) {
var min = Math.floor(time / 60);
var sec = Math.floor(time % 60);
return min + ':' + ((sec<10) ? ('0' + sec) : sec);
}
function togglePlay() {
if(player.paused) {
playPause.attributes.d.value = "M0 0h6v24H0zM12 0h6v24h-6z";
player.play();
} else {
playPause.attributes.d.value = "M18 12L0 24V0";
player.pause();
}
}
function makePlay() {
playpauseBtn.style.display = 'block';
loading.style.display = 'none';
}
.audio.green-audio-player {
width: 400px;
min-width: 300px;
height: 56px;
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.07);
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 24px;
padding-right: 24px;
border-radius: 4px;
user-select: none;
-webkit-user-select: none;
background-color: #fff;
}
.audio.green-audio-player .play-pause-btn {
display: none;
cursor: pointer;
}
.audio.green-audio-player .spinner {
width: 18px;
height: 18px;
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/355309/loading.png);
background-size: cover;
background-repeat: no-repeat;
animation: spin 0.4s linear infinite;
}
.audio.green-audio-player .slider {
flex-grow: 1;
background-color: #D8D8D8;
cursor: pointer;
position: relative;
}
.audio.green-audio-player .slider .progress {
background-color: #44BFA3;
border-radius: inherit;
position: absolute;
pointer-events: none;
}
.audio.green-audio-player .slider .progress .pin {
height: 16px;
width: 16px;
border-radius: 8px;
background-color: #44BFA3;
position: absolute;
pointer-events: all;
box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.32);
}
.audio.green-audio-player .controls {
font-family: 'Roboto', sans-serif;
font-size: 16px;
line-height: 18px;
color: #55606E;
display: flex;
flex-grow: 1;
justify-content: space-between;
align-items: center;
margin-left: 24px;
}
.audio.green-audio-player .controls .slider {
margin-left: 16px;
margin-right: 16px;
border-radius: 2px;
height: 4px;
}
.audio.green-audio-player .controls .slider .progress {
width: 0;
height: 100%;
}
.audio.green-audio-player .controls .slider .progress .pin {
right: -8px;
top: -6px;
}
.audio.green-audio-player .controls span {
cursor: default;
}
svg, img {
display: block;
}
@keyframes spin {
from {
transform: rotateZ(0);
}
to {
transform: rotateZ(1turn);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div class="audio green-audio-player">
<div class="loading">
<div class="spinner"></div>
</div>
<div class="play-pause-btn">
<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-icon" id="playPause"/>
</svg>
</div>
<div class="controls">
<span class="current-time">0:00</span>
<div class="slider" data-direction="horizontal">
<div class="progress">
<div class="pin" id="progress-pin" data-method="rewind"></div>
</div>
</div>
<span class="total-time">0:00</span>
</div>
<audio>
<source src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/355309/Swing_Jazz_Drum.mp3" type="audio/mpeg">
</audio>
</div>
Html Codepen: https://codepen.io/caiokawasaki/pen/JwVwry
Here is the Vue component:
Vue.component('audio-player', {
props: ['message'],
data: () => ({
audio: undefined,
loaded: false,
playing: false,
currentTime: '00:00',
totalTime: '00:00',
percent: '0%',
draggableClasses: ['pin'],
currentlyDragged: null
}),
computed: {},
methods: {
formatTime(time) {
var min = Math.floor(time / 60);
var sec = Math.floor(time % 60);
return min + ':' + ((sec < 10) ? ('0' + sec) : sec);
},
loadedMetaData() {
this.totalTime = this.formatTime(this.audio.duration)
},
canPlay() {
this.loaded = true
},
timeUpdate(){
var current = this.audio.currentTime;
var percent = (current / this.audio.duration) * 100;
this.percent = percent + '%';
this.currentTime = this.formatTime(current);
},
ended(){
this.playing = false
this.audio.currentTime = 0
},
isDraggable(el) {
let canDrag = false;
let classes = Array.from(el.classList);
this.draggableClasses.forEach(draggable => {
if (classes.indexOf(draggable) !== -1)
canDrag = true;
})
return canDrag;
},
inRange(event) {
let rangeBox = getRangeBox(event);
let rect = rangeBox.getBoundingClientRect();
let direction = rangeBox.dataset.direction;
if (direction == 'horizontal') {
var min = rangeBox.offsetLeft;
var max = min + rangeBox.offsetWidth;
if (event.clientX < min || event.clientX > max) return false;
} else {
var min = rect.top;
var max = min + rangeBox.offsetHeight;
if (event.clientY < min || event.clientY > max) return false;
}
return true;
},
togglePlay() {
if (this.audio.paused) {
this.audio.play();
this.playing = true;
} else {
this.audio.pause();
this.playing = false;
}
},
makePlay() {
playpauseBtn.style.display = 'block';
loading.style.display = 'none';
},
getRangeBox(event) {
let rangeBox = event.target;
let el = currentlyDragged;
if (event.type == 'click' && isDraggable(event.target)) {
rangeBox = event.target.parentElement.parentElement;
}
if (event.type == 'mousemove') {
rangeBox = el.parentElement.parentElement;
}
return rangeBox;
},
getCoefficient(event) {
let slider = getRangeBox(event);
let rect = slider.getBoundingClientRect();
let K = 0;
if (slider.dataset.direction == 'horizontal') {
let offsetX = event.clientX - slider.offsetLeft;
let width = slider.clientWidth;
K = offsetX / width;
} else if (slider.dataset.direction == 'vertical') {
let height = slider.clientHeight;
var offsetY = event.clientY - rect.top;
K = 1 - offsetY / height;
}
return K;
},
rewind(event) {
if (this.inRange(event)) {
this.audio.currentTime = this.audio.duration * getCoefficient(event);
}
}
},
mounted() {
this.audio = this.$refs.audio
},
template: `<div class="audio-message-content">
<a v-if="loaded" class="play-pause-btn" href="#" :title="playing ? 'Clique aqui para pausar o audio' : 'Clique aqui ouvir o audio'" @click.prevent="togglePlay">
<svg key="pause" v-if="playing" x="0px" y="0px" viewBox="0 0 18 20" style="width: 18px; height: 20px; margin-top: -10px">
<path d="M17.1,20c0.49,0,0.9-0.43,0.9-0.96V0.96C18,0.43,17.6,0,17.1,0h-5.39c-0.49,0-0.9,0.43-0.9,0.96v18.07c0,0.53,0.4,0.96,0.9,0.96H17.1z M17.1,20"/>
<path d="M6.29,20c0.49,0,0.9-0.43,0.9-0.96V0.96C7.19,0.43,6.78,0,6.29,0H0.9C0.4,0,0,0.43,0,0.96v18.07C0,19.57,0.4,20,0.9,20H6.29z M6.29,20"/>
</svg>
<svg key="play" v-else x="0px" y="0px" viewBox="0 0 18 22" style="width: 18px; height: 22px; margin-top: -11px">
<path d="M17.45,10.01L1.61,0.14c-0.65-0.4-1.46,0.11-1.46,0.91V20.8c0,0.81,0.81,1.32,1.46,0.91l15.84-9.87C18.1,11.43,18.1,10.41,17.45,10.01L17.45,10.01z M17.45,10.01"/>
</svg>
</a>
<div v-else class="loading">
<div class="spinner"></div>
</div>
<div class="controls">
<span class="current-time">{{ currentTime }}</span>
<div class="slider" data-direction="horizontal" @click="">
<div class="progress" :style="{width: percent}">
<div class="pin" id="progress-pin" data-method="rewind"></div>
</div>
</div>
<span class="total-time">{{ totalTime }}</span>
</div>
<audio ref="audio" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/355309/Swing_Jazz_Drum.mp3" @loadedmetadata="loadedMetaData" @canplay="canPlay" @timeupdate="timeUpdate" @ended="ended"></audio>
</div>`
})
var app = new Vue({
el: '#app'
})
.audio-message-content {
width: 400px;
min-width: 300px;
height: 56px;
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.07);
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 24px;
padding-right: 24px;
border-radius: 4px;
user-select: none;
-webkit-user-select: none;
background-color: #fff;
}
.audio-message-content .play-pause-btn {
position: relative;
width: 18px;
height: 22px;
cursor: pointer;
}
.audio-message-content .play-pause-btn svg {
display: block;
position: absolute;
top: 50%;
left: 50%;
margin-left: -9px;
}
.audio-message-content .spinner {
width: 18px;
height: 18px;
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/355309/loading.png);
background-size: cover;
background-repeat: no-repeat;
animation: spin 0.4s linear infinite;
}
.audio-message-content .slider {
flex-grow: 1;
background-color: #D8D8D8;
cursor: pointer;
position: relative;
}
.audio-message-content .slider .progress {
background-color: #44BFA3;
border-radius: inherit;
position: absolute;
pointer-events: none;
}
.audio-message-content .slider .progress .pin {
height: 16px;
width: 16px;
border-radius: 8px;
background-color: #44BFA3;
position: absolute;
pointer-events: all;
box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.32);
}
.audio-message-content .controls {
font-family: 'Roboto', sans-serif;
font-size: 16px;
line-height: 18px;
color: #55606E;
display: flex;
flex-grow: 1;
justify-content: space-between;
align-items: center;
margin-left: 24px;
}
.audio-message-content .controls .slider {
margin-left: 16px;
margin-right: 16px;
border-radius: 2px;
height: 4px;
}
.audio-message-content .controls .slider .progress {
width: 0;
height: 100%;
}
.audio-message-content .controls .slider .progress .pin {
right: -8px;
top: -6px;
}
.audio-message-content .controls span {
cursor: default;
}
svg, img {
display: block;
}
@keyframes spin {
from {
transform: rotateZ(0);
}
to {
transform: rotateZ(1turn);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<audio-player></audio-player>
</div>
Vue Component Codepen: https://codepen.io/caiokawasaki/pen/QzRMwz
Functions like the following I could not understand nor find anything on the internet:
window[handleMethod]
window[pin.dataset.method]
Can anyone help me finalize this component?
Edit
I've converted all of the html and javascript into a Vue component but anyway it still is not working properly.
The only thing that is not working properly is the progress bar. It needs to perform two functions:
- Clicking it should go to the desired time.
- When clicking on the pin and drag it should go to the desired time.
I use Vue Cli, neither of the above two work in the form of .vue files, but in Codepen normally written only function 2 works.