import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import moment from 'moment'; import ConfigProvider from '../config-provider'; import Overlay from '../overlay'; import Input from '../input'; import Calendar from '../calendar'; import RangeCalendar from '../calendar/range-calendar'; import TimePickerPanel from '../time-picker/panel'; import nextLocale from '../locale/zh-cn'; import { func, obj } from '../util'; import { PANEL, resetValueTime, formatDateValue, extend, getDateTimeFormat, isFunction, onDateKeydown, onTimeKeydown, } from './util'; import PanelFooter from './module/panel-footer'; const { Popup } = Overlay; function mapInputStateName(name) { return { startValue: 'startDateInputStr', endValue: 'endDateInputStr', startTime: 'startTimeInputStr', endTime: 'endTimeInputStr', }[name]; } function mapTimeToValue(name) { return { startTime: 'startValue', endTime: 'endValue', }[name]; } function getFormatValues(values, format) { if (!Array.isArray(values)) { return [null, null]; } return [ formatDateValue(values[0], format), formatDateValue(values[1], format), ]; } /** * DatePicker.RangePicker */ export default class RangePicker extends Component { static propTypes = { ...ConfigProvider.propTypes, prefix: PropTypes.string, rtl: PropTypes.bool, /** * 日期范围类型 */ type: PropTypes.oneOf(['date', 'month', 'year']), /** * 默认展示的起始月份 * @return {MomentObject} 返回包含指定月份的 moment 对象实例 */ defaultVisibleMonth: PropTypes.func, onVisibleMonthChange: PropTypes.func, /** * 日期范围值数组 [moment, moment] */ value: PropTypes.array, /** * 初始的日期范围值数组 [moment, moment] */ defaultValue: PropTypes.array, /** * 日期格式 */ format: PropTypes.string, /** * 是否使用时间控件,支持传入 TimePicker 的属性 */ showTime: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), /** * 每次选择是否重置时间(仅在 showTime 开启时有效) */ resetTime: PropTypes.bool, /** * 禁用日期函数 * @param {MomentObject} 日期值 * @param {String} view 当前视图类型,year: 年, month: 月, date: 日 * @return {Boolean} 是否禁用 */ disabledDate: PropTypes.func, /** * 自定义面板页脚 * @return {Node} 自定义的面板页脚组件 */ footerRender: PropTypes.func, /** * 日期范围值改变时的回调 [ MomentObject|String, MomentObject|String ] * @param {Array} value 日期值 */ onChange: PropTypes.func, /** * 点击确认按钮时的回调 返回开始时间和结束时间`[ MomentObject|String, MomentObject|String ]` * @return {Array} 日期范围 */ onOk: PropTypes.func, /** * 输入框内置标签 */ label: PropTypes.node, /** * 输入框状态 */ state: PropTypes.oneOf(['error', 'loading', 'success']), /** * 输入框尺寸 */ size: PropTypes.oneOf(['small', 'medium', 'large']), /** * 是否禁用 */ disabled: PropTypes.bool, /** * 是否显示清空按钮 */ hasClear: PropTypes.bool, /** * 弹层显示状态 */ visible: PropTypes.bool, /** * 弹层默认是否显示 */ defaultVisible: PropTypes.bool, /** * 弹层展示状态变化时的回调 * @param {Boolean} visible 弹层是否显示 * @param {String} type 触发弹层显示和隐藏的来源 okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发 */ onVisibleChange: PropTypes.func, /** * 弹层触发方式 */ popupTriggerType: PropTypes.oneOf(['click', 'hover']), /** * 弹层对齐方式, 具体含义见 OverLay文档 */ popupAlign: PropTypes.string, /** * 弹层容器 * @param {Element} target 目标元素 * @return {Element} 弹层的容器元素 */ popupContainer: PropTypes.any, /** * 弹层自定义样式 */ popupStyle: PropTypes.object, /** * 弹层自定义样式类 */ popupClassName: PropTypes.string, /** * 弹层其他属性 */ popupProps: PropTypes.object, /** * 是否跟随滚动 */ followTrigger: PropTypes.bool, /** * 输入框其他属性 */ inputProps: PropTypes.object, /** * 自定义日期单元格渲染 */ dateCellRender: PropTypes.func, /** * 自定义月份渲染函数 * @param {Object} calendarDate 对应 Calendar 返回的自定义日期对象 * @returns {ReactNode} */ monthCellRender: PropTypes.func, yearCellRender: PropTypes.func, // 兼容 0.x yearCellRender /** * 开始日期输入框的 aria-label 属性 */ startDateInputAriaLabel: PropTypes.string, /** * 开始时间输入框的 aria-label 属性 */ startTimeInputAriaLabel: PropTypes.string, /** * 结束日期输入框的 aria-label 属性 */ endDateInputAriaLabel: PropTypes.string, /** * 结束时间输入框的 aria-label 属性 */ endTimeInputAriaLabel: PropTypes.string, /** * 是否为预览态 */ isPreview: PropTypes.bool, /** * 预览态模式下渲染的内容 * @param {Array} value 日期区间 */ renderPreview: PropTypes.func, disableChangeMode: PropTypes.bool, yearRange: PropTypes.arrayOf(PropTypes.number), ranges: PropTypes.object, // 兼容0.x版本 locale: PropTypes.object, className: PropTypes.string, name: PropTypes.string, popupComponent: PropTypes.elementType, popupContent: PropTypes.node, placeholder: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.string), PropTypes.string, ]), }; static defaultProps = { prefix: 'next-', rtl: false, type: 'date', size: 'medium', showTime: false, resetTime: false, format: 'YYYY-MM-DD', disabledDate: () => false, footerRender: () => null, hasClear: true, defaultVisible: false, popupTriggerType: 'click', popupAlign: 'tl tl', locale: nextLocale.DatePicker, disableChangeMode: false, onChange: func.noop, onOk: func.noop, onVisibleChange: func.noop, }; constructor(props, context) { super(props, context); const dateTimeFormat = getDateTimeFormat( props.format, props.showTime, props.type, ); extend(dateTimeFormat, this); const val = props.value || props.defaultValue; const values = getFormatValues(val, this.dateTimeFormat); this.inputAsString = val && (typeof val[0] === 'string' || typeof val[1] === 'string'); this.state = { visible: props.visible || props.defaultVisible, startValue: values[0], endValue: values[1], startDateInputStr: '', endDateInputStr: '', activeDateInput: 'startValue', startTimeInputStr: '', endTimeInputStr: '', inputing: false, // 当前是否处于输入状态 panel: PANEL.DATE, }; } componentWillReceiveProps(nextProps) { if ('showTime' in nextProps) { const dateTimeFormat = getDateTimeFormat( nextProps.format || this.props.format, nextProps.showTime, nextProps.type, ); extend(dateTimeFormat, this); } if ('value' in nextProps) { const values = getFormatValues( nextProps.value, this.dateTimeFormat, ); this.setState({ startValue: values[0], endValue: values[1], }); this.inputAsString = nextProps.value && (typeof nextProps.value[0] === 'string' || typeof nextProps.value[1] === 'string'); } if ('visible' in nextProps) { this.setState({ visible: nextProps.visible, }); } } onValueChange = (values, handler = 'onChange') => { let ret; if (!values.length || !this.inputAsString) { ret = values; } else { ret = [ values[0] ? values[0].format(this.dateTimeFormat) : null, values[1] ? values[1].format(this.dateTimeFormat) : null, ]; } this.props[handler](ret); }; onSelectCalendarPanel = (value, active) => { const { showTime, resetTime } = this.props; const { activeDateInput: prevActiveDateInput, startValue: prevStartValue, endValue: prevEndValue, } = this.state; const newState = { activeDateInput: active || prevActiveDateInput, inputing: false, }; let newValue = value; switch (active || prevActiveDateInput) { case 'startValue': { if ( !prevEndValue || value.valueOf() <= prevEndValue.valueOf() ) { newState.activeDateInput = 'endValue'; } if (showTime) { if (!prevStartValue) { // 第一次选择,如果设置了时间默认值,则使用该默认时间 if (showTime.defaultValue) { const defaultTimeValue = formatDateValue( Array.isArray(showTime.defaultValue) ? showTime.defaultValue[0] : showTime.defaultValue, this.timeFormat, ); newValue = resetValueTime(value, defaultTimeValue); } } else if (!resetTime) { // 非第一次选择,如果开启了 resetTime ,则记住之前选择的时间值 newValue = resetValueTime(value, prevStartValue); } } newState.startValue = newValue; if (prevEndValue && value.valueOf() > prevEndValue.valueOf()) { newState.endValue = null; newState.activeDateInput = 'endValue'; } break; } case 'endValue': if (!prevStartValue) { newState.activeDateInput = 'startValue'; } if (showTime) { if (!prevEndValue) { // 第一次选择,如果设置了时间默认值,则使用该默认时间 if (showTime.defaultValue) { const defaultTimeValue = formatDateValue( Array.isArray(showTime.defaultValue) ? showTime.defaultValue[1] || showTime.defaultValue[0] : showTime.defaultValue, this.timeFormat, ); newValue = resetValueTime(value, defaultTimeValue); } } else if (!resetTime) { // 非第一次选择,如果开启了 resetTime ,则记住之前选择的时间值 newValue = resetValueTime(value, prevEndValue); } } newState.endValue = newValue; // 选择了一个比开始日期更小的结束日期,此时表示用户重新选择了 if ( prevStartValue && value.valueOf() < prevStartValue.valueOf() ) { newState.startValue = value; newState.endValue = null; } break; } const newStartValue = 'startValue' in newState ? newState.startValue : prevStartValue; const newEndValue = 'endValue' in newState ? newState.endValue : prevEndValue; // 受控状态选择不更新值 if ('value' in this.props) { delete newState.startValue; delete newState.endValue; } this.setState(newState); this.onValueChange([newStartValue, newEndValue]); }; clearRange = () => { this.setState({ startDateInputStr: '', endDateInputStr: '', startTimeInputStr: '', endTimeInputStr: '', }); if (!('value' in this.props)) { this.setState({ startValue: null, endValue: null, }); } this.onValueChange([]); }; onDateInputChange = (inputStr, e, eventType) => { if (eventType === 'clear' || !inputStr) { e.stopPropagation(); this.clearRange(); } else { const stateName = mapInputStateName(this.state.activeDateInput); this.setState({ [stateName]: inputStr, inputing: this.state.activeDateInput, }); } }; onDateInputBlur = () => { const stateName = mapInputStateName(this.state.activeDateInput); const dateInputStr = this.state[stateName]; if (dateInputStr) { const { format, disabledDate } = this.props; const parsed = moment(dateInputStr, format, true); this.setState({ [stateName]: '', inputing: false, }); if (parsed.isValid() && !disabledDate(parsed, 'date')) { const valueName = this.state.activeDateInput; const newValue = parsed; this.handleChange(valueName, newValue); } } }; onDateInputKeyDown = e => { const { type } = this.props; const { activeDateInput } = this.state; const stateName = mapInputStateName(activeDateInput); const dateInputStr = this.state[stateName]; const dateStr = onDateKeydown( e, { format: this.format, value: this.state[activeDateInput], dateInputStr, }, type === 'date' ? 'day' : type, ); if (!dateStr) return; return this.onDateInputChange(dateStr); }; onFocusDateInput = type => { if (type !== this.state.activeDateInput) { this.setState({ activeDateInput: type, }); } if (this.state.panel !== PANEL.DATE) { this.setState({ panel: PANEL.DATE, }); } }; onFocusTimeInput = type => { if (type !== this.state.activeDateInput) { this.setState({ activeDateInput: type, }); } if (this.state.panel !== PANEL.TIME) { this.setState({ panel: PANEL.TIME, }); } }; onSelectStartTime = value => { if (!('value' in this.props)) { this.setState({ startValue: value, inputing: false, activeDateInput: 'startTime', }); } if (value.valueOf() !== this.state.startValue.valueOf()) { this.onValueChange([value, this.state.endValue]); } }; onSelectEndTime = value => { if (!('value' in this.props)) { this.setState({ endValue: value, inputing: false, activeDateInput: 'endTime', }); } if (value.valueOf() !== this.state.endValue.valueOf()) { this.onValueChange([this.state.startValue, value]); } }; onTimeInputChange = inputStr => { const stateName = mapInputStateName(this.state.activeDateInput); this.setState({ [stateName]: inputStr, inputing: this.state.activeDateInput, }); }; onTimeInputBlur = () => { const stateName = mapInputStateName(this.state.activeDateInput); const timeInputStr = this.state[stateName]; if (timeInputStr) { const parsed = moment(timeInputStr, this.timeFormat, true); this.setState({ [stateName]: '', inputing: false, }); if (parsed.isValid()) { const hour = parsed.hour(); const minute = parsed.minute(); const second = parsed.second(); const valueName = mapTimeToValue(this.state.activeDateInput); const newValue = this.state[valueName] .clone() .hour(hour) .minute(minute) .second(second); this.handleChange(valueName, newValue); } } }; onTimeInputKeyDown = e => { const { showTime } = this.props; const { activeDateInput } = this.state; const stateName = mapInputStateName(activeDateInput); const timeInputStr = this.state[stateName]; const { disabledMinutes, disabledSeconds, hourStep = 1, minuteStep = 1, secondStep = 1, } = typeof showTime === 'object' ? showTime : {}; let unit = 'second'; if (disabledSeconds) { unit = disabledMinutes ? 'hour' : 'minute'; } const timeStr = onTimeKeydown( e, { format: this.timeFormat, timeInputStr, value: this.state[ activeDateInput.indexOf('start') ? 'startValue' : 'endValue' ], steps: { hour: hourStep, minute: minuteStep, second: secondStep, }, }, unit, ); if (!timeStr) return; this.onTimeInputChange(timeStr); }; handleChange = (valueName, newValue) => { if (!('value' in this.props)) { this.setState({ [valueName]: newValue, }); } const startValue = valueName === 'startValue' ? newValue : this.state.startValue; const endValue = valueName === 'endValue' ? newValue : this.state.endValue; this.onValueChange([startValue, endValue]); }; onVisibleChange = (visible, type) => { if (!('visible' in this.props)) { this.setState({ visible, }); } this.props.onVisibleChange(visible, type); }; changePanel = panel => { const { startValue, endValue } = this.state; this.setState({ panel, activeDateInput: panel === PANEL.DATE ? !!startValue && !endValue ? 'endValue' : 'startValue' : 'startTime', }); }; onOk = () => { this.onVisibleChange(false, 'okBtnClick'); this.onValueChange( [this.state.startValue, this.state.endValue], 'onOk', ); }; // 如果用户没有给定时间禁用逻辑,则给默认到禁用逻辑,即如果是同一天,则时间不能是同样的 getDisabledTime = ({ startValue, endValue }) => { const { disabledHours, disabledMinutes, disabledSeconds } = this.props.showTime || {}; let disabledTime = {}; if (startValue && endValue) { const isSameDay = startValue.format('L') === endValue.format('L'); const newDisabledHours = isFunction(disabledHours) ? disabledHours : index => { if (isSameDay && index < startValue.hour()) { return true; } }; const newDisabledMinutes = isFunction(disabledMinutes) ? disabledMinutes : index => { if ( isSameDay && startValue.hour() === endValue.hour() && index < startValue.minute() ) { return true; } }; const newDisabledSeconds = isFunction(disabledSeconds) ? disabledSeconds : index => { if ( isSameDay && startValue.hour() === endValue.hour() && startValue.minute() === endValue.minute() && index <= startValue.second() ) { return true; } }; disabledTime = { disabledHours: newDisabledHours, disabledMinutes: newDisabledMinutes, disabledSeconds: newDisabledSeconds, }; } return disabledTime; }; renderPreview([startValue, endValue], others) { const { prefix, format, className, renderPreview } = this.props; const previewCls = classnames(className, `${prefix}form-preview`); const startLabel = startValue ? startValue.format(format) : ''; const endLabel = endValue ? endValue.format(format) : ''; if (typeof renderPreview === 'function') { return (
{renderPreview([startValue, endValue], this.props)}
); } return (

{startLabel} - {endLabel}

); } render() { const { prefix, rtl, type, defaultVisibleMonth, onVisibleMonthChange, showTime, disabledDate, footerRender, label, ranges = {}, // 兼容0.x ranges 属性 state: inputState, size, disabled, hasClear, popupTriggerType, popupAlign, popupContainer, popupStyle, popupClassName, popupProps, popupComponent, popupContent, followTrigger, className, locale, inputProps, dateCellRender, monthCellRender, yearCellRender, startDateInputAriaLabel, startTimeInputAriaLabel, endDateInputAriaLabel, endTimeInputAriaLabel, isPreview, disableChangeMode, yearRange, placeholder, ...others } = this.props; const { state } = this; const classNames = classnames( { [`${prefix}range-picker`]: true, [`${prefix}${size}`]: size, [`${prefix}disabled`]: disabled, }, className, ); const panelBodyClassName = classnames({ [`${prefix}range-picker-body`]: true, [`${prefix}range-picker-body-show-time`]: showTime, }); const triggerCls = classnames({ [`${prefix}range-picker-trigger`]: true, [`${prefix}error`]: inputState === 'error', }); const startDateInputCls = classnames({ [`${prefix}range-picker-panel-input-start-date`]: true, [`${prefix}focus`]: state.activeDateInput === 'startValue', }); const endDateInputCls = classnames({ [`${prefix}range-picker-panel-input-end-date`]: true, [`${prefix}focus`]: state.activeDateInput === 'endValue', }); if (rtl) { others.dir = 'rtl'; } if (isPreview) { return this.renderPreview( [state.startValue, state.endValue], obj.pickOthers(others, RangePicker.PropTypes), ); } const startDateInputValue = state.inputing === 'startValue' ? state.startDateInputStr : (state.startValue && state.startValue.format(this.format)) || ''; const endDateInputValue = state.inputing === 'endValue' ? state.endDateInputStr : (state.endValue && state.endValue.format(this.format)) || ''; let startTriggerValue = startDateInputValue; let endTriggerValue = endDateInputValue; const sharedInputProps = { ...inputProps, size, disabled, onChange: this.onDateInputChange, onBlur: this.onDateInputBlur, onPressEnter: this.onDateInputBlur, onKeyDown: this.onDateInputKeyDown, }; const startDateInput = ( this.onFocusDateInput('startValue')} className={startDateInputCls} /> ); const endDateInput = ( this.onFocusDateInput('endValue')} className={endDateInputCls} /> ); const shareCalendarProps = { showOtherMonth: true, dateCellRender, monthCellRender, yearCellRender, format: this.format, defaultVisibleMonth, onVisibleMonthChange, }; const datePanel = type === 'date' ? ( ) : (
{ return ( state.endValue && date.isAfter(state.endValue, type) ); }} onSelect={value => { const selectedValue = value .clone() .date(1) .hour(0) .minute(0) .second(0); if (type === 'year') { selectedValue.month(0); } this.onSelectCalendarPanel( selectedValue, 'startValue', ); }} value={state.startValue} /> { return ( state.startValue && date.isBefore(state.startValue, type) ); }} onSelect={value => { const selectedValue = value .clone() .hour(23) .minute(59) .second(59); if (type === 'year') { selectedValue.month(11).date(31); } else { selectedValue.date(selectedValue.daysInMonth()); } this.onSelectCalendarPanel( selectedValue, 'endValue', ); }} value={state.endValue} />
); let startTimeInput = null; let endTimeInput = null; let timePanel = null; let panelFooter = footerRender(); if (showTime) { const startTimeInputValue = state.inputing === 'startTime' ? state.startTimeInputStr : (state.startValue && state.startValue.format(this.timeFormat)) || ''; const endTimeInputValue = state.inputing === 'endTime' ? state.endTimeInputStr : (state.endValue && state.endValue.format(this.timeFormat)) || ''; startTriggerValue = (state.startValue && state.startValue.format(this.dateTimeFormat)) || ''; endTriggerValue = (state.endValue && state.endValue.format(this.dateTimeFormat)) || ''; const sharedTimeInputProps = { size, placeholder: this.timeFormat, onFocus: this.onFocusTimeInput, onBlur: this.onTimeInputBlur, onPressEnter: this.onTimeInputBlur, onChange: this.onTimeInputChange, onKeyDown: this.onTimeInputKeyDown, }; const startTimeInputCls = classnames({ [`${prefix}range-picker-panel-input-start-time`]: true, [`${prefix}focus`]: state.activeDateInput === 'startTime', }); startTimeInput = ( this.onFocusTimeInput('startTime')} className={startTimeInputCls} /> ); const endTimeInputCls = classnames({ [`${prefix}range-picker-panel-input-end-time`]: true, [`${prefix}focus`]: state.activeDateInput === 'endTime', }); endTimeInput = ( this.onFocusTimeInput('endTime')} className={endTimeInputCls} /> ); const showSecond = this.timeFormat.indexOf('s') > -1; const showMinute = this.timeFormat.indexOf('m') > -1; const sharedTimePickerProps = { ...showTime, prefix, locale, disabled, showSecond, showMinute, }; const disabledTime = this.getDisabledTime(state); timePanel = (
); } panelFooter = panelFooter || ( ({ label: key, value: ranges[key], onChange: values => { this.setState({ startValue: values[0], endValue: values[1], }); this.onValueChange(values); }, }))} disabledOk={ !state.startValue || !state.endValue || state.startValue.valueOf() > state.endValue.valueOf() } locale={locale} panel={state.panel} onPanelChange={showTime ? this.changePanel : null} onOk={this.onOk} /> ); const panelBody = { [PANEL.DATE]: datePanel, [PANEL.TIME]: timePanel, }[state.panel]; const allowClear = state.startValue && state.endValue && hasClear; let [startPlaceholder, endPlaceholder] = placeholder || []; if (typeof placeholder === 'string') { startPlaceholder = placeholder; endPlaceholder = placeholder; } const trigger = (
this.onFocusDateInput('startValue')} /> - this.onFocusDateInput('endValue')} hasClear={allowClear} hint="calendar" />
); const PopupComponent = popupComponent || Popup; return (
{popupContent || (
{startDateInput} {startTimeInput} - {endDateInput} {endTimeInput}
{panelBody} {panelFooter}
)}
); } }