traxxx/assets/components/form/range.vue

246 lines
4.2 KiB
Vue
Executable File

<template>
<div class="range-container">
<div
class="label label-start"
:class="{ disabled }"
@click="setValue('valueA', 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="valueA"
:min="min"
:max="max"
:data-value="valueA"
:disabled="disabled"
type="range"
class="slider"
@input="emit('input')"
@change="emit('change')"
@click.stop
>
<input
v-model.number="valueB"
:min="min"
:max="max"
:data-value="valueB"
:disabled="disabled"
type="range"
class="slider"
@input="emit('input')"
@change="emit('change')"
@click.stop
>
</div>
<div
class="label label-end"
:class="{ disabled }"
@click="setValue('valueB', max)"
>
<slot name="end" />
</div>
</div>
</template>
<script>
import { nextTick } from 'vue';
function minValue() {
return Math.min(this.valueA, this.valueB);
}
function maxValue() {
return Math.max(this.valueA, this.valueB);
}
function minPercentage() {
return ((this.minValue - this.min) / (this.max - this.min)) * 100;
}
function maxPercentage() {
return ((this.maxValue - this.min) / (this.max - this.min)) * 100;
}
function emit(type = 'change') {
if (this.values) {
this.$emit(type, [this.values[this.minValue], this.values[this.maxValue]]);
return;
}
this.$emit(type, [this.minValue, this.maxValue]);
}
function setNearest(event) {
if (this.allowEnable) {
this.emit('enable');
}
nextTick(() => {
if (!this.disabled) {
const closestValue = Math.round((event.offsetX / event.target.getBoundingClientRect().width) * (this.max - this.min)) + this.min;
const closestSlider = Math.abs(this.valueA - closestValue) < Math.abs(this.valueB - closestValue) ? 'valueA' : 'valueB';
this[closestSlider] = closestValue;
this.emit();
}
});
}
function setValue(prop, value) {
if (this.allowEnable) {
this.emit('enable');
}
nextTick(() => {
if (!this.disabled) {
this[prop] = value;
this.emit();
}
});
}
export default {
props: {
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,
},
},
emits: ['change', 'input', 'enable'],
data() {
if (this.values) {
return {
valueA: this.values.indexOf(this.value[0]),
valueB: this.values.indexOf(this.value[1]),
};
}
return {
valueA: this.value[0],
valueB: this.value[1],
};
},
computed: {
minValue,
maxValue,
minPercentage,
maxPercentage,
},
methods: {
emit,
setNearest,
setValue,
},
};
</script>
<style lang="scss">
.dark .range-container .range {
--slider-range: var(--lighten-weak);
}
</style>
<style lang="scss" scoped>
@mixin 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(--darken-weak);
}
.range-container {
display: flex;
justify-content: space-between;
}
.range {
--slider-track: var(--shadow-hint);
--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);
--slider-thumb: var(--disabled-handle);
}
}
.slider {
width: 100%;
top: 0;
margin: 0;
appearance: none;
position: absolute;
background: none;
outline: none;
pointer-events: none;
}
.slider::-webkit-slider-thumb {
@include thumb;
}
.slider::-moz-range-thumb {
@include thumb;
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>