<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>