/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */ import React, { ReactInstance } from 'react'; import ReactDOM from 'react-dom'; import cls from 'classnames'; import { isEqual } from 'lodash'; import PropTypes from 'prop-types'; import { IconClose } from '@douyinfe/semi-icons'; // eslint-disable-next-line max-len import CalendarFoundation, { CalendarAdapter, EventObject, MonthData, MonthlyEvent, ParsedEventsType, ParsedEventsWithArray, ParsedRangeEvent } from '@douyinfe/semi-foundation/calendar/foundation'; import { cssClasses } from '@douyinfe/semi-foundation/calendar/constants'; import { DateObj } from '@douyinfe/semi-foundation/calendar/eventUtil'; import LocaleConsumer from '../locale/localeConsumer'; import localeContext from '../locale/context'; import BaseComponent from '../_base/baseComponent'; import Popover from '../popover'; import Button from '../iconButton'; import { Locale } from '../locale/interface'; import { MonthCalendarProps } from './interface'; import '@douyinfe/semi-foundation/calendar/calendar.scss'; const toPercent = (num: number) => { const res = num < 1 ? num * 100 : 100; return `${res}%`; }; const prefixCls = `${cssClasses.PREFIX}-month`; const contentPadding = 60; const contentHeight = 24; export interface MonthCalendarState { itemLimit: number; showCard: Record; parsedEvents: MonthlyEvent; cachedKeys: Array } export default class monthCalendar extends BaseComponent { static propTypes = { displayValue: PropTypes.instanceOf(Date), header: PropTypes.node, events: PropTypes.array, mode: PropTypes.string, markWeekend: PropTypes.bool, width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), style: PropTypes.object, className: PropTypes.string, dateGridRender: PropTypes.func, onClick: PropTypes.func, onClose: PropTypes.func, }; static defaultProps = { displayValue: new Date(), events: [] as EventObject[], mode: 'month', }; static contextType = localeContext; cellDom: React.RefObject; foundation: CalendarFoundation; cardRef: Map; contentCellHeight: number; monthlyData: MonthData; clickOutsideHandler: (e: MouseEvent) => void; constructor(props: MonthCalendarProps) { super(props); this.state = { itemLimit: 0, showCard: {}, parsedEvents: {} as MonthlyEvent, cachedKeys: [] }; this.cellDom = React.createRef(); this.foundation = new CalendarFoundation(this.adapter); this.handleClick = this.handleClick.bind(this); this.cardRef = new Map(); } get adapter(): CalendarAdapter { return { ...super.adapter, registerClickOutsideHandler: (key: string, cb: () => void) => { const clickOutsideHandler = (e: MouseEvent) => { const cardInstance = this.cardRef && this.cardRef.get(key); // eslint-disable-next-line react/no-find-dom-node const cardDom = ReactDOM.findDOMNode(cardInstance); if (cardDom && !cardDom.contains(e.target as any)) { cb(); } }; this.clickOutsideHandler = clickOutsideHandler; document.addEventListener('mousedown', clickOutsideHandler, false); }, unregisterClickOutsideHandler: () => { document.removeEventListener('mousedown', this.clickOutsideHandler, false); }, setMonthlyData: data => { this.monthlyData = data; }, getMonthlyData: () => this.monthlyData, notifyClose: (e, key) => { const updates = {}; updates[key] = [false]; this.setState(prevState => ({ showCard: { ...prevState.showCard, ...updates } })); this.props.onClose && this.props.onClose(e); }, openCard: (key, spacing) => { const updates = {}; const pos = spacing ? 'leftTopOver' : 'rightTopOver'; updates[key] = [true, pos]; this.setState(prevState => ({ showCard: { ...updates } })); }, setParsedEvents: (parsedEvents: ParsedEventsType) => { this.setState({ parsedEvents: parsedEvents as MonthlyEvent }); }, setItemLimit: itemLimit => { this.setState({ itemLimit }); }, cacheEventKeys: cachedKeys => { this.setState({ cachedKeys }); } }; } calcItemLimit = () => { this.contentCellHeight = this.cellDom.current.getBoundingClientRect().height; return Math.max(0, Math.ceil((this.contentCellHeight - contentPadding) / contentHeight)); }; componentDidMount() { this.foundation.init(); const itemLimit = this.calcItemLimit(); this.foundation.parseMonthlyEvents(itemLimit); } componentWillUnmount() { this.foundation.destroy(); } componentDidUpdate(prevProps: MonthCalendarProps, prevState: MonthCalendarState) { const prevEventKeys = prevState.cachedKeys; const nowEventKeys = this.props.events.map(event => event.key); let itemLimitUpdate = false; let { itemLimit } = this.state; if (prevProps.height !== this.props.height) { itemLimit = this.calcItemLimit(); if (prevState.itemLimit !== itemLimit) { itemLimitUpdate = true; } } if (!isEqual(prevEventKeys, nowEventKeys) || itemLimitUpdate) { this.foundation.parseMonthlyEvents((itemLimit || this.props.events) as any); } } handleClick = (e: React.MouseEvent, val: [Date]) => { const { onClick } = this.props; const value = this.foundation.formatCbValue(val); onClick && onClick(e, value); }; closeCard(e: React.MouseEvent, key: string) { this.foundation.closeCard(e, key); } showCard = (e: React.MouseEvent, key: string) => { this.foundation.showCard(e, key); }; renderHeader = (dateFnsLocale: Locale['dateFnsLocale']) => { const { markWeekend, displayValue } = this.props; this.monthlyData = this.foundation.getMonthlyData(displayValue, dateFnsLocale); return (
    {this.monthlyData[0].map(day => { const { weekday } = day; const listCls = cls({ [`${cssClasses.PREFIX}-weekend`]: markWeekend && day.isWeekend, }); return (
  • {weekday}
  • ); })}
); }; renderEvents = (events: ParsedRangeEvent[]) => { if (!events) { return undefined; } const list = events.map((event, ind) => { const { leftPos, width, topInd, key, children } = event; const style = { left: toPercent(leftPos), width: toPercent(width), top: `${topInd}em` }; return (
  • {children}
  • ); }); return list; }; renderCollapsed = (events: MonthlyEvent['day'][number], itemInfo: DateObj, listCls: string, month: string) => { const { itemLimit, showCard } = this.state; const { weekday, dayString, date } = itemInfo; const key = date.toString(); const remained = events.filter(i => Boolean(i)).length - itemLimit; const cardCls = `${prefixCls}-event-card`; // const top = contentPadding / 2 + this.state.itemLimit * contentHeight; const shouldRenderCard = remained > 0; const closer = (