import React from 'react'; export type CalendarProps = { year: Year; month: Month; todayJDN: number; onSwitch?: (year: Year, month: Month) => void, }; type CalendarState = { selecting: boolean, yearStr: string, }; export abstract class MonthBasedCalendar extends React.Component, CalendarState> { selection: React.RefObject; protected constructor(props: CalendarProps) { super(props); this.state = { selecting: false, yearStr: this.yearToString(props.year), }; this.selection = React.createRef(); } componentDidMount() { document.addEventListener('click', this.handleClickOutside, true); } componentDidUpdate(prevProps: CalendarProps) { if (prevProps.year !== this.props.year) { const yearStr = this.yearToString(this.props.year); if (this.state.yearStr !== yearStr) { this.setState({yearStr}); } } } componentWillUnmount() { document.removeEventListener('click', this.handleClickOutside, true); } goTo(year: Year, month: Month) { this.props.onSwitch?.(year, month); } abstract parseYear(year: string): Year; abstract parseMonth(month: string): Month; abstract yearToString(year: Year): string; abstract monthToString(month: Month): string; abstract prevYear(): void; abstract prevMonth(): void; abstract nextYear(): void; abstract nextMonth(): void; abstract isValidYear(year: string): boolean; abstract jdnLookup(jdn: number): {year: Year, month: Month}; abstract monthName(year: Year, month: Month): string; startSelection = () => { this.setState({selecting: true}); }; handleClickOutside = (event: any) => { if (this.state.selecting && this.selection.current && !this.selection.current.contains(event.target)) this.setState({selecting: false}); }; handleKeyUp = (event: any) => { if (event.key === 'Escape') this.setState({selecting: false}); }; monthChange = (event: any) => { this.goTo(this.props.year, this.parseMonth(event.target.value)); }; yearChange = (event: any) => { if (this.isValidYear(event.target.value)) { this.goTo(this.parseYear(event.target.value), this.props.month); } this.setState({yearStr: event.target.value}); }; goToToday = () => { const {year, month} = this.jdnLookup(this.props.todayJDN); this.goTo(year, month); this.setState({selecting: false}); }; abstract renderMonthOptions(): JSX.Element[]; abstract renderBody(): JSX.Element; renderPrevArrows(): JSX.Element { return
; } renderNextArrows(): JSX.Element { return
; } renderMonthName(): JSX.Element { const {year, month} = this.props; return
{this.monthName(year, month)}
; } renderMonthSelection(): JSX.Element { return
; } renderHead(): JSX.Element { return
{this.renderPrevArrows()} {this.state.selecting ? this.renderMonthSelection() : this.renderMonthName()} {this.renderNextArrows()}
; } render(): JSX.Element { return
{this.renderHead()} {this.renderBody()}
; } }