450 lines
14 KiB
Vue
450 lines
14 KiB
Vue
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 (
|
||
<Track
|
||
class={trackClassName}
|
||
vertical={vertical}
|
||
reverse={reverse}
|
||
included={included}
|
||
offset={offsets[i - 1]}
|
||
length={offsets[i] - offsets[i - 1]}
|
||
style={trackStyle[index]}
|
||
key={i}
|
||
/>
|
||
);
|
||
});
|
||
},
|
||
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);
|