418 lines
13 KiB
JavaScript
418 lines
13 KiB
JavaScript
import baseComponent from '../helpers/baseComponent'
|
|
import classNames from '../helpers/classNames'
|
|
import eventsMixin from '../helpers/eventsMixin'
|
|
import styleToCssString from '../helpers/styleToCssString'
|
|
import { getTouchPoints, getPointsNumber } from '../helpers/gestures'
|
|
|
|
/**
|
|
* 获取小数位数
|
|
*/
|
|
const getPrecision = (step) => {
|
|
const stepString = step.toString()
|
|
return stepString.indexOf('.') >= 0 ? stepString.length - stepString.indexOf('.') - 1 : 0
|
|
}
|
|
|
|
/**
|
|
* 返回精度正确的值
|
|
*/
|
|
const checkValuePrecision = (val, step, min) => {
|
|
const closestStep = Math.round((val - min) / step) * step + min
|
|
const precision = getPrecision(step)
|
|
return parseFloat(closestStep.toFixed(precision))
|
|
}
|
|
|
|
/**
|
|
* 获取样式
|
|
*/
|
|
const getStyles = (value) => {
|
|
return Array.isArray(value) ? value.map((n) => styleToCssString(n)) : styleToCssString(value)
|
|
}
|
|
|
|
const defaultEvents = {
|
|
onChange() {},
|
|
onAfterChange() {},
|
|
}
|
|
|
|
baseComponent({
|
|
behaviors: [eventsMixin({ defaultEvents })],
|
|
relations: {
|
|
'../field/index': {
|
|
type: 'ancestor',
|
|
},
|
|
},
|
|
properties: {
|
|
prefixCls: {
|
|
type: String,
|
|
value: 'wux-slider',
|
|
},
|
|
min: {
|
|
type: Number,
|
|
value: 0,
|
|
observer: 'getMarks',
|
|
},
|
|
max: {
|
|
type: Number,
|
|
value: 100,
|
|
observer: 'getMarks',
|
|
},
|
|
step: {
|
|
type: Number,
|
|
value: 1,
|
|
observer: 'getMarks',
|
|
},
|
|
defaultValue: {
|
|
type: Array,
|
|
value: [0],
|
|
},
|
|
value: {
|
|
type: Array,
|
|
value: [0],
|
|
observer(newVal) {
|
|
if (this.data.controlled) {
|
|
this.updated(newVal)
|
|
}
|
|
},
|
|
},
|
|
controlled: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
showMark: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
showValue: {
|
|
type: [Boolean, Object],
|
|
value: false,
|
|
},
|
|
tipFormatter: {
|
|
type: String,
|
|
value: '{d}',
|
|
},
|
|
markStyle: {
|
|
type: [String, Object, Array],
|
|
value: '',
|
|
observer(newVal) {
|
|
this.setData({
|
|
extMarkStyle: getStyles(newVal),
|
|
})
|
|
},
|
|
},
|
|
handleStyle: {
|
|
type: [String, Object, Array],
|
|
value: '',
|
|
observer(newVal) {
|
|
this.setData({
|
|
extHandleStyle: getStyles(newVal),
|
|
})
|
|
},
|
|
},
|
|
trackStyle: {
|
|
type: [String, Object, Array],
|
|
value: '',
|
|
observer(newVal) {
|
|
this.setData({
|
|
extTrackStyle: getStyles(newVal),
|
|
})
|
|
},
|
|
},
|
|
railStyle: {
|
|
type: [String, Object],
|
|
value: '',
|
|
observer(newVal) {
|
|
this.setData({
|
|
extRailStyle: styleToCssString(newVal),
|
|
})
|
|
},
|
|
},
|
|
wrapStyle: {
|
|
type: [String, Object],
|
|
value: '',
|
|
observer(newVal) {
|
|
this.setData({
|
|
extWrapStyle: styleToCssString(newVal),
|
|
})
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
offsets: [],
|
|
inputValue: [],
|
|
extMarkStyle: '',
|
|
extHandleStyle: '',
|
|
extTrackStyle: '',
|
|
extRailStyle: '',
|
|
extWrapStyle: '',
|
|
isTouched: false,
|
|
swiping: false,
|
|
},
|
|
computed: {
|
|
classes: ['prefixCls, disabled, tipFormatter', function(prefixCls, disabled, tipFormatter) {
|
|
const wrap = classNames(prefixCls, {
|
|
[`${prefixCls}--disabled`]: disabled,
|
|
[`${prefixCls}--has-tip`]: !!tipFormatter,
|
|
})
|
|
const min = `${prefixCls}__min`
|
|
const railWrap = `${prefixCls}__rail-wrap`
|
|
const rail = `${prefixCls}__rail`
|
|
const mark = `${prefixCls}__mark`
|
|
const track = `${prefixCls}__track`
|
|
const handle = `${prefixCls}__handle`
|
|
const max = `${prefixCls}__max`
|
|
|
|
return {
|
|
wrap,
|
|
min,
|
|
railWrap,
|
|
rail,
|
|
mark,
|
|
track,
|
|
handle,
|
|
max,
|
|
}
|
|
}],
|
|
},
|
|
observers: {
|
|
inputValue(newVal) {
|
|
const offsets = newVal.map((value) => this.calcOffset(this.checkValue(value)))
|
|
this.setData({ offsets })
|
|
},
|
|
},
|
|
methods: {
|
|
/**
|
|
* 更新选中值及偏移量
|
|
*/
|
|
updated(inputValue) {
|
|
if (this.hasFieldDecorator) return
|
|
if (this.data.inputValue !== inputValue) {
|
|
this.setData({ inputValue })
|
|
}
|
|
},
|
|
/**
|
|
* 手指触摸动作开始
|
|
*/
|
|
onTouchStart(e) {
|
|
if (this.data.disabled || getPointsNumber(e) > 1) return
|
|
const { index } = e.currentTarget.dataset
|
|
this.isMoved = false
|
|
this.startX = getTouchPoints(e).x
|
|
this.moveX = 0
|
|
// 记录选中值发生改变时的初始偏移量
|
|
this.startPos = this.data.offsets[index] || 0
|
|
// 记录最后一次选中项
|
|
this.setData({ last: index, isTouched: true, isMoved: false })
|
|
},
|
|
/**
|
|
* 手指触摸后移动
|
|
*/
|
|
onTouchMove(e) {
|
|
if (this.data.disabled || getPointsNumber(e) > 1) return
|
|
const { index } = e.currentTarget.dataset
|
|
const { prefixCls } = this.data
|
|
|
|
this.isMoved = true
|
|
this.setData({ isMoved: true })
|
|
this.moveX = getTouchPoints(e).x
|
|
|
|
this.getRect(`.${prefixCls}__rail`).then((rect) => {
|
|
if (!rect || !this.isMoved) return
|
|
|
|
const diffX = (this.moveX - this.startX) / rect.width * 100
|
|
const nextOffsets = [...this.data.offsets]
|
|
const offset = this.checkValue(this.startPos + diffX, 0, 100)
|
|
const { inputValue } = this.data
|
|
const currentValue = this.calcValue(offset)
|
|
const prevValue = inputValue[index - 1]
|
|
const nextValue = inputValue[index + 1]
|
|
|
|
// 通过合法的当前值反算偏移量
|
|
nextOffsets[index] = this.calcOffset(currentValue)
|
|
|
|
// 判断当前值是否小于前一值,是则重新计算偏移量
|
|
if (prevValue && prevValue > currentValue) {
|
|
nextOffsets[index] = this.calcOffset(prevValue)
|
|
}
|
|
|
|
// 判断当前值是否大于后一值,是则重新计算偏移量
|
|
if (nextValue && nextValue < currentValue) {
|
|
nextOffsets[index] = this.calcOffset(nextValue)
|
|
}
|
|
|
|
// 判断当前值是否发生变化,是则触发 change 事件
|
|
if (inputValue[index] !== currentValue) {
|
|
const value = this.getValue(nextOffsets)
|
|
|
|
if (!this.data.controlled) {
|
|
this.updated(value)
|
|
}
|
|
|
|
this.triggerEvent('change', { offsets: nextOffsets, value })
|
|
}
|
|
})
|
|
},
|
|
/**
|
|
* 手指触摸动作结束
|
|
*/
|
|
onTouchEnd(e) {
|
|
if (this.data.disabled || getPointsNumber(e) > 1 || !this.isMoved) return
|
|
this.isMoved = false
|
|
this.setData({ isTouched: false, isMoved: false })
|
|
const { offsets } = this.data
|
|
const value = this.getValue(offsets)
|
|
this.triggerEvent('afterChange', { offsets, value })
|
|
},
|
|
/**
|
|
* 获取界面上的节点信息
|
|
*/
|
|
getRect(selector, all) {
|
|
return new Promise((resolve) => {
|
|
wx
|
|
.createSelectorQuery()
|
|
.in(this)[all ? 'selectAll' : 'select'](selector)
|
|
.boundingClientRect((rect) => {
|
|
if (all && Array.isArray(rect) && rect.length) {
|
|
resolve(rect)
|
|
}
|
|
|
|
if (!all && rect) {
|
|
resolve(rect)
|
|
}
|
|
})
|
|
.exec()
|
|
})
|
|
},
|
|
/**
|
|
* 计算选中值
|
|
*/
|
|
calcValue(ratio) {
|
|
const { min, max } = this.data
|
|
return this.trimValue(ratio * (max - min) / 100 + min)
|
|
},
|
|
/**
|
|
* 计算偏移量
|
|
*/
|
|
calcOffset(value) {
|
|
const { min, max } = this.data
|
|
const ratio = (value - min) / (max - min)
|
|
return ratio * 100
|
|
},
|
|
/**
|
|
* 判断元素是否在指定的范围内
|
|
*/
|
|
checkValue(val, min = this.data.min, max = this.data.max) {
|
|
if (val <= min) {
|
|
return min
|
|
}
|
|
|
|
if (val >= max) {
|
|
return max
|
|
}
|
|
|
|
return val
|
|
},
|
|
/**
|
|
* 验证选中值
|
|
*/
|
|
trimValue(val) {
|
|
return checkValuePrecision(this.checkValue(val), this.data.step, this.data.min)
|
|
},
|
|
/**
|
|
* 获取选中值
|
|
*/
|
|
getValue(offsets = this.data.offsets) {
|
|
return offsets.map((offset) => this.calcValue(offset))
|
|
},
|
|
/**
|
|
* 获取间断点位置
|
|
*/
|
|
getMarks() {
|
|
if (!this.data.showMark) return
|
|
|
|
const {min, max, step } = this.data
|
|
const count = (max - min) / step
|
|
const marks = []
|
|
const offset = 100 * step / (max - min)
|
|
|
|
for (let i = 1; i < count; i++) {
|
|
marks.push(i * offset)
|
|
}
|
|
|
|
this.setData({ marks })
|
|
},
|
|
noop() {},
|
|
getValueByPosition(position) {
|
|
const { min, max, step } = this.data
|
|
const newPosition = position < min ? min : position > max ? max : position
|
|
let value = min
|
|
const lengthPerStep = 100 / ((max - min) / step)
|
|
const steps = Math.round(newPosition / lengthPerStep)
|
|
value = steps * lengthPerStep * (max - min) * 0.01 + min
|
|
return value
|
|
},
|
|
onRailClick(e) {
|
|
if (this.data.disabled || getPointsNumber(e) > 1) return
|
|
const { prefixCls, min, max, inputValue: sliderValue } = this.data
|
|
this.getRect(`.${prefixCls}__rail-wrap`).then((rect) => {
|
|
const position = ((getTouchPoints(e).x - rect.left) / Math.ceil(rect.width)) * (max - min) + min
|
|
const targetValue = this.getValueByPosition(position)
|
|
const indexLength = sliderValue.length - 1
|
|
const range = indexLength > 0
|
|
const nextOffsets = [...this.data.offsets]
|
|
let nextSliderValue = [...sliderValue]
|
|
let currentIndex = 0
|
|
if (range) {
|
|
let prevIndex = 0
|
|
let nextIndex = null
|
|
for (let i = indexLength; i >= 0; i--) {
|
|
if (sliderValue[i] <= targetValue) {
|
|
prevIndex = i
|
|
break
|
|
}
|
|
}
|
|
if (prevIndex === indexLength) {
|
|
nextIndex = prevIndex
|
|
prevIndex = nextIndex - 1
|
|
} else {
|
|
nextIndex = prevIndex + 1
|
|
}
|
|
// 移动的滑块采用就近原则
|
|
if (
|
|
Math.abs(targetValue - sliderValue[prevIndex]) >
|
|
Math.abs(targetValue - sliderValue[nextIndex])
|
|
) {
|
|
currentIndex = nextIndex
|
|
nextSliderValue[nextIndex] = targetValue
|
|
} else {
|
|
currentIndex = prevIndex
|
|
nextSliderValue[prevIndex] = targetValue
|
|
}
|
|
} else {
|
|
nextSliderValue = [targetValue]
|
|
}
|
|
|
|
nextOffsets[currentIndex] = this.calcOffset(targetValue)
|
|
|
|
if (sliderValue[currentIndex] !== targetValue) {
|
|
if (!this.data.controlled) {
|
|
this.updated(nextSliderValue)
|
|
}
|
|
|
|
this.triggerEvent('change', { offsets: nextOffsets, value: nextSliderValue })
|
|
this.triggerEvent('afterChange', { offsets: nextOffsets, value: nextSliderValue })
|
|
}
|
|
})
|
|
},
|
|
},
|
|
attached() {
|
|
const { defaultValue, value, controlled } = this.data
|
|
const inputValue = controlled ? value : defaultValue
|
|
|
|
this.getMarks()
|
|
this.updated(inputValue)
|
|
},
|
|
})
|