import { Prop, toNative } from 'vue-facing-decorator'; import { TsxComponentExtendedBase } from '../../app/vuestsx-extended'; import { Component } from '../../app/vuetsx'; import './css/slider.css'; interface SliderProps { values: [number, number]; minLimit?: number; maxLimit?: number; step?: number; priceGap?: number; onInput: (newValues: [number, number]) => void; /** Fires once with the final values when the user releases the handle (native `change`) — lets consumers commit only after dragging ended instead of on every drag tick. */ onChange?: (newValues: [number, number]) => void; disabled?: boolean; } @Component class Slider extends TsxComponentExtendedBase implements SliderProps { @Prop() values: [number, number]; @Prop() minLimit?: number; @Prop() maxLimit?: number; @Prop() step?: number; @Prop() priceGap?: number; @Prop() onInput: (newValues: [number, number]) => void; @Prop() onChange?: (newValues: [number, number]) => void; @Prop() disabled?: boolean; minVal = 0; maxVal = 0; mounted() { // Initialize state from props this.minVal = this.values[0]; this.maxVal = this.values[1]; } updated() { this.minVal = this.values[0]; this.maxVal = this.values[1]; } get progressStyle() { const min = this.minLimit ?? 0; const max = this.maxLimit ?? 100; const range = max - min; const left = ((this.minVal - min) / range) * 100; const right = 100 - ((this.maxVal - min) / range) * 100; return { left: `${left}%`, right: `${right}%`, }; } get minThumbStyle() { const gap = this.priceGap || 0; // When the two thumbs overlap, whichever paints on top captures the // pointer. The topmost thumb must be the one that still has room to escape, // otherwise the pair deadlocks (QA__G-383, both directions): // - overlap in the UPPER half (min was dragged up): min must be on top so it // can be dragged back down — raise .range-min above .range-max. // - overlap in the LOWER half (max was dragged down): max must be on top so it // can be dragged back up — keep the default DOM stacking (.range-max paints // on top because it follows .range-min in the DOM). const overlapping = this.minVal >= this.maxVal - gap; const min = this.minLimit ?? 0; const max = this.maxLimit ?? 100; const overlapInUpperHalf = this.maxVal >= (min + max) / 2; return { zIndex: overlapping && overlapInUpperHalf ? 5 : undefined, }; } handleSlider(e) { if (this.disabled) { return; } const value = parseInt(e.target.value); const isMin = e.target.classList.contains('range-min'); let min = this.minVal; let max = this.maxVal; const gap = this.priceGap || 0; if (isMin) { min = Math.min(value, max - gap); min = Math.max(this.minLimit || 0, min); if (min !== this.minVal) { this.minVal = min; this.onInput([ min, max, ]); } } else { max = Math.max(value, min + gap); max = Math.min(this.maxLimit || 0, max); if (max !== this.maxVal) { this.maxVal = max; this.onInput([ min, max, ]); } } this.$nextTick(() => { this.setRangeMin(); this.setRangeMax(); }); } handleSliderCommit() { if (this.disabled || this.onChange == null) { return; } // `input` events already clamped + stored the values — emit the settled pair. this.onChange([ this.minVal, this.maxVal, ]); } setRangeMin() { const minValInput = this.$refs.minValRef as HTMLInputElement; minValInput.value = this.values[0].toString(); } setRangeMax() { const maxValInput = this.$refs.maxValRef as HTMLInputElement; maxValInput.value = this.values[1].toString(); } render(h) { return (
); } } export default toNative(Slider);