react-mobile-datepicker
Version:
329 lines (286 loc) • 10.7 kB
JavaScript
/**
* @module Date组件
*/
import React, { Component } from 'react';
import * as TimeUtil from './time.js';
import { shallowEqual } from './pureRender.js';
import { addPrefixCss, formatCss } from './prefix.js';
const DATE_HEIGHT = 40; // 每个日期的高度
const DATE_LENGTH = 10; // 日期的个数
const MIDDLE_INDEX = Math.floor(DATE_LENGTH / 2); // 日期数组中间值的索引
const MIDDLE_Y = - DATE_HEIGHT * MIDDLE_INDEX; // translateY值
const isUndefined = val => typeof val === 'undefined';
const isFunction = val => Object.prototype.toString.apply(val) === '[object Function]';
type Props = {
value: Object,
min: Object,
max: Object,
format: string | Array<*>,
step: number,
onSelect: Function,
}
type State = {
translateY: number,
marginTop: number,
}
/**
* Class Date组件类
* @extends Component
*/
class DatePickerItem extends Component<void, Props, State> {
constructor(props) {
super(props);
this.animating = false; // 判断是否在transition过渡动画之中
this.touchY = 0; // 保存touchstart的pageY
this.translateY = 0; // 容器偏移的距离
this.currentIndex = MIDDLE_INDEX; // 滑动中当前日期的索引
this.moveDateCount = 0; // 一次滑动移动了多少个时间
this.state = {
translateY: MIDDLE_Y,
marginTop: (this.currentIndex - MIDDLE_INDEX) * DATE_HEIGHT,
};
this.renderDatepickerItem = this.renderDatepickerItem.bind(this);
this.handleContentTouch = this.handleContentTouch.bind(this);
this.handleContentMouseDown = this.handleContentMouseDown.bind(this);
this.handleContentMouseMove = this.handleContentMouseMove.bind(this);
this.handleContentMouseUp = this.handleContentMouseUp.bind(this);
}
componentWillMount() {
this._iniDates(this.props.value);
}
componentDidMount() {
const viewport = this.viewport;
viewport.addEventListener('touchstart', this.handleContentTouch, false);
viewport.addEventListener('touchmove', this.handleContentTouch, false);
viewport.addEventListener('touchend', this.handleContentTouch, false);
viewport.addEventListener('mousedown', this.handleContentMouseDown, false);
}
componentWillReceiveProps(nextProps) {
if (nextProps.value.getTime() === this.props.value.getTime()) {
return;
}
this._iniDates(nextProps.value);
this.currentIndex = MIDDLE_INDEX;
this.setState({
translateY: MIDDLE_Y,
marginTop: (this.currentIndex - MIDDLE_INDEX) * DATE_HEIGHT,
});
}
/**
* Optimization component, Prevents unnecessary rendering
* Only value or state change should re-rendering
*
* @param {Object} nextProps next props
* @param {Object} nextState next state
* @return {Boolean} Whether re-rendering
*/
shouldComponentUpdate(nextProps, nextState) {
return nextProps.value.getTime() !== this.props.value.getTime() ||
!shallowEqual(nextState, this.state);
}
componentWillUnmount() {
const viewport = this.viewport;
viewport.removeEventListener('touchstart', this.handleContentTouch, false);
viewport.removeEventListener('touchmove', this.handleContentTouch, false);
viewport.removeEventListener('touchend', this.handleContentTouch, false);
viewport.removeEventListener('mousedown', this.handleContentMouseDown, false);
}
_iniDates(date) {
const typeName = this.props.type;
const dates = Array(...Array(DATE_LENGTH))
.map((value, index) =>
TimeUtil[`next${typeName}`](date, (index - MIDDLE_INDEX) * this.props.step));
this.setState({ dates });
}
_updateDates(direction) {
const typeName = this.props.type;
const { dates } = this.state;
if (direction === 1) {
this.currentIndex ++;
this.setState({
dates: [
...dates.slice(1),
TimeUtil[`next${typeName}`](dates[dates.length - 1], this.props.step),
],
marginTop: (this.currentIndex - MIDDLE_INDEX) * DATE_HEIGHT,
});
} else {
this.currentIndex --;
this.setState({
dates: [
TimeUtil[`next${typeName}`](dates[0], -this.props.step),
...dates.slice(0, dates.length - 1),
],
marginTop: (this.currentIndex - MIDDLE_INDEX) * DATE_HEIGHT,
});
}
}
_checkIsUpdateDates(direction, translateY) {
return direction === 1 ?
this.currentIndex * DATE_HEIGHT + DATE_HEIGHT / 2 < -translateY :
this.currentIndex * DATE_HEIGHT - DATE_HEIGHT / 2 > -translateY;
}
/**
* 清除对象的transition样式
* @param {Dom} obj 指定的对象
* @return {undefined}
*/
_clearTransition(obj) {
addPrefixCss(obj, { transition: '' });
}
/**
* 滑动到下一日期
* @param {number} direction 滑动方向
* @return {undefined}
*/
_moveToNext(direction) {
const date = this.state.dates[MIDDLE_INDEX];
const { max, min } = this.props;
if (direction === -1 && date.getTime() < min.getTime() && this.moveDateCount) {
this._updateDates(1);
} else if (direction === 1 && date.getTime() > max.getTime() && this.moveDateCount) {
this._updateDates(-1);
}
this._moveTo(this.refs.scroll, this.currentIndex);
}
/**
* 添加滑动动画
* @param {DOM} obj DOM对象
* @param {number} angle 角度
* @return {undefined}
*/
_moveTo(obj, currentIndex) {
this.animating = true;
addPrefixCss(obj, { transition: 'transform .2s ease-out' });
this.setState({
translateY: -currentIndex * DATE_HEIGHT,
});
// NOTE: There is no transitionend, setTimeout is used instead.
setTimeout(() => {
this.animating = false;
this.props.onSelect(this.state.dates[MIDDLE_INDEX]);
this._clearTransition(this.refs.scroll);
}, 200);
}
handleStart(event) {
this.touchY =
(!isUndefined(event.targetTouches) &&
!isUndefined(event.targetTouches[0])) ?
event.targetTouches[0].pageY :
event.pageY;
this.translateY = this.state.translateY;
this.moveDateCount = 0;
}
handleMove(event) {
const touchY =
(!isUndefined(event.targetTouches) &&
!isUndefined(event.targetTouches[0])) ?
event.targetTouches[0].pageY :
event.pageY;
const dir = touchY - this.touchY;
const translateY = this.translateY + dir;
const direction = dir > 0 ? -1 : 1;
// 日期最小值,最大值限制
const date = this.state.dates[MIDDLE_INDEX];
const { max, min } = this.props;
if (date.getTime() < min.getTime() ||
date.getTime() > max.getTime()) {
return;
}
// 检测是否更新日期列表
if (this._checkIsUpdateDates(direction, translateY)) {
this.moveDateCount = direction > 0 ? this.moveDateCount + 1 : this.moveDateCount - 1;
this._updateDates(direction);
}
this.setState({ translateY });
}
handleEnd(event) {
const touchY = event.pageY || event.changedTouches[0].pageY;
const dir = touchY - this.touchY;
const direction = dir > 0 ? -1 : 1;
this._moveToNext(direction);
}
/**
* 滑动日期选择器触屏事件
* @param {Object} event 事件对象
* @return {undefined}
*/
handleContentTouch(event) {
event.preventDefault();
if (this.animating) return;
if (event.type === 'touchstart') {
this.handleStart(event);
} else if (event.type === 'touchmove') {
this.handleMove(event);
} else if (event.type === 'touchend') {
this.handleEnd(event);
}
}
/**
* 滑动日期选择器鼠标事件
* @param {Object} event 事件对象
* @return {undefined}
*/
handleContentMouseDown(event) {
if (this.animating) return;
this.handleStart(event);
document.addEventListener('mousemove', this.handleContentMouseMove);
document.addEventListener('mouseup', this.handleContentMouseUp);
}
handleContentMouseMove(event) {
if (this.animating) return;
this.handleMove(event);
}
handleContentMouseUp(event) {
if (this.animating) return;
this.handleEnd(event);
document.removeEventListener('mousemove', this.handleContentMouseMove);
document.removeEventListener('mouseup', this.handleContentMouseUp);
}
/**
* 渲染一个日期DOM对象
* @param {Object} date date数据
* @return {Object} JSX对象
*/
renderDatepickerItem(date, index) {
const className =
(date < this.props.min || date > this.props.max) ?
'disabled' : '';
let formatDate;
if (isFunction(this.props.format)) {
formatDate = this.props.format(date);
} else {
formatDate = TimeUtil.convertDate(date, this.props.format);
}
return (
<li
key={index}
className={className}>
{formatDate}
</li>
);
}
render() {
const scrollStyle = formatCss({
transform: `translateY(${this.state.translateY}px)`,
marginTop: this.state.marginTop,
});
return (
<div className="datepicker-col-1">
<div
ref={viewport => this.viewport = viewport} // eslint-disable-line
className="datepicker-viewport">
<div className="datepicker-wheel">
<ul
ref="scroll"
className="datepicker-scroll"
style={scrollStyle}>
{this.state.dates.map(this.renderDatepickerItem)}
</ul>
</div>
</div>
</div>
);
}
}
export default DatePickerItem;