<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; set('change'); } } async function setRange(prop, value) { if (props.allowEnable) { emit('enable'); } await nextTick; if (!props.disabled) { range.value[prop] = value; set('change'); } } </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-light-30); --slider-thumb: var(--primary); position: relative; height: 1.25rem; flex-grow: 1; border-radius: .625rem; &.disabled { --slider-range: var(--shadow-weak-40); --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 { 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 { height: 100%; 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>