123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- /* 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<string, [boolean] | [boolean, string]>;
- parsedEvents: MonthlyEvent;
- cachedKeys: Array<string>
- }
- export default class monthCalendar extends BaseComponent<MonthCalendarProps, MonthCalendarState> {
- 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<HTMLDivElement>;
- foundation: CalendarFoundation;
- cardRef: Map<string, ReactInstance>;
- 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<MonthCalendarProps, MonthCalendarState> {
- 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 (
- <div className={`${prefixCls}-header`} role="presentation">
- <div role="presentation" className={`${prefixCls}-grid`}>
- <ul role="row" className={`${prefixCls}-grid-row`}>
- {this.monthlyData[0].map(day => {
- const { weekday } = day;
- const listCls = cls({
- [`${cssClasses.PREFIX}-weekend`]: markWeekend && day.isWeekend,
- });
- return (
- <li role="columnheader" aria-label={weekday} key={`${weekday}-monthheader`} className={listCls}>
- <span>{weekday}</span>
- </li>
- );
- })}
- </ul>
- </div>
- </div>
- );
- };
- 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 (
- <li
- className={`${cssClasses.PREFIX}-event-item ${cssClasses.PREFIX}-event-month`}
- key={key || `${ind}-monthevent`}
- style={style}
- >
- {children}
- </li>
- );
- });
- 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 = (
- <Button
- className={`${cardCls}-close`}
- onClick={e => this.closeCard(e, key)}
- type="tertiary"
- icon={<IconClose />}
- theme="borderless"
- size="small"
- />
- );
- const header = (
- <div className={`${cardCls}-header-info`}>
- <div className={`${cardCls}-header-info-weekday`}>{weekday}</div>
- <div className={`${cardCls}-header-info-date`}>{dayString}</div>
- </div>
- );
- const content = (
- <div className={cardCls}>
- <div className={`${cardCls}-content`}>
- <div className={`${cardCls}-header`}>
- {header}
- {closer}
- </div>
- <div className={`${cardCls}-body`}>
- <ul className={`${cardCls}-list`}>
- {events.map(item => (
- <li key={item.key || `${item.start.toString()}-event`}>{item.children}</li>
- ))}
- </ul>
- </div>
- </div>
- </div>
- );
- const pos = showCard && showCard[key] ? showCard[key][1] : 'leftTopOver';
- const text = (
- <LocaleConsumer componentName="Calendar">
- {(locale: Locale['Calendar']) => (// eslint-disable-next-line jsx-a11y/no-static-element-interactions
- <div
- className={`${cardCls}-wrapper`}
- style={{ bottom: 0 }}
- onClick={e => this.showCard(e, key)}
- >
- {locale.remaining.replace('${remained}', String(remained))}
- </div>
- )}
- </LocaleConsumer>
- );
- return (
- <Popover
- key={`${date.valueOf()}`}
- content={content}
- position={pos as any}
- trigger="custom"
- visible={showCard && showCard[key] && showCard[key][0]}
- ref={ref => this.cardRef.set(key, ref)}
- >
- <li key={date as any} className={listCls} onClick={e => this.handleClick(e, [date])}>
- {this.formatDayString(month, dayString)}
- {shouldRenderCard ? text : null}
- {this.renderCusDateGrid(date)}
- </li>
- </Popover>
- );
- };
- formatDayString = (month: string, date: string) => {
- if (date === '1') {
- return (
- <LocaleConsumer componentName="Calendar">
- {(locale: Locale['Calendar'], localeCode: string) => (
- <span className={`${prefixCls}-date`}>
- {month}
- <span className={`${cssClasses.PREFIX}-today-date`}> {date}</span>
- {locale.datestring}
- </span>
- )}
- </LocaleConsumer>
- );
- }
- return (
- // eslint-disable-next-line max-len
- <span className={`${prefixCls}-date`}><span className={`${cssClasses.PREFIX}-today-date`}>{date}</span></span>
- );
- };
- renderCusDateGrid = (date: Date) => {
- const { dateGridRender } = this.props;
- if (!dateGridRender) {
- return null;
- }
- return dateGridRender(date.toString(), date);
- };
- renderWeekRow = (index: number | string, weekDay: MonthData[number], events: MonthlyEvent = {} as MonthlyEvent) => {
- const { markWeekend } = this.props;
- const { itemLimit } = this.state;
- const { display, day } = events;
- return (
- <div role="presentation" className={`${prefixCls}-weekrow`} ref={this.cellDom} key={`${index}-weekrow`}>
- <ul role="row" className={`${prefixCls}-skeleton`}>
- {weekDay.map(each => {
- const { date, dayString, isToday, isSameMonth, isWeekend, month, ind } = each;
- const listCls = cls({
- [`${cssClasses.PREFIX}-today`]: isToday,
- [`${cssClasses.PREFIX}-weekend`]: markWeekend && isWeekend,
- [`${prefixCls}-same`]: isSameMonth
- });
- const shouldRenderCollapsed = Boolean(day && day[ind] && day[ind].length > itemLimit);
- const inner = (
- <li role="gridcell" aria-label={date.toLocaleDateString()} aria-current={isToday ? "date" : false} key={`${date}-weeksk`} className={listCls} onClick={e => this.handleClick(e, [date])}>
- {this.formatDayString(month, dayString)}
- {this.renderCusDateGrid(date)}
- </li>
- );
- if (!shouldRenderCollapsed) {
- return inner;
- }
- return this.renderCollapsed(day[ind], each, listCls, month);
- })}
- </ul>
- <ul className={`${cssClasses.PREFIX}-event-items`}>
- {display ? this.renderEvents(display) : null}
- </ul>
- </div>
- );
- };
- renderMonthGrid = () => {
- const { parsedEvents } = this.state;
- return (
- <div role="presentation" className={`${prefixCls}-week`}>
- <ul role="presentation" className={`${prefixCls}-grid-col`}>
- {Object.keys(this.monthlyData).map(weekInd =>
- this.renderWeekRow(weekInd, this.monthlyData[weekInd], parsedEvents[weekInd])
- )}
- </ul>
- </div>
- );
- };
- render() {
- const { className, height, width, style, header } = this.props;
- const monthCls = cls(prefixCls, className);
- const monthStyle = {
- height,
- width,
- ...style,
- };
- return (
- <LocaleConsumer componentName="Calendar">
- {(locale: Locale['Calendar'], localeCode: string, dateFnsLocale: Locale['dateFnsLocale']) => (
- <div role="grid" className={monthCls} key={this.state.itemLimit} style={monthStyle}>
- <div role="presentation" className={`${prefixCls}-sticky-top`}>
- {header}
- {this.renderHeader(dateFnsLocale)}
- </div>
- <div role="presentation" className={`${prefixCls}-grid-wrapper`}>
- {this.renderMonthGrid()}
- </div>
- </div>
- )}
- </LocaleConsumer>
- );
- }
- }
|