slider.vue 11.8 KB
<template>
    <div :class="classes">
        <Input-number
            v-if="!range && showInput"
            :min="min"
            :max="max"
            :step="step"
            :value="currentValue[0]"
            :disabled="disabled"
            @on-change="handleInputChange"></Input-number>
        <div :class="[prefixCls + '-wrap']" ref="slider" @click.self="sliderClick">
            <input type="hidden" :name="name" :value="currentValue">
            <template v-if="showStops">
                <div :class="[prefixCls + '-stop']" v-for="item in stops" :style="{ 'left': item + '%' }" @click.self="sliderClick"></div>
            </template>
            <div :class="[prefixCls + '-bar']" :style="barStyle" @click.self="sliderClick"></div>
            <div
                :class="[prefixCls + '-button-wrap']"
                :style="{left: minPosition + '%'}"
                @touchstart="onPointerDown($event, 'min')"
                @mousedown="onPointerDown($event, 'min')">
                <Tooltip :controlled="pointerDown === 'min'" placement="top" :content="tipFormat(currentValue[0])"
                         :disabled="tipDisabled" :always="showTip === 'always'" ref="minTooltip">
                    <div :class="minButtonClasses"></div>
                </Tooltip>
            </div>
            <div v-if="range"
                 :class="[prefixCls + '-button-wrap']"
                 :style="{left: maxPosition + '%'}"
                 @touchstart="onPointerDown($event, 'max')"
                 @mousedown="onPointerDown($event, 'max')">
                <Tooltip :controlled="pointerDown === 'max'" placement="top" :content="tipFormat(currentValue[1])"
                         :disabled="tipDisabled" :always="showTip === 'always'" ref="maxTooltip">
                    <div :class="maxButtonClasses"></div>
                </Tooltip>
            </div>
        </div>
    </div>
</template>
<script>
    import InputNumber from '../../components/input-number/input-number.vue';
    import Tooltip from '../../components/tooltip/tooltip.vue';
    import { getStyle, oneOf } from '../../utils/assist';
    import { on, off } from '../../utils/dom';
    import Emitter from '../../mixins/emitter';

    const prefixCls = 'ivu-slider';

    export default {
        name: 'Slider',
        mixins: [ Emitter ],
        components: { InputNumber, Tooltip },
        props: {
            min: {
                type: Number,
                default: 0
            },
            max: {
                type: Number,
                default: 100
            },
            step: {
                type: Number,
                default: 1
            },
            range: {
                type: Boolean,
                default: false
            },
            value: {
                type: [Number, Array],
                default: 0
            },
            disabled: {
                type: Boolean,
                default: false
            },
            showInput: {
                type: Boolean,
                default: false
            },
            showStops: {
                type: Boolean,
                default: false
            },
            tipFormat: {
                type: Function,
                default (val) {
                    return val;
                }
            },
            showTip: {
                type: String,
                default: 'hover',
                validator (value) {
                    return oneOf(value, ['hover', 'always', 'never']);
                }
            },
            name: {
                type: String
            }
        },
        data () {
            const val = this.checkLimits(Array.isArray(this.value) ? this.value : [this.value]);
            return {
                prefixCls: prefixCls,
                currentValue: val,
                dragging: false,
                pointerDown: '',
                startX: 0,
                currentX: 0,
                startPos: 0,
                oldValue: val
            };
        },
        watch: {
            value (val) {
                val = this.checkLimits(Array.isArray(val) ? val : [val]);
                if (val[0] !== this.currentValue[0] || val[1] !== this.currentValue[1]) {
                    this.currentValue = val;
                }
            },
            currentValue (val) {
                this.$nextTick(() => {
                    this.$refs.minTooltip.updatePopper();
                    if (this.range) {
                        this.$refs.maxTooltip.updatePopper();
                    }
                });
                const exportValue = this.range ? val : val[0];
                this.$emit('input', exportValue);
                this.$emit('on-input', exportValue);
            }
        },
        computed: {
            classes () {
                return [
                    `${prefixCls}`,
                    {
                        [`${prefixCls}-input`]: this.showInput && !this.range,
                        [`${prefixCls}-range`]: this.range,
                        [`${prefixCls}-disabled`]: this.disabled
                    }
                ];
            },
            minButtonClasses () {
                return [
                    `${prefixCls}-button`,
                    {
                        [`${prefixCls}-button-dragging`]: this.pointerDown === 'min'
                    }
                ];
            },
            maxButtonClasses () {
                return [
                    `${prefixCls}-button`,
                    {
                        [`${prefixCls}-button-dragging`]: this.pointerDown === 'max'
                    }
                ];
            },
            minPosition () {
                const val = this.currentValue;
                return (val[0] - this.min) / this.valueRange * 100;
            },
            maxPosition: function () {
                const val = this.currentValue;

                return (val[1] - this.min) / this.valueRange * 100;
            },
            barStyle () {

                const style = {
                    width: (this.currentValue[0] - this.min) / this.valueRange * 100 + '%'
                };

                if (this.range) {
                    style.left = (this.currentValue[0] - this.min) / this.valueRange * 100 + '%';
                    style.width = (this.currentValue[1] - this.currentValue[0]) / this.valueRange * 100 + '%';
                }

                return style;
            },
            stops () {
                let stopCount = this.valueRange / this.step;
                let result = [];
                let stepWidth = 100 * this.step / this.valueRange;
                for (let i = 1; i < stopCount; i++) {
                    result.push(i * stepWidth);
                }
                return result;
            },
            sliderWidth () {
                return parseInt(getStyle(this.$refs.slider, 'width'), 10);
            },
            tipDisabled () {
                return this.tipFormat(this.currentValue[0]) === null || this.showTip === 'never';
            },
            valueRange(){
                return this.max - this.min;
            }
        },
        methods: {
            getPointerX (e) {
                return e.type.indexOf('touch') !== -1 ? e.touches[0].clientX : e.clientX;
            },
            checkLimits ([min, max]) {
                min = Math.max(this.min, min);
                min = Math.min(this.max, min);

                max = Math.max(this.min, min, max);
                max = Math.min(this.max, max);
                return [min, max];
            },
            onPointerDown (event, type) {
                if (this.disabled) return;
                event.preventDefault();
                this.pointerDown = type;

                this.onPointerDragStart(event);
                on(window, 'mousemove', this.onPointerDrag);
                on(window, 'touchmove', this.onPointerDrag);
                on(window, 'mouseup', this.onPointerDragEnd);
                on(window, 'touchend', this.onPointerDragEnd);
            },
            onPointerDragStart (event) {
                this.dragging = false;
                this.startX = this.getPointerX(event);
                this.startPos = (this[`${this.pointerDown}Position`] * this.valueRange / 100) + this.min;
            },
            onPointerDrag (event) {
                this.dragging = true;
                this.$refs[`${this.pointerDown}Tooltip`].visible = true;
                this.currentX = this.getPointerX(event);
                const diff = (this.currentX - this.startX) / this.sliderWidth * this.valueRange;

                this.changeButtonPosition(this.startPos + diff);
            },
            onPointerDragEnd () {
                if (this.dragging) {
                    this.dragging = false;
                    this.$refs[`${this.pointerDown}Tooltip`].visible = false;
                    this.emitChange();
                }

                this.pointerDown = '';
                off(window, 'mousemove', this.onPointerDrag);
                off(window, 'touchmove', this.onPointerDrag);
                off(window, 'mouseup', this.onPointerDragEnd);
                off(window, 'touchend', this.onPointerDragEnd);
            },
            changeButtonPosition (newPos, forceType) {
                const type = forceType || this.pointerDown;
                const index = type === 'min' ? 0 : 1;
                if (type === 'min') newPos = this.checkLimits([newPos, this.maxPosition])[0];
                else newPos = this.checkLimits([this.minPosition, newPos])[1];

                const modulus = newPos % this.step;
                const value = this.currentValue;
                value[index] = newPos - modulus;
                this.currentValue = [...value];

                if (!this.dragging) {
                    if (this.currentValue[index] !== this.oldValue[index]) {
                        this.emitChange();
                        this.oldValue[index] = this.currentValue[index];
                    }
                }
            },

            emitChange(){
                const exportValue = this.range ? this.currentValue : this.currentValue[0];
                this.$emit('on-change', exportValue);
                this.dispatch('FormItem', 'on-form-change', exportValue);
            },

            sliderClick (event) {
                if (this.disabled) return;
                const currentX = this.getPointerX(event);
                const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left;
                let newPos = ((currentX - sliderOffsetLeft) / this.sliderWidth * this.valueRange) + this.min;

                if (!this.range || newPos <= this.minPosition) this.changeButtonPosition(newPos, 'min');
                else if (newPos >= this.maxPosition) this.changeButtonPosition(newPos, 'max');
                else this.changeButtonPosition(newPos, ((newPos - this.firstPosition) <= (this.secondPosition - newPos)) ? 'min' : 'max');
            },

            handleInputChange (val) {
                this.currentValue = [val, this.currentValue[1]];
                const exportValue = this.range ? this.currentValue : this.currentValue[0];
                this.$emit('on-change', exportValue);
                this.dispatch('FormItem', 'on-form-change', exportValue);
            },
        },
        mounted () {
            // #2852
            this.$on('on-visible-change', (val) => {
                if (val && this.showTip === 'always') {
                    this.$refs.minTooltip.doDestroy();
                    if (this.range) {
                        this.$refs.maxTooltip.doDestroy();
                    }
                    this.$nextTick(() => {
                        this.$refs.minTooltip.updatePopper();
                        if (this.range) {
                            this.$refs.maxTooltip.updatePopper();
                        }
                    });
                }
            });
        }
    };
</script>