215 lines
6.4 KiB
Vue
215 lines
6.4 KiB
Vue
<!--
|
|
* batu-ui - stopSlider.vue
|
|
*
|
|
* Created by Janis Hutz 09/30/2024, Licensed under the GPL V3 License
|
|
* https://janishutz.com, development@janishutz.com
|
|
*
|
|
*
|
|
-->
|
|
|
|
|
|
<script setup lang="ts">
|
|
import { ref, type Ref } from 'vue';
|
|
|
|
interface SliderStop {
|
|
x: number;
|
|
index: number;
|
|
}
|
|
|
|
const maxWidth = ref( window.innerWidth ); // TODO: Update to reflect slider width
|
|
const sliderOffset = -10;
|
|
const sliderFillPosLeft = ref( sliderOffset ); // Offset from left edge
|
|
const sliderFillPosRight = ref( maxWidth.value ); // Offset from right edge
|
|
const sliderStops: Ref<SliderStop[]> = ref( [] );
|
|
const sliderIndex = ref( 0 );
|
|
let intervalSize = 0;
|
|
|
|
const setUp = ( stops: number, preset?: number ) => {
|
|
maxWidth.value = document.getElementById( 'slider-' + props[ 'slider-id' ] )!.clientWidth;
|
|
sliderStops.value = [];
|
|
intervalSize = maxWidth.value / ( stops - 1 );
|
|
for ( let i = 0; i < stops; i ++ ) {
|
|
sliderStops.value.push( {
|
|
x: intervalSize * i - 10,
|
|
index: i,
|
|
} );
|
|
}
|
|
|
|
setSliderIndex( preset ?? 0 );
|
|
}
|
|
|
|
const xOffset = ref( 0 );
|
|
const isDragging = ref( false );
|
|
const sliderKnobPos = ref( sliderOffset );
|
|
let originalSliderPos = sliderOffset;
|
|
|
|
const startDrag = ( x: number ) => {
|
|
xOffset.value = x;
|
|
isDragging.value = true;
|
|
originalSliderPos = sliderKnobPos.value;
|
|
}
|
|
|
|
const setSliderIndex = ( index: number ) => {
|
|
sliderIndex.value = index ?? 0;
|
|
sliderKnobPos.value = sliderIndex.value * intervalSize + sliderOffset;
|
|
sliderFillPosRight.value = maxWidth.value - sliderIndex.value * intervalSize;
|
|
emits( 'slider-pos', sliderIndex.value );
|
|
}
|
|
|
|
const handleDrag = ( x: number ) => {
|
|
if ( isDragging.value ) {
|
|
const newPos = originalSliderPos + ( x - xOffset.value );
|
|
if ( newPos <= ( maxWidth.value + sliderOffset ) && newPos >= sliderOffset ) {
|
|
sliderKnobPos.value = newPos;
|
|
} else if ( newPos < sliderOffset ) {
|
|
sliderKnobPos.value = sliderOffset;
|
|
} else {
|
|
sliderKnobPos.value = maxWidth.value + sliderOffset;
|
|
}
|
|
|
|
// Calculate progress of slider background / progress bar
|
|
sliderIndex.value = Math.round( sliderKnobPos.value / intervalSize );
|
|
sliderFillPosRight.value = maxWidth.value - sliderIndex.value * intervalSize;
|
|
emits( 'slider-pos', sliderIndex.value );
|
|
}
|
|
}
|
|
|
|
const endDrag = () => {
|
|
isDragging.value = false;
|
|
|
|
// snapping
|
|
sliderIndex.value = Math.round( sliderKnobPos.value / intervalSize );
|
|
sliderFillPosRight.value = maxWidth.value - sliderIndex.value * intervalSize;
|
|
sliderKnobPos.value = sliderIndex.value * intervalSize + sliderOffset;
|
|
emits( 'slider-pos', sliderIndex.value );
|
|
}
|
|
|
|
const props = defineProps( {
|
|
'slider-id': {
|
|
default: '1',
|
|
required: true,
|
|
type: String
|
|
}
|
|
} );
|
|
|
|
|
|
defineExpose( {
|
|
setUp
|
|
} );
|
|
|
|
const emits = defineEmits( [ 'slider-pos', 'ready' ] );
|
|
setTimeout( () => {
|
|
emits( 'ready', true );
|
|
} );
|
|
|
|
window.addEventListener( 'resize', () => {
|
|
setUp( sliderStops.value.length, sliderIndex.value );
|
|
} );
|
|
</script>
|
|
|
|
|
|
<template>
|
|
<div class="slider-container">
|
|
<div :class="'slider-knob' + ( isDragging ? ' dragging' : '' )" :style="'left: ' + sliderKnobPos + 'px;'">
|
|
<div :class="'slider-drag-support' + ( isDragging ? ' dragging' : '' )" @mousemove="( e ) => handleDrag( e.clientX )"
|
|
@mouseup="() => endDrag()" @mousedown="( e ) => startDrag( e.clientX )"
|
|
@touchstart="( e ) => startDrag( e.touches[ 0 ].clientX )"
|
|
@touchend="() => endDrag()" @touchmove="( e ) => handleDrag( e.touches[ 0 ].clientX )"></div>
|
|
</div>
|
|
|
|
|
|
<div :class="'slider-stop' + ( stop.index <= sliderIndex ? ' stop-filled' : '' )"
|
|
v-for="stop in sliderStops" v-bind:key="stop.index" :style="'left: ' + stop.x + 'px;'"
|
|
@click="setSliderIndex( stop.index )"></div>
|
|
<div class="slider" :id="'slider-' + $props[ 'slider-id' ]">
|
|
<div class="slider-fill" :style="'left: ' + sliderFillPosLeft + 'px; width: ' + ( maxWidth - sliderFillPosLeft - sliderFillPosRight ) + 'px'"></div>
|
|
<div :class="'slider-stop-fill' + ( stop.index <= sliderIndex ? ' stop-filled' : '' )"
|
|
v-for="stop in sliderStops" v-bind:key="stop.index" :style="'left: ' + stop.x + 'px;'"
|
|
@click="setSliderIndex( stop.index )"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
.slider-container {
|
|
position: relative;
|
|
}
|
|
|
|
.slider-knob {
|
|
background-color: var( --hover-color );
|
|
width: 25px;
|
|
height: 25px;
|
|
border-radius: 15px;
|
|
position: absolute;
|
|
z-index: 5;
|
|
top: -5px;
|
|
transition: left 0.5s;
|
|
}
|
|
|
|
.slider-knob.dragging {
|
|
transition: none;
|
|
}
|
|
|
|
.slider-drag-support {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50px;
|
|
background: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
|
|
.slider-drag-support.dragging {
|
|
position: fixed;
|
|
cursor: move;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
}
|
|
|
|
.slider {
|
|
width: 100%;
|
|
height: 10px;
|
|
position: relative;
|
|
border-radius: 15px;
|
|
z-index: 2;
|
|
border: 2px var( --gray-color ) solid;
|
|
background-color: var( --background-color );
|
|
}
|
|
|
|
.slider-fill {
|
|
height: 10px;
|
|
position: absolute;
|
|
background-color: var( --slider-color );
|
|
transition: width 0.5s;
|
|
}
|
|
|
|
.slider-stop {
|
|
border-radius: 10px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: solid 2px var( --gray-color );
|
|
position: absolute;
|
|
top: -5px;
|
|
transition: background-color 0.5s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.slider-stop-fill {
|
|
border-radius: 10px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: none;
|
|
position: absolute;
|
|
top: -5px;
|
|
background-color: var( --background-color );
|
|
transition: background-color 0.5s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.stop-filled {
|
|
background-color: var( --slider-color );
|
|
}
|
|
</style> |