UNPKG

saven

Version:
607 lines (549 loc) 16.4 kB
/** * @author chenjiajian * @property {Array} range mode为 selector 或 multiSelector 时,range 有效 * @property {String} rangeKey 当 range 是一个 Object Array 时,通过 rangeKey 来指定 Object 中 key 的值作为选择器显示内容 * @property {Number} value value 的值表示选择了 range 中的第几个(下标从 0 开始) * @property {EventHandle} onChange value 改变时触发 change 事件,event.detail = {value: value} * @property {Boolean} disabled 是否禁用 * @property {EventHandle} onCancel 取消选择或点遮罩层收起 picker 时触发 */ import Nerv from 'nervjs' import PickerGroup from './picker-group' import classNames from 'classnames' import { TOP, LINE_HEIGHT } from './constant' import dateHandle from './date' // todos: // 1. 加入滚动惯性 // 2. shouldComponentUpdate // 3. 多指操控。。。 // 4. timePicker 样式问题:不在指定时间范围时,选项样式置灰。缩窄两列间宽度。 export default class Picker extends Nerv.Component { constructor (props) { super(props) let { value, range, mode } = this.props this.index = [] if (mode === 'multiSelector') { if (!range) { range = [] this.props.range = [] } range.forEach((r, i) => { const v = value && value.length ? value[i] : undefined this.index.push(this.verifyValue(v, r) ? Math.floor(value[i]) : 0) }) } else if (mode === 'time') { // check value... if (!this.verifyTime(value)) { console.warn('time picker value illegal') value = '0:0' } const time = value.split(':').map(n => +n) this.index = time } else if (mode === 'date') { const { start = '', end = '' } = this.props let _value = dateHandle.verifyDate(value) let _start = dateHandle.verifyDate(start) let _end = dateHandle.verifyDate(end) if (!_value) _value = new Date() // 没传值或值的合法性错误默认今天时间 if (!_start) _start = new Date('1970/01/01') if (!_end) _end = new Date('2999/01/01') // 时间区间有效性 if ( _value.getTime() >= _start.getTime() && _value.getTime() <= _end.getTime() ) { this.index = [ _value.getFullYear(), _value.getMonth() + 1, _value.getDate() ] let maxDay = dateHandle.getMaxDay( _value.getFullYear(), _value.getMonth() + 1 ) this.pickerDate = { _value, _start, _end, _updateValue: [ _value.getFullYear(), _value.getMonth() + 1, _value.getDate() ] } this.setState({ dateMaxDay: this.getDateRange(1, maxDay, '日') }) } else { throw new Error('Date Interval Error') } // this.index = dateHandle. } else { if (!range) { range = [] this.props.range = [] } this.index.push(this.verifyValue(value, range) ? Math.floor(value) : 0) } this.state = { pickerValue: this.index, hidden: true, fadeOut: false, height: [] } } shouldComponentUpdate (nextProps, nextState) { // console.log(nextProps, nextState) // todo... } // 校验传入的 value 是否合法 verifyValue (value, range) { if (!isNaN(+value) && value >= 0 && value < range.length) return true return false } // 检验传入的 time value 是否合法 verifyTime (value) { if (!/^\d{1,2}:\d{1,2}$/.test(value)) return false const time = value.split(':').map(num => +num) if (time[0] < 0 || time[0] > 23) return false if (time[1] < 0 || time[1] > 59) return false return true } // 比较时间 compareTime (t1, t2) { // logic: t1 <= t2: return true t1 = t1.split(':').map(i => +i) t2 = t2.split(':').map(i => +i) if (t1[0] < t2[0]) return true if (t1[0] === t2[0]) { if (t1[1] <= t2[1]) return true } return false } // 获取年月日下标或者下标对应的数 getDateArrIndex (value, fields, getIdx = false) { let year = this.getDateRange( this.pickerDate._start.getFullYear(), this.pickerDate._end.getFullYear() ) let month = this.getDateRange(1, 12) let day = this.getDateRange(1, 31) if (getIdx) { if (fields === 0) { return year[value] } else if (fields === 1) { return month[value] } else { return day[value] } } else { if (fields === 0) { return year.indexOf(value + '') } else if (fields === 1) { return month.indexOf(value + '') } else { return day.indexOf(value + '') } } } // 获取时间数组 getDateRange (begin, end, fields = '') { const range = [] for (let i = begin; i <= end; i++) { range.push(`${i}${fields}`) } return range } // 隐藏 picker hidePicker () { this.setState({ fadeOut: true }) setTimeout(() => this.setState({ hidden: true, fadeOut: false }), 350) } render () { // 展示 Picker const showPicker = () => { if (this.props.disabled) return const height = this.index.map((i, idx) => { let factor = 0 if (this.props.mode === 'time') { factor = LINE_HEIGHT * 4 } if (this.props.mode === 'date') { return TOP - LINE_HEIGHT * this.getDateArrIndex(i, idx) - factor } return TOP - LINE_HEIGHT * i - factor }) this.setState({ hidden: false, height }) } // 点击确定 const onChange = e => { this.hidePicker() // 除了 multiSeclector,都在点击确认时才改变记录的下标值 this.index = this.state.height.map(h => (TOP - h) / LINE_HEIGHT) const eventObj = getEventObj(e, 'change', { value: this.index.length > 1 ? this.index : this.index[0] }) if (this.props.mode === 'time') { const range = [ [ '20', '21', '22', '23', ...getTimeRange(0, 23), '00', '01', '02', '03' ], [ '56', '57', '58', '59', ...getTimeRange(0, 59), '00', '01', '02', '03' ] ] this.index = this.index.map((n, i) => range[i][n]) eventObj.detail.value = this.index.join(':') } if (this.props.mode === 'date') { this.index = this.index.map((n, i) => this.getDateArrIndex(n, i, true)) if (this.props.fields === 'year') { eventObj.detail.value = [this.index[0]] } else if (this.props.fields === 'month') { eventObj.detail.value = [this.index[0], this.index[1]] } else { eventObj.detail.value = this.index } } this.setState({ pickerValue: eventObj.detail.value }) this.props.onChange && this.props.onChange(eventObj) } // 点击取消或蒙层 const onCancel = e => { this.hidePicker() const eventObj = getEventObj(e, 'cancel', {}) this.props.onCancel && this.props.onCancel(eventObj) } // 列改变 const onColumnChange = (height, columnId, e) => { // 获取 touchend 时的 index const index = this.state.height.map((h, i) => { if (columnId === i) h = height return (TOP - h) / LINE_HEIGHT }) // 需要做个比对再确定是否触发 let valueChanged = false this.index.some((value, num) => { if (value !== index[num]) { valueChanged = true return true } }) if (!valueChanged) return // 前一级列改变时,后面所有级别的 index 都要设为 0, 并初始化高度 const rangeLen = this.props.range.length if (columnId < rangeLen - 1) { for (let i = columnId + 1; i < rangeLen; i++) index[i] = 0 } this.setState({ height: index.map(i => TOP - i * LINE_HEIGHT) }) // 改变 index 的值 this.index = index const eventObj = getEventObj(e, 'columnChange', { column: columnId, value: index[columnId] }) this.props.onColumnchange && this.props.onColumnchange(eventObj) } // 统一抛出的事件对象,和小程序对齐 const getEventObj = (e, type, detail) => { return { currentTarget: e.target, detail, target: e.target, timeStamp: Math.floor(e.timeStamp), type } } // 供 PickerGroup 修改对应的 height 值 const updateHeight = (height, columnId, needRevise = false) => { this.setState( prevState => { prevState.height[columnId] = height return { height: prevState.height } }, () => { // time picker 必须在规定时间范围内,因此需要在 touchEnd 做修正 if (needRevise) { let { start, end } = this.props if (!this.verifyTime(start)) start = '00:00' if (!this.verifyTime(end)) end = '23:59' if (!this.compareTime(start, end)) return let time = this.state.height.map(h => (TOP - h) / LINE_HEIGHT) const range = [ [ '20', '21', '22', '23', ...getTimeRange(0, 23), '00', '01', '02', '03' ], [ '56', '57', '58', '59', ...getTimeRange(0, 59), '00', '01', '02', '03' ] ] time = time.map((n, i) => range[i][n]).join(':') if (!this.compareTime(start, time)) { // 修正到 start const height = start .split(':') .map(i => TOP - LINE_HEIGHT * (+i + 4)) this.setState({ height }) } else if (!this.compareTime(time, end)) { // 修正到 end const height = end .split(':') .map(i => TOP - LINE_HEIGHT * (+i + 4)) this.setState({ height }) } } } ) } // 单列 const getSelector = () => { return ( <PickerGroup range={this.props.range} rangeKey={this.props['rangeKey']} height={this.state.height[0]} updateHeight={updateHeight} columnId='0' /> ) } // 多列 const getMultiSelector = () => { return this.props.range.map((range, index) => { return ( <PickerGroup range={range} rangeKey={this.props['rangeKey']} height={this.state.height[index]} updateHeight={updateHeight} onColumnChange={onColumnChange} columnId={index} /> ) }) } // 时间 const getTimeSelector = () => { const hourRange = [ '20', '21', '22', '23', ...getTimeRange(0, 23), '00', '01', '02', '03' ] const minRange = [ '56', '57', '58', '59', ...getTimeRange(0, 59), '00', '01', '02', '03' ] return [ <PickerGroup mode='time' range={hourRange} height={this.state.height[0]} updateHeight={updateHeight} columnId='0' />, <PickerGroup mode='time' range={minRange} height={this.state.height[1]} updateHeight={updateHeight} columnId='1' /> ] } const getTimeRange = (begin, end) => { const range = [] for (let i = begin; i <= end; i++) { range.push(`${i < 10 ? '0' : ''}${i}`) } return range } /** * @author zhongxin * @description 时间选择 * */ // ======================= Date start =====================// const updateDay = (value, fields) => { this.pickerDate._updateValue[fields] = value let max = dateHandle.getMaxDay( this.pickerDate._updateValue[0], this.pickerDate._updateValue[1] ) if (max < this.pickerDate._updateValue[2]) { this.state.height[2] = TOP - LINE_HEIGHT * max + 34 } this.setState({ dateMaxDay: this.getDateRange(1, max, '日') }) } const gitDateSelector = () => { let year = this.getDateRange( this.pickerDate._start.getFullYear(), this.pickerDate._end.getFullYear(), '年' ) let month = this.getDateRange(1, 12, '月') let renderView = [] if (this.props.fields === 'year') { renderView.push( <PickerGroup mode='date' range={year} height={this.state.height[0]} updateDay={updateDay} updateHeight={updateHeight} columnId='0' /> ) } else if (this.props.fields === 'month') { renderView.push( <PickerGroup mode='date' range={year} height={this.state.height[0]} updateDay={updateDay} updateHeight={updateHeight} columnId='0' />, <PickerGroup mode='date' range={month} height={this.state.height[1]} updateDay={updateDay} updateHeight={updateHeight} columnId='1' /> ) } else { renderView = [ <PickerGroup mode='date' range={year} height={this.state.height[0]} updateDay={updateDay} updateHeight={updateHeight} columnId='0' />, <PickerGroup mode='date' range={month} height={this.state.height[1]} updateDay={updateDay} updateHeight={updateHeight} columnId='1' />, <PickerGroup mode='date' range={this.state.dateMaxDay} updateDay={updateDay} height={this.state.height[2]} updateHeight={updateHeight} columnId='2' /> ] } return renderView } // ======================= Date end =====================// // 动画类名控制逻辑 const clsMask = classNames('weui-mask', 'weui-animate-fade-in', { 'weui-animate-fade-out': this.state.fadeOut }) const clsSlider = classNames('weui-picker', 'weui-animate-slide-up', { 'weui-animate-slide-down': this.state.fadeOut }) const shouldDivHidden = this.state.hidden ? 'display: none;' : '' // 给 children 绑定事件 const children = Nerv.Children.map(this.props.children, child => { return Nerv.cloneElement(child, { onClick: showPicker }) }) // picker__group let pickerGroup switch (this.props.mode) { case 'multiSelector': pickerGroup = getMultiSelector() break case 'time': pickerGroup = getTimeSelector() break case 'date': pickerGroup = gitDateSelector() break default: pickerGroup = getSelector() } const { name = '' } = this.props return ( <div className={this.props.className}> {children} <div style={shouldDivHidden} className={clsMask} onClick={onCancel} /> <div style={shouldDivHidden} className={clsSlider}> <div className='weui-picker__hd'> <div className='weui-picker__action' onClick={onCancel}> 取消 </div> <div className='weui-picker__action' onClick={onChange}> 确定 </div> </div> <div className='weui-picker__bd'>{pickerGroup}</div> <input type='hidden' name={name} value={this.state.pickerValue} /> </div> </div> ) } }