import classNames from '../../_util/classNames'; import PropTypes, { withUndefined } from '../../_util/vue-types'; import BaseMixin from '../../_util/BaseMixin'; import { initDefaultProps, hasProp } from '../../_util/props-util'; import Track from './common/Track'; import createSlider from './common/createSlider'; import * as utils from './utils'; const trimAlignValue = ({ value, handle, bounds, props }) => { const { allowCross, pushable } = props; const thershold = Number(pushable); const valInRange = utils.ensureValueInRange(value, props); let valNotConflict = valInRange; if (!allowCross && handle != null && bounds !== undefined) { if (handle > 0 && valInRange <= bounds[handle - 1] + thershold) { valNotConflict = bounds[handle - 1] + thershold; } if (handle < bounds.length - 1 && valInRange >= bounds[handle + 1] - thershold) { valNotConflict = bounds[handle + 1] - thershold; } } return utils.ensureValuePrecision(valNotConflict, props); }; const rangeProps = { defaultValue: PropTypes.arrayOf(PropTypes.number), value: PropTypes.arrayOf(PropTypes.number), count: PropTypes.number, pushable: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.number])), allowCross: PropTypes.looseBool, disabled: PropTypes.looseBool, reverse: PropTypes.looseBool, tabindex: PropTypes.arrayOf(PropTypes.number), prefixCls: PropTypes.string, min: PropTypes.number, max: PropTypes.number, autofocus: PropTypes.looseBool, }; const Range = { name: 'Range', inheritAttrs: false, displayName: 'Range', mixins: [BaseMixin], props: initDefaultProps(rangeProps, { count: 1, allowCross: true, pushable: false, tabindex: [], }), data() { const { count, min, max } = this; const initialValue = Array(...Array(count + 1)).map(() => min); const defaultValue = hasProp(this, 'defaultValue') ? this.defaultValue : initialValue; let { value } = this; if (value === undefined) { value = defaultValue; } const bounds = value.map((v, i) => trimAlignValue({ value: v, handle: i, props: this.$props, }), ); const recent = bounds[0] === max ? 0 : bounds.length - 1; return { sHandle: null, recent, bounds, }; }, watch: { value: { handler(val) { const { bounds } = this; this.setChangeValue(val || bounds); }, deep: true, }, min() { const { value } = this; this.setChangeValue(value || this.bounds); }, max() { const { value } = this; this.setChangeValue(value || this.bounds); }, }, methods: { setChangeValue(value) { const { bounds } = this; const nextBounds = value.map((v, i) => trimAlignValue({ value: v, handle: i, bounds, props: this.$props, }), ); if (nextBounds.length === bounds.length && nextBounds.every((v, i) => v === bounds[i])) return; this.setState({ bounds: nextBounds }); if (value.some(v => utils.isValueOutOfRange(v, this.$props))) { const newValues = value.map(v => { return utils.ensureValueInRange(v, this.$props); }); this.__emit('change', newValues); } }, onChange(state) { const isNotControlled = !hasProp(this, 'value'); if (isNotControlled) { this.setState(state); } else { const controlledState = {}; ['sHandle', 'recent'].forEach(item => { if (state[item] !== undefined) { controlledState[item] = state[item]; } }); if (Object.keys(controlledState).length) { this.setState(controlledState); } } const data = { ...this.$data, ...state }; const changedValue = data.bounds; this.__emit('change', changedValue); }, onStart(position) { const { bounds } = this; this.__emit('beforeChange', bounds); const value = this.calcValueByPos(position); this.startValue = value; this.startPosition = position; const closestBound = this.getClosestBound(value); this.prevMovedHandleIndex = this.getBoundNeedMoving(value, closestBound); this.setState({ sHandle: this.prevMovedHandleIndex, recent: this.prevMovedHandleIndex, }); const prevValue = bounds[this.prevMovedHandleIndex]; if (value === prevValue) return; const nextBounds = [...bounds]; nextBounds[this.prevMovedHandleIndex] = value; this.onChange({ bounds: nextBounds }); }, onEnd(force) { const { sHandle } = this; this.removeDocumentEvents(); if (sHandle !== null || force) { this.__emit('afterChange', this.bounds); } this.setState({ sHandle: null }); }, onMove(e, position) { utils.pauseEvent(e); const { bounds, sHandle } = this; const value = this.calcValueByPos(position); const oldValue = bounds[sHandle]; if (value === oldValue) return; this.moveTo(value); }, onKeyboard(e) { const { reverse, vertical } = this.$props; const valueMutator = utils.getKeyboardValueMutator(e, vertical, reverse); if (valueMutator) { utils.pauseEvent(e); const { bounds, sHandle } = this; const oldValue = bounds[sHandle === null ? this.recent : sHandle]; const mutatedValue = valueMutator(oldValue, this.$props); const value = trimAlignValue({ value: mutatedValue, handle: sHandle, bounds, props: this.$props, }); if (value === oldValue) return; const isFromKeyboardEvent = true; this.moveTo(value, isFromKeyboardEvent); } }, getClosestBound(value) { const { bounds } = this; let closestBound = 0; for (let i = 1; i < bounds.length - 1; ++i) { if (value > bounds[i]) { closestBound = i; } } if (Math.abs(bounds[closestBound + 1] - value) < Math.abs(bounds[closestBound] - value)) { closestBound += 1; } return closestBound; }, getBoundNeedMoving(value, closestBound) { const { bounds, recent } = this; let boundNeedMoving = closestBound; const isAtTheSamePoint = bounds[closestBound + 1] === bounds[closestBound]; if (isAtTheSamePoint && bounds[recent] === bounds[closestBound]) { boundNeedMoving = recent; } if (isAtTheSamePoint && value !== bounds[closestBound + 1]) { boundNeedMoving = value < bounds[closestBound + 1] ? closestBound : closestBound + 1; } return boundNeedMoving; }, getLowerBound() { return this.bounds[0]; }, getUpperBound() { const { bounds } = this; return bounds[bounds.length - 1]; }, /** * Returns an array of possible slider points, taking into account both * `marks` and `step`. The result is cached. */ getPoints() { const { marks, step, min, max } = this; const cache = this._getPointsCache; if (!cache || cache.marks !== marks || cache.step !== step) { const pointsObject = { ...marks }; if (step !== null) { for (let point = min; point <= max; point += step) { pointsObject[point] = point; } } const points = Object.keys(pointsObject).map(parseFloat); points.sort((a, b) => a - b); this._getPointsCache = { marks, step, points }; } return this._getPointsCache.points; }, moveTo(value, isFromKeyboardEvent) { const nextBounds = [...this.bounds]; const { sHandle, recent } = this; const handle = sHandle === null ? recent : sHandle; nextBounds[handle] = value; let nextHandle = handle; if (this.$props.pushable !== false) { this.pushSurroundingHandles(nextBounds, nextHandle); } else if (this.$props.allowCross) { nextBounds.sort((a, b) => a - b); nextHandle = nextBounds.indexOf(value); } this.onChange({ recent: nextHandle, sHandle: nextHandle, bounds: nextBounds, }); if (isFromKeyboardEvent) { // known problem: because setState is async, // so trigger focus will invoke handler's onEnd and another handler's onStart too early, // cause onBeforeChange and onAfterChange receive wrong value. // here use setState callback to hackļ¼Œbut not elegant this.__emit('afterChange', nextBounds); this.setState({}, () => { this.handlesRefs[nextHandle].focus(); }); this.onEnd(); } }, pushSurroundingHandles(bounds, handle) { const value = bounds[handle]; let { pushable: threshold } = this; threshold = Number(threshold); let direction = 0; if (bounds[handle + 1] - value < threshold) { direction = +1; // push to right } if (value - bounds[handle - 1] < threshold) { direction = -1; // push to left } if (direction === 0) { return; } const nextHandle = handle + direction; const diffToNext = direction * (bounds[nextHandle] - value); if (!this.pushHandle(bounds, nextHandle, direction, threshold - diffToNext)) { // revert to original value if pushing is impossible bounds[handle] = bounds[nextHandle] - direction * threshold; } }, pushHandle(bounds, handle, direction, amount) { const originalValue = bounds[handle]; let currentValue = bounds[handle]; while (direction * (currentValue - originalValue) < amount) { if (!this.pushHandleOnePoint(bounds, handle, direction)) { // can't push handle enough to create the needed `amount` gap, so we // revert its position to the original value bounds[handle] = originalValue; return false; } currentValue = bounds[handle]; } // the handle was pushed enough to create the needed `amount` gap return true; }, pushHandleOnePoint(bounds, handle, direction) { const points = this.getPoints(); const pointIndex = points.indexOf(bounds[handle]); const nextPointIndex = pointIndex + direction; if (nextPointIndex >= points.length || nextPointIndex < 0) { // reached the minimum or maximum available point, can't push anymore return false; } const nextHandle = handle + direction; const nextValue = points[nextPointIndex]; const { pushable: threshold } = this; const diffToNext = direction * (bounds[nextHandle] - nextValue); if (!this.pushHandle(bounds, nextHandle, direction, threshold - diffToNext)) { // couldn't push next handle, so we won't push this one either return false; } // push the handle bounds[handle] = nextValue; return true; }, trimAlignValue(value) { const { sHandle, bounds } = this; return trimAlignValue({ value, handle: sHandle, bounds, props: this.$props, }); }, ensureValueNotConflict(handle, val, { allowCross, pushable: thershold }) { const state = this.$data || {}; const { bounds } = state; handle = handle === undefined ? state.sHandle : handle; thershold = Number(thershold); /* eslint-disable eqeqeq */ if (!allowCross && handle != null && bounds !== undefined) { if (handle > 0 && val <= bounds[handle - 1] + thershold) { return bounds[handle - 1] + thershold; } if (handle < bounds.length - 1 && val >= bounds[handle + 1] - thershold) { return bounds[handle + 1] - thershold; } } /* eslint-enable eqeqeq */ return val; }, getTrack({ bounds, prefixCls, reverse, vertical, included, offsets, trackStyle }) { return bounds.slice(0, -1).map((_, index) => { const i = index + 1; const trackClassName = classNames({ [`${prefixCls}-track`]: true, [`${prefixCls}-track-${i}`]: true, }); return ( ); }); }, renderSlider() { const { sHandle, bounds, prefixCls, vertical, included, disabled, min, max, reverse, handle, defaultHandle, trackStyle, handleStyle, tabindex, } = this; const handleGenerator = handle || defaultHandle; const offsets = bounds.map(v => this.calcOffset(v)); const handleClassName = `${prefixCls}-handle`; const handles = bounds.map((v, i) => { let _tabIndex = tabindex[i] || 0; if (disabled || tabindex[i] === null) { _tabIndex = null; } return handleGenerator({ class: classNames({ [handleClassName]: true, [`${handleClassName}-${i + 1}`]: true, }), prefixCls, vertical, offset: offsets[i], value: v, dragging: sHandle === i, index: i, tabindex: _tabIndex, min, max, reverse, disabled, style: handleStyle[i], ref: h => this.saveHandle(i, h), onFocus: this.onFocus, onBlur: this.onBlur, }); }); return { tracks: this.getTrack({ bounds, prefixCls, reverse, vertical, included, offsets, trackStyle, }), handles, }; }, }, }; export default createSlider(Range);