From f67bd1bbef0518be7f4ed261e41a05a3adb14bac Mon Sep 17 00:00:00 2001 From: Quantum Date: Sat, 15 Jul 2023 14:51:51 -0400 Subject: [PATCH] Factor out abstract month-based calendar UI --- common/src/MonthBasedCalendar.tsx | 153 +++++++++++++++++++++++++++++ frcal/src/Calendar.tsx | 158 +++++++++--------------------- jcal/src/Calendar.tsx | 151 ++++++++-------------------- 3 files changed, 241 insertions(+), 221 deletions(-) create mode 100644 common/src/MonthBasedCalendar.tsx diff --git a/common/src/MonthBasedCalendar.tsx b/common/src/MonthBasedCalendar.tsx new file mode 100644 index 0000000..a7a4eb5 --- /dev/null +++ b/common/src/MonthBasedCalendar.tsx @@ -0,0 +1,153 @@ +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()} +
; + } +} diff --git a/frcal/src/Calendar.tsx b/frcal/src/Calendar.tsx index e0dd067..13eb204 100644 --- a/frcal/src/Calendar.tsx +++ b/frcal/src/Calendar.tsx @@ -4,21 +4,24 @@ import { dateName, dateRuralName, decadeNames, - frEndYear, FrenchDay, FrenchMonth, + frEndYear, frIsLeap, frJDN, + frStartYear, jdnFrench, monthName, - frStartYear, } from '@common/french'; import {jdnDate} from '@common/gregorian'; import {jdnLongCount} from '@common/longCount'; import {useMobileTooltipProps} from '@common/MobileTooltip'; +import {MonthBasedCalendar} from '@common/MonthBasedCalendar'; + +type FrenchYear = number; type MonthProps = { - year: number; + year: FrenchYear; month: FrenchMonth; }; @@ -73,7 +76,7 @@ function ComplementaryDay({year, month, day, todayJDN}: DateProps & { todayJDN: ; } -function ComplementaryDays({year, todayJDN}: { year: number, todayJDN: number }): JSX.Element { +function ComplementaryDays({year, todayJDN}: { year: FrenchYear, todayJDN: number }): JSX.Element { const leap = frIsLeap(year); return
{ Array.from(Array(6).keys()).map(i => @@ -84,43 +87,30 @@ function ComplementaryDays({year, todayJDN}: { year: number, todayJDN: number }) }
; } -export type CalendarProps = MonthProps & { - todayJDN: number; - onSwitch?: (year: number, month: FrenchMonth) => void, -}; - -type CalendarState = { - selecting: boolean, - yearStr: string, -}; - -export class Calendar extends React.Component { - selection: React.RefObject; - - constructor(props: CalendarProps) { - super(props); - this.state = { - selecting: false, - yearStr: this.props.year.toString(), - }; - this.selection = React.createRef(); +export class Calendar extends MonthBasedCalendar { + override parseYear(year: string): FrenchYear { + return +year; } - componentDidMount() { - document.addEventListener('click', this.handleClickOutside, true); + override parseMonth(month: string): FrenchMonth { + return +month as FrenchMonth; } - componentWillUnmount() { - document.removeEventListener('click', this.handleClickOutside, true); + override yearToString(year: FrenchYear): string { + return year.toString(); + } + + override monthToString(month: FrenchMonth): string { + return month.toString(); } private goToNormalized(year: number, month: number) { - if (month < 1) { + while (month < 1) { --year; month += 13; } - if (month > 13) { + while (month > 13) { ++year; month -= 13; } @@ -133,105 +123,49 @@ export class Calendar extends React.Component { month = 13; } - this.props.onSwitch && this.props.onSwitch(year, month as FrenchMonth); + this.goTo(year, month as FrenchMonth); } - prevYear = () => { + override prevYear = () => { this.goToNormalized(this.props.year - 1, this.props.month); - } + }; - prevMonth = () => { + override prevMonth = () => { this.goToNormalized(this.props.year, this.props.month - 1); - } + }; - nextYear = () => { + override nextYear = () => { this.goToNormalized(this.props.year + 1, this.props.month); - } + }; - nextMonth = () => { + override nextMonth = () => { this.goToNormalized(this.props.year, this.props.month + 1); + }; + + override isValidYear(year: string): boolean { + return /^-?\d+/.test(year); } - startSelection = () => { - this.setState({selecting: true}); + override jdnLookup(jdn: number): { year: FrenchYear; month: FrenchMonth } { + return jdnFrench(jdn); } - handleClickOutside = (event: any) => { - if (this.state.selecting && this.selection.current && !this.selection.current.contains(event.target)) - this.setState({selecting: false}); + override monthName(year: FrenchYear, month: FrenchMonth): string { + return month === 13 ? year.toString() : `${monthName(month)} ${year}`; } - handleKeyUp = (event: any) => { - if (event.key === 'Escape') - this.setState({selecting: false}); + override renderMonthOptions(): JSX.Element[] { + return Array.from(Array(13).keys()).map(i => { + const month = i + 1 as FrenchMonth; + return ; + }); } - monthChange = (event: any) => { - this.goToNormalized(this.props.year, +event.target.value as FrenchMonth); - } - - yearChange = (event: any) => { - if (/^-?\d+/.test(event.target.value)) { - this.goToNormalized(+event.target.value, this.props.month); + override renderBody(): JSX.Element { + if (this.props.month < 13) { + return ; + } else { + return ; } - this.setState({yearStr: event.target.value}); - } - - goToToday = () => { - const {year, month} = jdnFrench(this.props.todayJDN); - this.goToNormalized(year, month); - this.setState({selecting: false}); - } - - componentDidUpdate(prevProps: CalendarProps) { - if (prevProps.year !== this.props.year) { - const yearStr = this.props.year.toString(); - if (this.state.yearStr !== yearStr) { - this.setState({ - yearStr: yearStr, - }); - } - } - } - - render(): JSX.Element { - return
-
-
- - -
- {!this.state.selecting &&
- {this.props.month < 13 && monthName(this.props.month)} {this.props.year} -
} - {this.state.selecting &&
- - - -
} -
- - -
-
- {this.props.month < 13 && - } - {this.props.month === 13 && } -
; } } diff --git a/jcal/src/Calendar.tsx b/jcal/src/Calendar.tsx index 45865e1..581ebdd 100644 --- a/jcal/src/Calendar.tsx +++ b/jcal/src/Calendar.tsx @@ -5,9 +5,12 @@ import {jdnLongCount} from '@common/longCount'; import {jdnJulian, julianJDN, julianMonthDays} from '@common/julian'; import {frDateFormat, frEndJD, frStartJD, jdnFrench} from '@common/french'; import {useMobileTooltipProps} from '@common/MobileTooltip'; +import {MonthBasedCalendar} from '@common/MonthBasedCalendar'; + +type JulianYear = number; type MonthProps = { - year: number; + year: JulianYear; month: JulianMonth; }; @@ -72,144 +75,74 @@ function Month({year, month, todayJDN}: MonthProps & { todayJDN: number }): JSX. ; } -export type CalendarProps = MonthProps & { - todayJDN: number; - onSwitch?: (year: number, month: JulianMonth) => void, -}; - -type CalendarState = { - selecting: boolean, - yearStr: string, -}; - -export class Calendar extends React.Component { - selection: React.RefObject; - - constructor(props: CalendarProps) { - super(props); - this.state = { - selecting: false, - yearStr: this.props.year.toString(), - }; - this.selection = React.createRef(); +export class Calendar extends MonthBasedCalendar { + override parseYear(year: string): JulianYear { + return +year; } - componentDidMount() { - document.addEventListener('click', this.handleClickOutside, true); + override parseMonth(month: string): JulianMonth { + return +month as JulianMonth; } - componentWillUnmount() { - document.removeEventListener('click', this.handleClickOutside, true); + override yearToString(year: JulianYear): string { + return year.toString(); + } + + override monthToString(month: JulianMonth): string { + return month.toString(); } private goToNormalized(year: number, month: number) { - if (month < 1) { + while (month < 1) { --year; month += 12; } - if (month > 12) { + while (month > 12) { ++year; month -= 12; } - this.props.onSwitch && this.props.onSwitch(year, month as JulianMonth); + this.goTo(year, month as JulianMonth); } - prevYear = () => { + override prevYear = () => { this.goToNormalized(this.props.year - 1, this.props.month); }; - prevMonth = () => { + override prevMonth = () => { this.goToNormalized(this.props.year, this.props.month - 1); }; - nextYear = () => { + override nextYear = () => { this.goToNormalized(this.props.year + 1, this.props.month); }; - nextMonth = () => { + override nextMonth = () => { this.goToNormalized(this.props.year, this.props.month + 1); }; - 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.goToNormalized(this.props.year, +event.target.value as JulianMonth); - }; - - yearChange = (event: any) => { - if (/^-?\d+/.test(event.target.value)) { - this.goToNormalized(+event.target.value, this.props.month); - } - this.setState({yearStr: event.target.value}); - }; - - goToToday = () => { - const [year, month] = jdnJulian(this.props.todayJDN); - this.goToNormalized(year, month); - this.setState({selecting: false}); - }; - - componentDidUpdate(prevProps: CalendarProps) { - if (prevProps.year !== this.props.year) { - const yearStr = this.props.year.toString(); - if (this.state.yearStr !== yearStr) { - this.setState({ - yearStr: yearStr, - }); - } - } + override isValidYear(year: string): boolean { + return /^-?\d+/.test(year); } - render(): JSX.Element { - return
-
-
- - -
- {!this.state.selecting &&
- {monthName(this.props.month)} {this.props.year} -
} - {this.state.selecting &&
- - - -
} -
- - -
-
- -
; + override jdnLookup(jdn: number): { year: JulianYear; month: JulianMonth } { + const [year, month] = jdnJulian(jdn); + return {year, month}; + } + + override monthName(year: JulianYear, month: JulianMonth): string { + return `${monthName(month)} ${year}`; + } + + override renderMonthOptions(): JSX.Element[] { + return Array.from(Array(12).keys()).map(i => { + const month = i + 1 as JulianMonth; + return ; + }); + } + + override renderBody(): JSX.Element { + return } }