import PropTypes from '../../_util/vue-types'; import BaseMixin from '../../_util/BaseMixin'; import { getOptionProps, hasProp, mergeProps, getComponentFromProp, getListeners, } from '../../_util/props-util'; import moment from 'moment'; import KeyCode from '../../_util/KeyCode'; import CalendarPart from './range-calendar/CalendarPart'; import TodayButton from './calendar/TodayButton'; import OkButton from './calendar/OkButton'; import TimePickerButton from './calendar/TimePickerButton'; import CommonMixin from './mixin/CommonMixin'; import enUs from './locale/en_US'; import { syncTime, getTodayTime, isAllowedDate } from './util/'; import { goTime, goStartMonth, goEndMonth, includesTime } from './util/toTime'; function noop() {} function isEmptyArray(arr) { return Array.isArray(arr) && (arr.length === 0 || arr.every(i => !i)); } function isArraysEqual(a, b) { if (a === b) return true; if (a === null || typeof a === 'undefined' || b === null || typeof b === 'undefined') { return false; } if (a.length !== b.length) return false; for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) return false; } return true; } function getValueFromSelectedValue(selectedValue) { let [start, end] = selectedValue; if (end && (start === undefined || start === null)) { start = end.clone().subtract(1, 'month'); } if (start && (end === undefined || end === null)) { end = start.clone().add(1, 'month'); } return [start, end]; } function normalizeAnchor(props, init) { const selectedValue = props.selectedValue || (init && props.defaultSelectedValue); const value = props.value || (init && props.defaultValue); const normalizedValue = value ? getValueFromSelectedValue(value) : getValueFromSelectedValue(selectedValue); return !isEmptyArray(normalizedValue) ? normalizedValue : init && [moment(), moment().add(1, 'months')]; } function generateOptions(length, extraOptionGen) { const arr = extraOptionGen ? extraOptionGen().concat() : []; for (let value = 0; value < length; value++) { if (arr.indexOf(value) === -1) { arr.push(value); } } return arr; } function onInputSelect(direction, value, cause) { if (!value) { return; } const originalValue = this.sSelectedValue; const selectedValue = originalValue.concat(); const index = direction === 'left' ? 0 : 1; selectedValue[index] = value; if (selectedValue[0] && this.compare(selectedValue[0], selectedValue[1]) > 0) { selectedValue[1 - index] = this.sShowTimePicker ? selectedValue[index] : undefined; } this.__emit('inputSelect', selectedValue); this.fireSelectValueChange(selectedValue, null, cause || { source: 'dateInput' }); } const RangeCalendar = { props: { locale: PropTypes.object.def(enUs), visible: PropTypes.bool.def(true), prefixCls: PropTypes.string.def('rc-calendar'), dateInputPlaceholder: PropTypes.any, seperator: PropTypes.string.def('~'), defaultValue: PropTypes.any, value: PropTypes.any, hoverValue: PropTypes.any, mode: PropTypes.arrayOf(PropTypes.oneOf(['time', 'date', 'month', 'year', 'decade'])), showDateInput: PropTypes.bool.def(true), timePicker: PropTypes.any, showOk: PropTypes.bool, showToday: PropTypes.bool.def(true), defaultSelectedValue: PropTypes.array.def([]), selectedValue: PropTypes.array, showClear: PropTypes.bool, showWeekNumber: PropTypes.bool, // locale: PropTypes.object, // onChange: PropTypes.func, // onSelect: PropTypes.func, // onValueChange: PropTypes.func, // onHoverChange: PropTypes.func, // onPanelChange: PropTypes.func, format: PropTypes.oneOfType([ PropTypes.string, PropTypes.arrayOf(PropTypes.string), PropTypes.func, ]), // onClear: PropTypes.func, type: PropTypes.any.def('both'), disabledDate: PropTypes.func, disabledTime: PropTypes.func.def(noop), renderFooter: PropTypes.func.def(() => null), renderSidebar: PropTypes.func.def(() => null), dateRender: PropTypes.func, clearIcon: PropTypes.any, inputReadOnly: PropTypes.bool, }, mixins: [BaseMixin, CommonMixin], data() { const props = this.$props; const selectedValue = props.selectedValue || props.defaultSelectedValue; const value = normalizeAnchor(props, 1); return { sSelectedValue: selectedValue, prevSelectedValue: selectedValue, firstSelectedValue: null, sHoverValue: props.hoverValue || [], sValue: value, sShowTimePicker: false, sMode: props.mode || ['date', 'date'], sPanelTriggerSource: '', // Trigger by which picker panel: 'start' & 'end' }; }, watch: { value() { const newState = {}; newState.sValue = normalizeAnchor(this.$props, 0); this.setState(newState); }, hoverValue(val) { if (!isArraysEqual(this.sHoverValue, val)) { this.setState({ sHoverValue: val }); } }, selectedValue(val) { const newState = {}; newState.sSelectedValue = val; newState.prevSelectedValue = val; this.setState(newState); }, mode(val) { if (!isArraysEqual(this.sMode, val)) { this.setState({ sMode: val }); } }, }, methods: { onDatePanelEnter() { if (this.hasSelectedValue()) { this.fireHoverValueChange(this.sSelectedValue.concat()); } }, onDatePanelLeave() { if (this.hasSelectedValue()) { this.fireHoverValueChange([]); } }, onSelect(value) { const { type, sSelectedValue, prevSelectedValue, firstSelectedValue } = this; let nextSelectedValue; if (type === 'both') { if (!firstSelectedValue) { syncTime(prevSelectedValue[0], value); nextSelectedValue = [value]; } else if (this.compare(firstSelectedValue, value) < 0) { syncTime(prevSelectedValue[1], value); nextSelectedValue = [firstSelectedValue, value]; } else { syncTime(prevSelectedValue[0], value); syncTime(prevSelectedValue[1], firstSelectedValue); nextSelectedValue = [value, firstSelectedValue]; } } else if (type === 'start') { syncTime(prevSelectedValue[0], value); const endValue = sSelectedValue[1]; nextSelectedValue = endValue && this.compare(endValue, value) > 0 ? [value, endValue] : [value]; } else { // type === 'end' const startValue = sSelectedValue[0]; if (startValue && this.compare(startValue, value) <= 0) { syncTime(prevSelectedValue[1], value); nextSelectedValue = [startValue, value]; } else { syncTime(prevSelectedValue[0], value); nextSelectedValue = [value]; } } this.fireSelectValueChange(nextSelectedValue); }, onKeyDown(event) { if (event.target.nodeName.toLowerCase() === 'input') { return; } const { keyCode } = event; const ctrlKey = event.ctrlKey || event.metaKey; const { sSelectedValue: selectedValue, sHoverValue: hoverValue, firstSelectedValue, sValue: value, // Value is used for `CalendarPart` current page } = this.$data; const { disabledDate } = this.$props; // Update last time of the picker const updateHoverPoint = func => { // Change hover to make focus in UI let currentHoverTime; let nextHoverTime; let nextHoverValue; if (!firstSelectedValue) { currentHoverTime = hoverValue[0] || selectedValue[0] || value[0] || moment(); nextHoverTime = func(currentHoverTime); nextHoverValue = [nextHoverTime]; this.fireHoverValueChange(nextHoverValue); } else { if (hoverValue.length === 1) { currentHoverTime = hoverValue[0].clone(); nextHoverTime = func(currentHoverTime); nextHoverValue = this.onDayHover(nextHoverTime); } else { currentHoverTime = hoverValue[0].isSame(firstSelectedValue, 'day') ? hoverValue[1] : hoverValue[0]; nextHoverTime = func(currentHoverTime); nextHoverValue = this.onDayHover(nextHoverTime); } } // Find origin hover time on value index if (nextHoverValue.length >= 2) { const miss = nextHoverValue.some(ht => !includesTime(value, ht, 'month')); if (miss) { const newValue = nextHoverValue.slice().sort((t1, t2) => t1.valueOf() - t2.valueOf()); if (newValue[0].isSame(newValue[1], 'month')) { newValue[1] = newValue[0].clone().add(1, 'month'); } this.fireValueChange(newValue); } } else if (nextHoverValue.length === 1) { // If only one value, let's keep the origin panel let oriValueIndex = value.findIndex(time => time.isSame(currentHoverTime, 'month')); if (oriValueIndex === -1) oriValueIndex = 0; if (value.every(time => !time.isSame(nextHoverTime, 'month'))) { const newValue = value.slice(); newValue[oriValueIndex] = nextHoverTime.clone(); this.fireValueChange(newValue); } } event.preventDefault(); return nextHoverTime; }; switch (keyCode) { case KeyCode.DOWN: updateHoverPoint(time => goTime(time, 1, 'weeks')); return; case KeyCode.UP: updateHoverPoint(time => goTime(time, -1, 'weeks')); return; case KeyCode.LEFT: if (ctrlKey) { updateHoverPoint(time => goTime(time, -1, 'years')); } else { updateHoverPoint(time => goTime(time, -1, 'days')); } return; case KeyCode.RIGHT: if (ctrlKey) { updateHoverPoint(time => goTime(time, 1, 'years')); } else { updateHoverPoint(time => goTime(time, 1, 'days')); } return; case KeyCode.HOME: updateHoverPoint(time => goStartMonth(time)); return; case KeyCode.END: updateHoverPoint(time => goEndMonth(time)); return; case KeyCode.PAGE_DOWN: updateHoverPoint(time => goTime(time, 1, 'month')); return; case KeyCode.PAGE_UP: updateHoverPoint(time => goTime(time, -1, 'month')); return; case KeyCode.ENTER: { let lastValue; if (hoverValue.length === 0) { lastValue = updateHoverPoint(time => time); } else if (hoverValue.length === 1) { lastValue = hoverValue[0]; } else { lastValue = hoverValue[0].isSame(firstSelectedValue, 'day') ? hoverValue[1] : hoverValue[0]; } if (lastValue && (!disabledDate || !disabledDate(lastValue))) { this.onSelect(lastValue); } event.preventDefault(); return; } default: this.__emit('keydown', event); } }, onDayHover(value) { let hoverValue = []; const { sSelectedValue, firstSelectedValue, type } = this; if (type === 'start' && sSelectedValue[1]) { hoverValue = this.compare(value, sSelectedValue[1]) < 0 ? [value, sSelectedValue[1]] : [value]; } else if (type === 'end' && sSelectedValue[0]) { hoverValue = this.compare(value, sSelectedValue[0]) > 0 ? [sSelectedValue[0], value] : []; } else { if (!firstSelectedValue) { if (this.sHoverValue.length) { this.setState({ sHoverValue: [] }); } return hoverValue; } hoverValue = this.compare(value, firstSelectedValue) < 0 ? [value, firstSelectedValue] : [firstSelectedValue, value]; } this.fireHoverValueChange(hoverValue); return hoverValue; }, onToday() { const startValue = getTodayTime(this.sValue[0]); const endValue = startValue.clone().add(1, 'months'); this.setState({ sValue: [startValue, endValue] }); }, onOpenTimePicker() { this.setState({ sShowTimePicker: true, }); }, onCloseTimePicker() { this.setState({ sShowTimePicker: false, }); }, onOk() { const { sSelectedValue } = this; if (this.isAllowedDateAndTime(sSelectedValue)) { this.__emit('ok', sSelectedValue); } }, onStartInputChange(...oargs) { const args = ['left'].concat(oargs); return onInputSelect.apply(this, args); }, onEndInputChange(...oargs) { const args = ['right'].concat(oargs); return onInputSelect.apply(this, args); }, onStartInputSelect(value) { const args = ['left', value, { source: 'dateInputSelect' }]; return onInputSelect.apply(this, args); }, onEndInputSelect(value) { const args = ['right', value, { source: 'dateInputSelect' }]; return onInputSelect.apply(this, args); }, onStartValueChange(leftValue) { const value = [...this.sValue]; value[0] = leftValue; return this.fireValueChange(value); }, onEndValueChange(rightValue) { const value = [...this.sValue]; value[1] = rightValue; return this.fireValueChange(value); }, onStartPanelChange(value, mode) { const { sMode, sValue } = this; const newMode = [mode, sMode[1]]; const newValue = [value || sValue[0], sValue[1]]; this.__emit('panelChange', newValue, newMode); const newState = { sPanelTriggerSource: 'start', }; if (!hasProp(this, 'mode')) { newState.sMode = newMode; } this.setState(newState); }, onEndPanelChange(value, mode) { const { sMode, sValue } = this; const newMode = [sMode[0], mode]; const newValue = [sValue[0], value || sValue[1]]; this.__emit('panelChange', newValue, newMode); const newState = { sPanelTriggerSource: 'end', }; if (!hasProp(this, 'mode')) { newState.sMode = newMode; } this.setState(newState); }, getStartValue() { const { sSelectedValue: selectedValue, sShowTimePicker: showTimePicker, sValue: value, sMode: mode, sPanelTriggerSource: panelTriggerSource, } = this.$data; let startValue = value[0]; // keep selectedTime when select date if (selectedValue[0] && this.$props.timePicker) { startValue = startValue.clone(); syncTime(selectedValue[0], startValue); } if (showTimePicker && selectedValue[0]) { startValue = selectedValue[0]; } // Adjust month if date not align if ( panelTriggerSource === 'end' && mode[0] === 'date' && mode[1] === 'date' && startValue.isSame(value[1], 'month') ) { startValue = startValue.clone().subtract(1, 'month'); } return startValue; }, getEndValue() { const { sSelectedValue: selectedValue, sShowTimePicker: showTimePicker, sValue: value, sMode: mode, sPanelTriggerSource: panelTriggerSource, } = this.$data; let endValue = value[1] ? value[1].clone() : value[0].clone().add(1, 'month'); // keep selectedTime when select date if (selectedValue[1] && this.$props.timePicker) { syncTime(selectedValue[1], endValue); } if (showTimePicker) { endValue = selectedValue[1] ? selectedValue[1] : this.getStartValue(); } // Adjust month if date not align if ( !showTimePicker && panelTriggerSource !== 'end' && mode[0] === 'date' && mode[1] === 'date' && endValue.isSame(value[0], 'month') ) { endValue = endValue.clone().add(1, 'month'); } return endValue; }, // get disabled hours for second picker getEndDisableTime() { const { sSelectedValue, sValue, disabledTime } = this; const userSettingDisabledTime = disabledTime(sSelectedValue, 'end') || {}; const startValue = (sSelectedValue && sSelectedValue[0]) || sValue[0].clone(); // if startTime and endTime is same day.. // the second time picker will not able to pick time before first time picker if (!sSelectedValue[1] || startValue.isSame(sSelectedValue[1], 'day')) { const hours = startValue.hour(); const minutes = startValue.minute(); const second = startValue.second(); let { disabledHours, disabledMinutes, disabledSeconds } = userSettingDisabledTime; const oldDisabledMinutes = disabledMinutes ? disabledMinutes() : []; const olddisabledSeconds = disabledSeconds ? disabledSeconds() : []; disabledHours = generateOptions(hours, disabledHours); disabledMinutes = generateOptions(minutes, disabledMinutes); disabledSeconds = generateOptions(second, disabledSeconds); return { disabledHours() { return disabledHours; }, disabledMinutes(hour) { if (hour === hours) { return disabledMinutes; } return oldDisabledMinutes; }, disabledSeconds(hour, minute) { if (hour === hours && minute === minutes) { return disabledSeconds; } return olddisabledSeconds; }, }; } return userSettingDisabledTime; }, isAllowedDateAndTime(selectedValue) { return ( isAllowedDate(selectedValue[0], this.disabledDate, this.disabledStartTime) && isAllowedDate(selectedValue[1], this.disabledDate, this.disabledEndTime) ); }, isMonthYearPanelShow(mode) { return ['month', 'year', 'decade'].indexOf(mode) > -1; }, hasSelectedValue() { const { sSelectedValue } = this; return !!sSelectedValue[1] && !!sSelectedValue[0]; }, compare(v1, v2) { if (this.timePicker) { return v1.diff(v2); } return v1.diff(v2, 'days'); }, fireSelectValueChange(selectedValue, direct, cause) { const { timePicker, prevSelectedValue } = this; if (timePicker) { const timePickerProps = getOptionProps(timePicker); if (timePickerProps.defaultValue) { const timePickerDefaultValue = timePickerProps.defaultValue; if (!prevSelectedValue[0] && selectedValue[0]) { syncTime(timePickerDefaultValue[0], selectedValue[0]); } if (!prevSelectedValue[1] && selectedValue[1]) { syncTime(timePickerDefaultValue[1], selectedValue[1]); } } } // 尚未选择过时间,直接输入的话 if (!this.sSelectedValue[0] || !this.sSelectedValue[1]) { const startValue = selectedValue[0] || moment(); const endValue = selectedValue[1] || startValue.clone().add(1, 'months'); this.setState({ sSelectedValue: selectedValue, sValue: selectedValue && selectedValue.length === 2 ? getValueFromSelectedValue([startValue, endValue]) : this.sValue, }); } if (selectedValue[0] && !selectedValue[1]) { this.setState({ firstSelectedValue: selectedValue[0] }); this.fireHoverValueChange(selectedValue.concat()); } this.__emit('change', selectedValue); if (direct || (selectedValue[0] && selectedValue[1])) { this.setState({ prevSelectedValue: selectedValue, firstSelectedValue: null, }); this.fireHoverValueChange([]); this.__emit('select', selectedValue, cause); } if (!hasProp(this, 'selectedValue')) { this.setState({ sSelectedValue: selectedValue, }); } }, fireValueChange(value) { if (!hasProp(this, 'value')) { this.setState({ sValue: value, }); } this.__emit('valueChange', value); }, fireHoverValueChange(hoverValue) { if (!hasProp(this, 'hoverValue')) { this.setState({ sHoverValue: hoverValue }); } this.__emit('hoverChange', hoverValue); }, clear() { this.fireSelectValueChange([], true); this.__emit('clear'); }, disabledStartTime(time) { return this.disabledTime(time, 'start'); }, disabledEndTime(time) { return this.disabledTime(time, 'end'); }, disabledStartMonth(month) { const { sValue } = this; return month.isAfter(sValue[1], 'month'); }, disabledEndMonth(month) { const { sValue } = this; return month.isBefore(sValue[0], 'month'); }, }, render() { const props = getOptionProps(this); const { prefixCls, dateInputPlaceholder, timePicker, showOk, locale, showClear, showToday, type, seperator, } = props; const clearIcon = getComponentFromProp(this, 'clearIcon'); const { sHoverValue, sSelectedValue, sMode: mode, sShowTimePicker, sValue } = this; const className = { [prefixCls]: 1, [`${prefixCls}-hidden`]: !props.visible, [`${prefixCls}-range`]: 1, [`${prefixCls}-show-time-picker`]: sShowTimePicker, [`${prefixCls}-week-number`]: props.showWeekNumber, }; const baseProps = { props, on: getListeners(this), }; const newProps = { props: { selectedValue: sSelectedValue, }, on: { select: this.onSelect, dayHover: (type === 'start' && sSelectedValue[1]) || (type === 'end' && sSelectedValue[0]) || !!sHoverValue.length ? this.onDayHover : noop, }, }; let placeholder1; let placeholder2; if (dateInputPlaceholder) { if (Array.isArray(dateInputPlaceholder)) { [placeholder1, placeholder2] = dateInputPlaceholder; } else { placeholder1 = placeholder2 = dateInputPlaceholder; } } const showOkButton = showOk === true || (showOk !== false && !!timePicker); const cls = { [`${prefixCls}-footer`]: true, [`${prefixCls}-range-bottom`]: true, [`${prefixCls}-footer-show-ok`]: showOkButton, }; const startValue = this.getStartValue(); const endValue = this.getEndValue(); const todayTime = getTodayTime(startValue); const thisMonth = todayTime.month(); const thisYear = todayTime.year(); const isTodayInView = (startValue.year() === thisYear && startValue.month() === thisMonth) || (endValue.year() === thisYear && endValue.month() === thisMonth); const nextMonthOfStart = startValue.clone().add(1, 'months'); const isClosestMonths = nextMonthOfStart.year() === endValue.year() && nextMonthOfStart.month() === endValue.month(); const leftPartProps = mergeProps(baseProps, newProps, { props: { hoverValue: sHoverValue, direction: 'left', disabledTime: this.disabledStartTime, disabledMonth: this.disabledStartMonth, format: this.getFormat(), value: startValue, mode: mode[0], placeholder: placeholder1, showDateInput: this.showDateInput, timePicker, showTimePicker: sShowTimePicker || mode[0] === 'time', enablePrev: true, enableNext: !isClosestMonths || this.isMonthYearPanelShow(mode[1]), clearIcon, }, on: { inputChange: this.onStartInputChange, inputSelect: this.onStartInputSelect, valueChange: this.onStartValueChange, panelChange: this.onStartPanelChange, }, }); const rightPartProps = mergeProps(baseProps, newProps, { props: { hoverValue: sHoverValue, direction: 'right', format: this.getFormat(), timePickerDisabledTime: this.getEndDisableTime(), placeholder: placeholder2, value: endValue, mode: mode[1], showDateInput: this.showDateInput, timePicker, showTimePicker: sShowTimePicker || mode[1] === 'time', disabledTime: this.disabledEndTime, disabledMonth: this.disabledEndMonth, enablePrev: !isClosestMonths || this.isMonthYearPanelShow(mode[0]), enableNext: true, clearIcon, }, on: { inputChange: this.onEndInputChange, inputSelect: this.onEndInputSelect, valueChange: this.onEndValueChange, panelChange: this.onEndPanelChange, }, }); let TodayButtonNode = null; if (showToday) { const todayButtonProps = mergeProps(baseProps, { props: { disabled: isTodayInView, value: sValue[0], text: locale.backToToday, }, on: { today: this.onToday, }, }); TodayButtonNode = ; } let TimePickerButtonNode = null; if (props.timePicker) { const timePickerButtonProps = mergeProps(baseProps, { props: { showTimePicker: sShowTimePicker || (mode[0] === 'time' && mode[1] === 'time'), timePickerDisabled: !this.hasSelectedValue() || sHoverValue.length, }, on: { openTimePicker: this.onOpenTimePicker, closeTimePicker: this.onCloseTimePicker, }, }); TimePickerButtonNode = ; } let OkButtonNode = null; if (showOkButton) { const okButtonProps = mergeProps(baseProps, { props: { okDisabled: !this.isAllowedDateAndTime(sSelectedValue) || !this.hasSelectedValue() || sHoverValue.length, }, on: { ok: this.onOk, }, }); OkButtonNode = ; } const extraFooter = this.renderFooter(mode); return (
{props.renderSidebar()}
{showClear && sSelectedValue[0] && sSelectedValue[1] ? ( {clearIcon || } ) : null}
{seperator}
{showToday || props.timePicker || showOkButton || extraFooter ? ( ) : null}
); }, }; export default RangeCalendar;