traxxx-web/components/form/range.vue

226 lines
4.3 KiB
Vue
Raw Normal View History

<template>
<div class="range-container">
<div
class="label label-start"
:class="{ disabled }"
@click="setRange(range.a === minValue ? 'a' : 'b', min)"
>
<slot name="start" />
</div>
<div
class="range"
:class="{ disabled }"
:style="{ background: `linear-gradient(90deg, var(--slider-track) ${minPercentage}%, var(--slider-range) ${minPercentage}%, var(--slider-range) ${maxPercentage}%, var(--slider-track) ${maxPercentage}%)` }"
@click="setNearest"
>
<input
v-model.number="range.a"
:min="min"
:max="max"
:data-value="range.a"
:disabled="disabled"
type="range"
class="slider"
@input="set('input')"
@change="set('change')"
@click.stop
>
<input
v-model.number="range.b"
:min="min"
:max="max"
:data-value="range.b"
:disabled="disabled"
type="range"
class="slider"
@input="set('input')"
@change="set('change')"
@click.stop
>
</div>
<div
class="label label-end"
:class="{ disabled }"
@click="setRange(range.b === maxValue ? 'b' : 'a', max)"
>
<slot name="end" />
</div>
</div>
</template>
<script setup>
import {
ref,
computed,
nextTick,
} from 'vue';
const props = defineProps({
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 10,
},
value: {
type: Array,
default: () => [3, 7],
},
values: {
type: Array,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
allowEnable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['change', 'input', 'enable']);
const range = ref({
a: props.values ? props.values.indexOf(props.value[0]) : props.value[0],
b: props.values ? props.values.indexOf(props.value[1]) : props.value[1],
});
const minValue = computed(() => Math.min(range.value.a, range.value.b));
const maxValue = computed(() => Math.max(range.value.a, range.value.b));
const minPercentage = computed(() => ((minValue.value - props.min) / (props.max - props.min)) * 100);
const maxPercentage = computed(() => ((maxValue.value - props.min) / (props.max - props.min)) * 100);
function set(type = 'change') {
if (props.values) {
emit(type, [props.values[minValue.value], props.values[maxValue.value]]);
return;
}
emit(type, [minValue.value, maxValue.value]);
}
async function setNearest(event) {
if (props.allowEnable) {
emit('enable');
}
await nextTick;
if (!props.disabled) {
const closestValue = Math.round((event.offsetX / event.target.getBoundingClientRect().width) * (props.max - props.min)) + props.min;
const closestSlider = Math.abs(range.value.a - closestValue) < Math.abs(range.value.b - closestValue) ? 'a' : 'b';
range.value[closestSlider] = closestValue;
emit('change', [minValue.value, maxValue.value]);
}
}
async function setRange(prop, value) {
if (props.allowEnable) {
emit('enable');
}
await nextTick;
if (!props.disabled) {
range.value[prop] = value;
emit('change', [minValue.value, maxValue.value]);
}
}
</script>
<style>
.dark .range-container .range {
--slider-range: var(--lighten-weak-10);
}
</style>
<style scoped>
.range-container {
display: flex;
justify-content: space-between;
}
.range {
--slider-track: var(--shadow-weak-30);
--slider-range: var(--primary-faded);
--slider-thumb: var(--primary);
position: relative;
height: 1.25rem;
flex-grow: 1;
border-radius: .625rem;
&.disabled {
--slider-range: var(--shadow-weak-10);
--slider-thumb: var(--grey-dark-10);
}
}
.slider {
width: 100%;
top: 0;
margin: 0;
appearance: none;
position: absolute;
background: none;
outline: none;
pointer-events: none;
}
.slider::-webkit-slider-thumb {
appearance: none;
display: block;
width: 1.25rem;
height: 1.25rem;
border: none;
border-radius: 50%;
background: var(--slider-thumb);
pointer-events: visible;
cursor: pointer;
box-shadow: 0 0 3px var(--shadow-weak-10);
}
.slider::-moz-range-thumb {
appearance: none;
display: block;
width: 1.25rem;
height: 1.25rem;
border: none;
border-radius: 50%;
background: var(--slider-thumb);
pointer-events: visible;
cursor: pointer;
box-shadow: 0 0 3px var(--shadow-weak-10);
transform: translateY(2px);
}
.label {
padding: 0 .5rem;
&:hover:not(.disabled) {
cursor: pointer;
::v-deep(.icon) {
fill: var(--primary);
}
}
}
::v-deep(.icon) {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
fill: var(--shadow);
}
</style>