zent
Version:
一套前端设计语言和基于React的实现
487 lines (425 loc) • 12.3 kB
JavaScript
import React, { Component, PureComponent } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Popover from 'popover';
import formatDate from 'zan-utils/date/formatDate';
import parseDate from 'zan-utils/date/parseDate';
import DatePanel from './date/DatePanel';
import PanelFooter from './common/PanelFooter';
import { goMonths, isArray, isSameMonth } from './utils';
import { dayStart, setTime } from './utils/date';
import {
timeFnMap,
noop,
popPositionMap,
commonProps,
commonPropTypes
} from './constants/';
let retType = 'string';
function isValidValue(val) {
if (!isArray(val)) return false;
const ret = val.filter(item => !!item);
return ret.length === 2;
}
function getDateTime(date, time) {
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
time.getHours(),
time.getMinutes(),
time.getSeconds()
);
}
const extractStateFromProps = props => {
const { format, defaultValue, defaultTime } = props;
let showPlaceholder;
let selected = [];
let actived = [];
let range = [];
let value = [];
if (isValidValue(props.value)) {
showPlaceholder = false;
selected = [
parseDate(props.value[0], format),
parseDate(props.value[1], format)
];
const tmp = [setTime(selected[0]), setTime(selected[1])];
range = tmp.slice();
actived = tmp.slice();
value = [formatDate(selected[0], format), formatDate(selected[1], format)];
// 特殊处理:如果两个时间在同一个月,右边的面板月份加一
if (isSameMonth(actived[0], actived[1])) {
actived[1] = goMonths(actived[1], 1);
}
} else {
showPlaceholder = true;
value = [];
if (defaultValue && isValidValue(defaultValue)) {
actived = [
parseDate(defaultValue[0], format),
parseDate(defaultValue[1], format)
];
} else {
const start = dayStart();
actived = [start, goMonths(start, 1)];
}
}
if (defaultTime && isValidValue(defaultTime)) {
actived = actived.map((item, idx) => setTime(item, defaultTime[idx]));
range = range.map((item, idx) => setTime(item, defaultTime[idx]));
}
let activedTime;
if (selected.length === 2) {
activedTime = selected.slice();
} else {
activedTime = actived.slice();
}
return {
value,
range,
selected,
actived,
activedTime,
openPanel: false,
showError: false,
openStartTimePanel: false,
openEndTimePanel: false,
showPlaceholder
};
};
class CombineDateRangePicker extends (PureComponent || Component) {
static PropTypes = {
...commonPropTypes,
showTime: PropTypes.bool,
placeholder: PropTypes.array,
defaultTime: PropTypes.arrayOf(PropTypes.string)
};
static defaultProps = {
...commonProps,
placeholder: ['开始日期', '结束日期'],
errorText: '请选择起止时间',
defaultTime: ['00:00:00', '00:00:00']
};
constructor(props) {
super(props);
const { value, valueType } = props;
if (valueType) {
retType = valueType.toLowerCase();
} else if (isValidValue(value)) {
if (typeof value[0] === 'number') retType = 'number';
if (value[0] instanceof Date) retType = 'date';
}
this.state = extractStateFromProps(props);
}
componentWillReceiveProps(next) {
const state = extractStateFromProps(next);
this.setState(state);
}
getDate = () => {
return this.state.actived;
};
onHover = val => {
const { selected, range } = this.state;
const scp = selected.slice();
const rcp = range.slice();
if (scp.length !== 1) {
rcp.splice(0, 2);
return;
}
if (rcp[0] && rcp[0] < val) {
rcp.splice(1, 1, val);
this.setState({
range: rcp
});
}
};
onSelectDate = val => {
const { selected, actived, range } = this.state;
const { onClick } = this.props;
const scp = selected.slice();
const acp = actived.slice();
const rcp = range.slice();
let type;
/**
* 选择日期时,可能如下出现四种情况
* 1. 还没有选择过,这次选择的日期作为开始日期
* 2. 选择过一次,并且第二次选择日期大于第一次,这次选择的日期作为结束日期
* 3. 有效选择过两次,清空之前的选择,重新设置这次选择的日期作为开始日期
* 4. 选择过一次,并且这次选择的日期小于第一次,替换这次选择的日期为开始日期
*/
if (scp.length === 2) {
scp.splice(0, 2, val);
rcp.splice(0, 2, val);
acp.splice(0, 2, val, goMonths(val, 1));
type = 'start';
// 支持选择同一天
} else if (
scp[0] &&
(scp[0] < val || formatDate(scp[0]) === formatDate(val))
) {
scp.splice(1, 1, val);
if (scp[0].getMonth() < val.getMonth()) {
acp.splice(1, 1, val);
}
type = 'end';
} else {
acp.splice(0, 2, val, goMonths(val, 1));
scp.splice(0, 1, val);
rcp.splice(0, 1, val);
type = 'start';
}
this.setState({
selected: scp,
actived: acp,
range: rcp
});
onClick && onClick(val, type);
};
isDisabled = val => {
const { disabledDate, format, min, max } = this.props;
if (disabledDate && disabledDate(val)) return true;
if (min && val < parseDate(min, format)) return true;
if (max && val > parseDate(max, format)) return true;
return false;
};
onChangeDate = (val, i) => {
const { actived } = this.state;
const acp = actived.slice();
acp.splice(i, 1, val);
this.setState({
actived: acp
});
};
onChangeStart = val => {
this.onChangeDate(val, 0);
};
onChangeEnd = val => {
this.onChangeDate(val, 1);
};
onChangeTime = (val, i, type) => {
const { activedTime } = this.state;
const tcp = activedTime.slice();
const fn = timeFnMap[type];
tcp[i][fn](val);
this.setState({
activedTime: tcp
});
};
onChangeStartTime = (val, type) => {
this.onChangeTime(val, 0, type);
};
onChangeEndTime = (val, type) => {
this.onChangeTime(val, 1, type);
};
// next&prev month 翻页效果联动
onChangeMonth = type => {
const baseMap = {
prev: 0,
next: 1
};
const typeMap = {
prev: -1,
next: 1
};
return () => {
const { actived } = this.state;
const base = actived[baseMap[type]];
let acp = [base, base];
acp.splice(baseMap[type], 1, goMonths(base, typeMap[type]));
this.setState({
actived: acp
});
};
};
onOpenStartTime = () => {
this.setState({
openStartTimePanel: true,
openEndTimePanel: false
});
};
onOpenEndTime = () => {
this.setState({
openStartTimePanel: false,
openEndTimePanel: true
});
};
onClearInput = evt => {
evt.stopPropagation();
this.props.onChange([]);
};
/**
* 如果传入为数字,返回值也为数字
* 如果传入为 Date 的实例,返回值也为 Date 的实例
* 默认返回 format 格式的字符串
*/
getReturnValue(date, format) {
if (retType === 'number') {
return date.getTime();
}
if (retType === 'date') {
return date;
}
return formatDate(date, format);
}
onConfirm = () => {
const { selected, activedTime } = this.state;
if (selected.length !== 2) {
this.setState({
showError: true
});
// eslint-disable-next-line
let timer = setTimeout(() => {
this.setState({
showError: false
});
timer = null;
}, 2000);
return;
}
const { format, showTime } = this.props;
let tmp = selected.slice();
if (showTime) {
tmp = [
getDateTime(tmp[0], activedTime[0]),
getDateTime(tmp[1], activedTime[1])
];
}
const vcp = [formatDate(tmp[0], format), formatDate(tmp[1], format)];
this.setState({
value: vcp,
showPlaceholder: false,
openPanel: false
});
const ret = [
this.getReturnValue(tmp[0], format),
this.getReturnValue(tmp[1], format)
];
this.props.onChange(ret);
};
renderPicker() {
const state = this.state;
const props = this.props;
let rangePicker;
const getTimeConfig = type => {
if (!props.showTime) return false;
const handleMap = {
start: this.onChangeStartTime,
end: this.onChangeEndTime
};
const indexMap = {
start: 0,
end: 1
};
const timeStatusMap = {
start: 'openEndTimePanel',
end: 'openStartTimePanel'
};
const timeHandleMap = {
start: this.onOpenStartTime,
end: this.onOpenEndTime
};
return {
hidePanel: state[timeStatusMap[type]],
actived: state.activedTime[indexMap[type]],
disabledTime: props.disabledTime && props.disabledTime(type),
onChange: handleMap[type],
onOpen: timeHandleMap[type]
};
};
if (state.openPanel) {
const pickerCls = classNames({
'range-picker': true,
'range-picker--showTime': props.showTime
});
rangePicker = (
<div className={pickerCls} ref={ref => (this.picker = ref)}>
<div className="date-picker">
<DatePanel
range={state.range}
showTime={getTimeConfig('start')}
actived={state.actived[0]}
selected={state.selected}
disabledDate={this.isDisabled}
onSelect={this.onSelectDate}
onChange={this.onChangeStart}
onHover={this.onHover}
onPrev={this.onChangeMonth('prev')}
onNext={noop}
showPrev
showNext={false}
/>
</div>
<div className="date-picker">
<DatePanel
range={state.range}
showTime={getTimeConfig('end')}
actived={state.actived[1]}
selected={state.selected}
disabledDate={this.isDisabled}
onSelect={this.onSelectDate}
onChange={this.onChangeEnd}
onHover={this.onHover}
onPrev={noop}
onNext={this.onChangeMonth('next')}
showPrev={false}
showNext
/>
</div>
<PanelFooter
buttonText={props.confirmText}
onClickButton={this.onConfirm}
showError={state.showError}
errorText={props.errorText}
/>
</div>
);
}
return rangePicker;
}
togglePicker = visible => {
const { onOpen, onClose, disabled } = this.props;
if (disabled) return;
visible ? onOpen && onOpen() : onClose && onClose();
this.setState({
openPanel: visible
});
};
render() {
const state = this.state;
const props = this.props;
const prefixCls = `${props.prefix}-datetime-picker ${props.className}`;
const inputCls = classNames({
'picker-input--range picker-input picker-input--combine': true,
'picker-input--filled': !state.showPlaceholder,
'picker-input--showTime': props.showTime,
'picker-input--disabled': props.disabled
});
return (
<div className={prefixCls}>
<Popover
cushion={5}
visible={state.openPanel}
onVisibleChange={this.togglePicker}
className={`${props.prefix}-datetime-picker-popover ${props.className}-popover`}
position={popPositionMap[props.popPosition.toLowerCase()]}
>
<Popover.Trigger.Click>
<div className={inputCls} onClick={evt => evt.preventDefault()}>
{state.showPlaceholder
? props.placeholder.join(' 至 ')
: state.value.join(' 至 ')}
<span className="zenticon zenticon-calendar-o" />
<span
onClick={this.onClearInput}
className="zenticon zenticon-close-circle"
/>
</div>
</Popover.Trigger.Click>
<Popover.Content>{this.renderPicker()}</Popover.Content>
</Popover>
</div>
);
}
}
export default CombineDateRangePicker;