/* eslint-disable camelcase */ import dayjs, {Dayjs} from 'dayjs' import {ReactNode, MouseEvent} from 'react' import {createStateOperator} from '@befe/brick-utils' import {UILayoutItem} from '../ui-comps/ui-item' import { TypeItemStatus, TypeLayoutItem, TypePickerContentType, } from '../module-defs/ui-types' import {getMonthRows, Type_GetMonthText} from '../utils/date-utils' import {UiItemLayout} from '../ui-comps/ui-item-layout' import {UiPanelTitle} from '../ui-comps/ui-panel-title' import {UiPanel} from '../ui-comps/ui-panel' import {InnerRangePicker} from '../inner-comps/inner-range-picker' import {TypeInnerDatePicker} from '../inner-comps/create-inner-date-picker' import {PanelComp} from '../module-defs/common-types' import {createAside} from './create-aside' import { createDisplayedDateSwitcher, TypeMonthPanelDisplayProp, TypeMonthPanelValueProp, } from './create-date-switcher' import {createInitOpts} from './create-init-opts' import {normalizeRange} from './create-date-panel' export interface TypeInitOpts { onSelected?: (value: Dayjs) => void onSelectedRange?: (start: Dayjs, end: Dayjs) => void onZoomIn?: () => void } const startDisplayProp = 'startDisplayedDate' const endDisplayProp = 'endDisplayedDate' const zoomedStartDisplayProp = 'startZoomedDisplayedDate' const zoomedEndDisplayProp = 'endZoomedDisplayedDate' export function createMonthPanel( comp: TypeInnerDatePicker | InnerRangePicker, { isForDateZooming = false, isRange = false, valueProp, isSelectingProp = 'isSelecting', firstSelectedDateProp = 'firstSelectedDate', secondHoveringDateProp = 'secondHoveringDate', startValueProp = 'startValue', endValueProp = 'endValue', isStartPanel = false, onMouseDown, displayProp, getTitle, getMonthText, syncRangePickersYearAside, }: { isForDateZooming?: boolean isRange?: boolean valueProp?: TypeMonthPanelValueProp isSelectingProp?: 'isSelecting' firstSelectedDateProp?: 'firstSelectedDate' secondHoveringDateProp?: 'secondHoveringDate' startValueProp?: 'startValue' endValueProp?: 'endValue' isStartPanel?: boolean startDisplayProp?: 'startDisplayedDate' endDisplayProp?: 'endDisplayedDate' onMouseDown?: (e: MouseEvent) => void displayProp?: TypeMonthPanelDisplayProp getTitle: (date: Dayjs) => string getMonthText: Type_GetMonthText syncRangePickersYearAside?: () => void } ) { const [ getState, setState, ] = createStateOperator(comp as PanelComp) const initOpts = createInitOpts('MonthPanel') const getDisplayProp = () => { if (!isRange) { return displayProp! } if (isStartPanel) { if (isForDateZooming) { return zoomedStartDisplayProp } return startDisplayProp } else { if (isForDateZooming) { return zoomedEndDisplayProp } return endDisplayProp } } const displayedSwitcher = createDisplayedDateSwitcher( comp, { displayProp: getDisplayProp(), } ) const aside = createAside( comp, { displayProp: getDisplayProp(), onYearChange: () => { const shouldSyncDisplayPair = isRange && !isForDateZooming if (!shouldSyncDisplayPair) { return } const startDisplayed = getState(startDisplayProp) as Dayjs const endDisplayed = getState(endDisplayProp) as Dayjs if (!startDisplayed.isBefore(endDisplayed)) { if (isStartPanel) { setState({ [endDisplayProp]: startDisplayed.add(1, 'year'), }) } else { setState({ [startDisplayProp]: endDisplayed.subtract(1, 'year'), }) } syncRangePickersYearAside && syncRangePickersYearAside() } }, } ) const getDisplayed = (): Dayjs => { // @ts-ignore generic getter return getState(getDisplayProp()) } const getPanelTitleText = () => { return getTitle(getDisplayed()) } const handleZoomIn = () => { initOpts.get().onZoomIn!() } const getSelectedValueTimestamp = (): null | number => { const value = getState(valueProp!) // @ts-ignore generic getter // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access return value && value.startOf('month').valueOf() } const handleSelectDate = (selectedTimestamp?: number) => { const selectedDate = dayjs(selectedTimestamp) setState({ [valueProp!]: selectedDate, }) initOpts.get().onSelected!(selectedDate) } function handleRangeClickOnItem(timestamp?: number) { const isSelecting = getState(isSelectingProp) const clickedDate = dayjs(timestamp).startOf('month') if (!isSelecting) { setState({ [isSelectingProp]: true, [firstSelectedDateProp]: clickedDate, [secondHoveringDateProp]: null, }) } else { let first = getState(firstSelectedDateProp) as Dayjs let second = clickedDate if (first.isAfter(second)) { [first, second] = [second, first] } setState({ [isSelectingProp]: false, [startValueProp]: first, [endValueProp]: second, }) initOpts.get().onSelectedRange!(first, second) } } function setRangeHoveringDate(timestamp?: number) { if (getState(isSelectingProp)) { setState({ [secondHoveringDateProp]: dayjs(timestamp).startOf('month'), }) } } const renderContentItem = ( { today, selectedValueTimestamp, isRange = false, rangeStart, rangeEnd, }: { today: Dayjs selectedValueTimestamp?: number | null isRange?: boolean rangeStart?: Dayjs rangeEnd?: Dayjs }, item: TypeLayoutItem ) => { const data = item.data! let disabled = false if (!isForDateZooming && comp.props.getDisabledItem) { disabled = comp.props.getDisabledItem(data.date, today) } const itemDate = item.data!.date const itemValue = item.data!.date.valueOf() let selected if (isRange) { selected = false } else if (selectedValueTimestamp) { selected = itemValue === selectedValueTimestamp } // 处理范围样式 & 间隙的样式 let rangeType: TypeItemStatus['rangeType'] let prevGapClassName const shouldSetGapClass = !data.outside && (itemDate.month() % 3 !== 0) /** * 此处可参考 : components/date-picker/src/modules/create-date-panel.tsx */ if ((isRange && !isForDateZooming) && (rangeStart || rangeEnd)) { const { normalizedRangeStart, normalizedRangeEnd, } = normalizeRange(rangeStart, rangeEnd) if ( normalizedRangeEnd.isSame(normalizedRangeStart) && itemDate.isSame(normalizedRangeStart) ) { rangeType = 'single' } else if (itemDate.isSame(normalizedRangeStart)) { rangeType = 'start' } else if (itemDate.isSame(normalizedRangeEnd)) { prevGapClassName = shouldSetGapClass ? 'inside-range' : undefined rangeType = 'end' } else if ( itemDate.isAfter(normalizedRangeStart) && itemDate.isBefore(normalizedRangeEnd) ) { prevGapClassName = shouldSetGapClass ? 'inside-range' : undefined rangeType = 'inside' } else { rangeType = undefined } } return { prevGapClassName, elem: , } } const renderPanelContent = () => { const displayed = getDisplayed() return ( ) } const renderPanelTitle = () => { // eslint-disable-next-line @typescript-eslint/unbound-method const { goBackwardYear, goForwardYear, goBackwardMonth, goForwardMonth, } = displayedSwitcher const [startDisplay, endDisplay] = getState( [startDisplayProp, endDisplayProp] ) as [Dayjs, Dayjs] return ( ) } function renderRangeContent(): ReturnType { const displayed = getDisplayed() const isSelecting = getState(isSelectingProp) let rangeStart = isSelecting ? getState(firstSelectedDateProp) as Dayjs : getState(startValueProp) as Dayjs let rangeEnd = isSelecting ? getState(secondHoveringDateProp) as Dayjs : getState(endValueProp) as Dayjs if (rangeStart && rangeEnd && rangeStart.isAfter(rangeEnd)) { [rangeEnd, rangeStart] = [rangeStart, rangeEnd] } return ( ) } const api = { init( opts: TypeInitOpts ) { initOpts.init(opts) const switcherOpts = { onDisplayDateChange: (displayedDate: Dayjs) => { aside.setYearRangeAnchor(displayedDate.year()) aside.syncAsideScroll() }, } displayedSwitcher.init(switcherOpts) }, getRenderElem() { const renderedElem: { titleElem: ReactNode contentElem: ReactNode asideElem: ReactNode contentType: TypePickerContentType } = { titleElem: renderPanelTitle(), contentElem: isRange ? renderRangeContent() : renderPanelContent(), asideElem: aside.render(), contentType: 'month', } return renderedElem }, renderPanel() { return ( ) }, } return aside.extendApi(api) } export type TypeModuleMonthPanel = ReturnType