123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- /* eslint-disable jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */
- /* eslint-disable max-len */
- import React from 'react';
- import classNames from 'classnames';
- import PropTypes from 'prop-types';
- import MonthFoundation, { MonthAdapter, MonthDayInfo, MonthFoundationProps, MonthFoundationState } from '@douyinfe/semi-foundation/datePicker/monthFoundation';
- import { cssClasses, numbers } from '@douyinfe/semi-foundation/datePicker/constants';
- import BaseComponent, { BaseProps } from '../_base/baseComponent';
- import { isBefore, isAfter, isBetween, isSameDay } from '@douyinfe/semi-foundation/datePicker/_utils/index';
- import { noop, stubFalse, isFunction } from 'lodash';
- import { parseISO } from 'date-fns';
- import { Locale } from '../locale/interface';
- const prefixCls = cssClasses.PREFIX;
- export interface MonthProps extends MonthFoundationProps, BaseProps {
- forwardRef: React.Ref<any>;
- locale: Locale['DatePicker'];
- focusRecordsRef: React.RefObject<{ rangeStart: boolean; rangeEnd: boolean }>;
- }
- export type MonthState = MonthFoundationState;
- export default class Month extends BaseComponent<MonthProps, MonthState> {
- static propTypes = {
- month: PropTypes.object,
- selected: PropTypes.object,
- rangeStart: PropTypes.string,
- rangeEnd: PropTypes.string,
- offsetRangeStart: PropTypes.string,
- offsetRangeEnd: PropTypes.string,
- onDayClick: PropTypes.func,
- onDayHover: PropTypes.func,
- weekStartsOn: PropTypes.number,
- disabledDate: PropTypes.func,
- weeksRowNum: PropTypes.number,
- onWeeksRowNumChange: PropTypes.func,
- renderDate: PropTypes.func,
- renderFullDate: PropTypes.func,
- hoverDay: PropTypes.string, // Real-time hover date
- startDateOffset: PropTypes.func,
- endDateOffset: PropTypes.func,
- rangeInputFocus: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
- focusRecordsRef: PropTypes.object,
- multiple: PropTypes.bool,
- };
- static defaultProps = {
- month: new Date(),
- selected: new Set(),
- rangeStart: '',
- rangeEnd: '',
- onDayClick: noop,
- onDayHover: noop,
- onWeeksRowNumChange: noop,
- weekStartsOn: numbers.WEEK_START_ON,
- disabledDate: stubFalse,
- weeksRowNum: 0,
- };
- monthRef: React.RefObject<HTMLDivElement>;
- foundation: MonthFoundation;
- constructor(props: MonthProps) {
- super(props);
- this.state = {
- weekdays: [],
- month: { weeks: [], monthText: '' },
- todayText: '',
- weeksRowNum: props.weeksRowNum,
- };
- this.monthRef = React.createRef();
- }
- get adapter(): MonthAdapter {
- return {
- ...super.adapter,
- updateToday: todayText => this.setState({ todayText }),
- setWeekDays: weekdays => this.setState({ weekdays }),
- setWeeksRowNum: (weeksRowNum, callback) => this.setState({ weeksRowNum }, callback),
- updateMonthTable: month => this.setState({ month }),
- notifyDayClick: day => this.props.onDayClick(day),
- notifyDayHover: day => this.props.onDayHover(day),
- notifyWeeksRowNumChange: weeksRowNum => this.props.onWeeksRowNumChange(weeksRowNum),
- };
- }
- componentDidMount() {
- this.foundation = new MonthFoundation(this.adapter);
- this.foundation.init();
- }
- componentWillUnmount() {
- this.foundation.destroy();
- }
- componentDidUpdate(prevProps: MonthProps, prevState: MonthState) {
- if (prevProps.month !== this.props.month) {
- this.foundation.getMonthTable();
- }
- }
- getSingleDayStatus(options: Partial<MonthProps> & { fullDate: string; todayText: string }) {
- const { fullDate, todayText, selected, disabledDate, rangeStart, rangeEnd } = options;
- const disabledOptions = { rangeStart, rangeEnd };
- const isToday = fullDate === todayText;
- const isSelected = selected.has(fullDate);
- let isDisabled = disabledDate && disabledDate(parseISO(fullDate), disabledOptions);
- if (
- !isDisabled &&
- this.props.rangeInputFocus === 'rangeStart' &&
- rangeEnd &&
- this.props.focusRecordsRef &&
- this.props.focusRecordsRef.current.rangeEnd
- ) {
- // The reason for splitting is that the dateRangeTime format: 'yyyy-MM-dd HH:MM:SS'
- isDisabled = isAfter(fullDate, rangeEnd.trim().split(/\s+/)[0]);
- }
- if (
- !isDisabled &&
- this.props.rangeInputFocus === 'rangeEnd' &&
- rangeStart &&
- this.props.focusRecordsRef &&
- this.props.focusRecordsRef.current.rangeStart
- ) {
- // The reason for splitting is that the dateRangeTime format: 'yyyy-MM-dd HH:MM:SS'
- isDisabled = isBefore(fullDate, rangeStart.trim().split(/\s+/)[0]);
- }
- return {
- isToday, // Today
- isSelected, // Selected
- isDisabled // Disabled
- };
- }
- getDateRangeStatus(options: Partial<MonthProps> & { fullDate: string }) {
- const { rangeStart, rangeEnd, fullDate, hoverDay, offsetRangeStart, offsetRangeEnd, rangeInputFocus } = options;
- // If no item is selected, return the empty object directly
- const _isDateRangeAnySelected = Boolean(rangeStart || rangeEnd);
- const _isDateRangeSelected = Boolean(rangeStart && rangeEnd);
- const _isOffsetDateRangeAnyExist = offsetRangeStart || offsetRangeEnd;
- if (!_isDateRangeAnySelected) {
- return ({});
- }
- // The range selects the hover date, and the normal hover is .semi-datepicker-main: hover
- const _isHoverDay = isSameDay(hoverDay, fullDate);
- // When one is selected
- // eslint-disable-next-line one-var
- let _isHoverAfterStart, _isHoverBeforeEnd, isSelectedStart, isSelectedEnd, isHoverDayAroundOneSelected;
- if (rangeStart) {
- isSelectedStart = isSameDay(fullDate, rangeStart);
- if (rangeInputFocus === 'rangeEnd') {
- _isHoverAfterStart = isBetween(fullDate, { start: rangeStart, end: hoverDay });
- }
- }
- if (rangeEnd) {
- isSelectedEnd = isSameDay(fullDate, rangeEnd);
- if (rangeInputFocus === 'rangeStart') {
- _isHoverBeforeEnd = isBetween(fullDate, { start: hoverDay, end: rangeEnd });
- }
- }
- if (!_isDateRangeSelected && _isDateRangeAnySelected) {
- isHoverDayAroundOneSelected = _isHoverDay;
- }
- // eslint-disable-next-line one-var
- let isHover;
- if (!_isOffsetDateRangeAnyExist) {
- isHover = _isHoverAfterStart || _isHoverBeforeEnd || _isHoverDay;
- }
- // Select all
- // eslint-disable-next-line one-var
- let isInRange, isSelectedStartAfterHover, isSelectedEndBeforeHover, isHoverDayInStartSelection, isHoverDayInEndSelection, isHoverDayInRange;
- if (_isDateRangeSelected) {
- isInRange = isBetween(fullDate, { start: rangeStart, end: rangeEnd });
- if (!_isOffsetDateRangeAnyExist) {
- isSelectedStartAfterHover = isSelectedStart && isAfter(rangeStart, hoverDay);
- isSelectedEndBeforeHover = isSelectedEnd && isBefore(rangeEnd, hoverDay);
- isHoverDayInStartSelection = _isHoverDay && rangeInputFocus === 'rangeStart';
- isHoverDayInEndSelection = _isHoverDay && rangeInputFocus === 'rangeEnd';
- isHoverDayInRange = _isHoverDay && isBetween(hoverDay, { start: rangeStart, end: rangeEnd });
- }
- }
- return {
- isHoverDay: _isHoverDay, // Is the current hover date
- isSelectedStart, // Select Start
- isSelectedEnd, // End of selection
- isInRange, // Range within the selected date
- isHover, // Date between selection and hover date
- isSelectedStartAfterHover, // Choose to start behind the hover
- isSelectedEndBeforeHover, // Choose to end in front of the hover
- isHoverDayInRange, // Hover date within range
- isHoverDayInStartSelection, // Hover date when starting Date is selected
- isHoverDayInEndSelection, // Hover date when endDate is selected
- isHoverDayAroundOneSelected, // Hover date and select a date
- };
- }
- getOffsetDateStatus(options: Partial<MonthProps> & { fullDate: string }) {
- const { offsetRangeStart, offsetRangeEnd, rangeStart, rangeEnd, fullDate, hoverDay } = options;
- // When there is no offset, return the empty object directly
- const _isOffsetDateRangeNull = !(offsetRangeStart || offsetRangeEnd);
- if (_isOffsetDateRangeNull) {
- return ({});
- }
- // Range Select base date
- const _isInRange = isBetween(fullDate, { start: rangeStart, end: rangeEnd });
- const _isHoverDay = isSameDay(hoverDay, fullDate);
- const _isSelectedStart = rangeStart && isSameDay(fullDate, rangeStart);
- const _isSelectedEnd = rangeEnd && isSameDay(fullDate, rangeEnd);
- const _isDateRangeSelected = Boolean(rangeStart && rangeEnd);
- // Determine whether it is offsetStart or offsetRangeEnd
- const isOffsetRangeStart = isSameDay(fullDate, offsetRangeStart);
- const isOffsetRangeEnd = isSameDay(fullDate, offsetRangeEnd);
- const isHoverDayOffset = _isHoverDay;
- // When selected
- let isHoverInOffsetRange, isInOffsetRange;
- if (_isDateRangeSelected) {
- isHoverInOffsetRange = _isInRange && _isHoverDay;
- }
- // When there is an offset area
- const _isOffsetDateRangeSelected = Boolean(offsetRangeStart && offsetRangeEnd);
- if (_isOffsetDateRangeSelected) {
- isInOffsetRange = (_isSelectedStart || isBetween(fullDate, { start: offsetRangeStart, end: offsetRangeEnd }) || _isSelectedEnd);
- }
- return {
- isOffsetRangeStart, // Week selection start
- isOffsetRangeEnd, // End of week selection
- isHoverInOffsetRange, // Hover in the week selection
- isHoverDayOffset, // Week selection hover day
- isInOffsetRange // Include start and end within the week selection (start and end styles are the same as other dates, so start and end are included)
- };
- }
- /**
- * get day current status
- * @param {Object} fullDate
- * @param {Object} options
- * @returns {Object}
- */
- getDayStatus(currentDay: MonthDayInfo, options: MonthProps & { todayText: string }) {
- const { fullDate } = currentDay;
- const { hoverDay, rangeStart, rangeEnd, todayText, offsetRangeStart, offsetRangeEnd, disabledDate, selected, rangeInputFocus } = options;
- const singleDayStatus = this.getSingleDayStatus({ fullDate, todayText, hoverDay, selected, disabledDate, rangeStart, rangeEnd });
- const dateRangeStatus = this.getDateRangeStatus({ fullDate, rangeStart, rangeEnd, hoverDay, offsetRangeStart, offsetRangeEnd, rangeInputFocus, ...singleDayStatus });
- const offsetDataStatus = this.getOffsetDateStatus({ offsetRangeStart, offsetRangeEnd, rangeStart, rangeEnd, fullDate, hoverDay, ...singleDayStatus, ...dateRangeStatus });
- // this parameter will pass to the user when given renderFullDate function, do not delete or modify its key
- const dayStatus = {
- ...singleDayStatus,
- ...dateRangeStatus,
- ...offsetDataStatus,
- };
- return dayStatus;
- }
- renderDayOfWeek() {
- const { locale } = this.props;
- const weekdayCls = classNames(cssClasses.WEEKDAY);
- const weekdayItemCls = classNames(`${prefixCls}-weekday-item`);
- const { weekdays } = this.state;
- // i18n
- const weekdaysText = weekdays.map(key => locale.weeks[key]);
- return (
- <div role="row" className={weekdayCls}>
- {weekdaysText.map((E, i) => (
- <div role="columnheader" key={E + i} className={weekdayItemCls}>
- {E}
- </div>
- ))}
- </div>
- );
- }
- renderWeeks() {
- const { month } = this.state;
- const { weeks } = month;
- const { weeksRowNum } = this.props;
- let style = {};
- if (weeksRowNum) {
- const height = weeksRowNum * numbers.WEEK_HEIGHT;
- style = { height };
- }
- const weeksCls = classNames(cssClasses.WEEKS);
- return (
- <div className={weeksCls} style={style}>
- {weeks.map((week, weekIndex) => this.renderWeek(week, weekIndex))}
- </div>
- );
- }
- renderWeek(week: MonthDayInfo[], weekIndex: number) {
- const weekCls = cssClasses.WEEK;
- return (
- <div role="row" className={weekCls} key={weekIndex}>
- {week.map((day, dayIndex) => this.renderDay(day, dayIndex))}
- </div>
- );
- }
- renderDay(day: MonthDayInfo, dayIndex: number) {
- const { todayText } = this.state;
- const { renderFullDate, renderDate } = this.props;
- const { fullDate, dayNumber } = day;
- if (!fullDate) {
- return (
- <div role="gridcell" tabIndex={-1} key={(dayNumber as number) + dayIndex} className={cssClasses.DAY}>
- <span />
- </div>
- );
- }
- const dayStatus = this.getDayStatus(day, { todayText, ...this.props });
- const dayCls = classNames(cssClasses.DAY, {
- [cssClasses.DAY_TODAY]: dayStatus.isToday,
- [cssClasses.DAY_IN_RANGE]: dayStatus.isInRange,
- [cssClasses.DAY_HOVER]: dayStatus.isHover,
- [cssClasses.DAY_SELECTED]: dayStatus.isSelected,
- [cssClasses.DAY_SELECTED_START]: dayStatus.isSelectedStart,
- [cssClasses.DAY_SELECTED_END]: dayStatus.isSelectedEnd,
- [cssClasses.DAY_DISABLED]: dayStatus.isDisabled,
- // offsetDate class
- [cssClasses.DAY_HOVER_DAY]: dayStatus.isHoverDayOffset,
- [cssClasses.DAY_IN_OFFSET_RANGE]: dayStatus.isInOffsetRange,
- [cssClasses.DAY_SELECTED_RANGE_HOVER]: dayStatus.isHoverInOffsetRange,
- [cssClasses.DAY_OFFSET_RANGE_START]: dayStatus.isOffsetRangeStart,
- [cssClasses.DAY_OFFSET_RANGE_END]: dayStatus.isOffsetRangeEnd,
- // range input class
- [cssClasses.DAY_SELECTED_START_AFTER_HOVER]: dayStatus.isSelectedStartAfterHover,
- [cssClasses.DAY_SELECTED_END_BEFORE_HOVER]: dayStatus.isSelectedEndBeforeHover,
- [cssClasses.DAY_HOVER_DAY_BEFORE_RANGE]: dayStatus.isHoverDayInStartSelection,
- [cssClasses.DAY_HOVER_DAY_AFTER_RANGE]: dayStatus.isHoverDayInEndSelection,
- [cssClasses.DAY_HOVER_DAY_AROUND_SINGLE_SELECTED]: dayStatus.isHoverDayAroundOneSelected,
- });
- const dayMainCls = classNames({
- [`${cssClasses.DAY}-main`]: true,
- });
- const fullDateArgs = [dayNumber, fullDate, dayStatus];
- const customRender = isFunction(renderFullDate);
- return (
- <div
- role="gridcell"
- tabIndex={dayStatus.isDisabled ? -1 : 0}
- aria-disabled={dayStatus.isDisabled}
- aria-selected={dayStatus.isSelected}
- aria-label={fullDate}
- className={!customRender ? dayCls : cssClasses.DAY}
- title={fullDate}
- key={(dayNumber as number) + dayIndex}
- onClick={e => !dayStatus.isDisabled && this.foundation.handleClick(day)}
- onMouseEnter={() => this.foundation.handleHover(day)}
- onMouseLeave={() => this.foundation.handleHover()}
- >
- {customRender ? renderFullDate(...fullDateArgs) : (
- <div className={dayMainCls}>
- {isFunction(renderDate) ? renderDate(dayNumber, fullDate) : <span>{dayNumber}</span>}
- </div>
- )}
- </div>
- );
- }
- render() {
- const { forwardRef, multiple } = this.props;
- const weekday = this.renderDayOfWeek();
- const weeks = this.renderWeeks();
- const monthCls = classNames(cssClasses.MONTH);
- const ref = forwardRef || this.monthRef;
- return (
- <div role="grid" aria-multiselectable={multiple} ref={ref} className={monthCls} >
- {weekday}
- {weeks}
- </div>
- );
- }
- }
|