/* eslint-disable camelcase */ import dayjs, {Dayjs} from 'dayjs' import {ReactNode, MouseEvent} from 'react' import {createStateOperator} from '@befe/brick-utils' import {TypeLayoutRenderedItem, UiItemLayout} from '../ui-comps/ui-item-layout' import { getDateRows, getWeekTitleRow, Type_GetWeekDayTitle, } from '../utils/date-utils' import {UiPanel} from '../ui-comps/ui-panel' import {UiPanelTitle} from '../ui-comps/ui-panel-title' import { TypeItemStatus, TypeLayoutItem, TypePickerContentType, } from '../module-defs/ui-types' import {UILayoutItem} from '../ui-comps/ui-item' import { InnerRangePicker, RangePickerState, } from '../inner-comps/inner-range-picker' import { DatePickerState, TypeInnerDatePicker, } from '../inner-comps/create-inner-date-picker' import {PanelComp} from '../module-defs/common-types' import { createDisplayedDateSwitcher, TypeDisplayedDateSwitcherInitOpts, } from './create-date-switcher' import {createInitOpts} from './create-init-opts' export interface TypeInitOpts { onSelected?: (value: Dayjs) => void onSelectedRange?: (start: Dayjs, end: Dayjs) => void onZoomOut: () => void } export function normalizeRange( rangeStart: Dayjs | undefined, rangeEnd: Dayjs | undefined ): ({ normalizedRangeEnd: Dayjs normalizedRangeStart: Dayjs }) { if (rangeStart && !rangeEnd) { return { normalizedRangeStart: rangeStart, normalizedRangeEnd: rangeStart, } } else if (!rangeStart && rangeEnd) { return { normalizedRangeStart: rangeEnd, normalizedRangeEnd: rangeEnd, } } else if (rangeStart && rangeEnd) { return { normalizedRangeStart: rangeStart, normalizedRangeEnd: rangeEnd, } } throw new Error('rangeStart & rangeEnd 至少应该一个不为空') } export function createDatePanel( comp: InnerRangePicker | TypeInnerDatePicker, { isRange = false, valueProp = 'value', isSelectingProp = 'isSelecting', firstSelectedDateProp = 'firstSelectedDate', secondHoveringDateProp = 'secondHoveringDate', startValueProp = 'startValue', endValueProp = 'endValue', isStartPanel = true, startDisplayProp = 'startDisplayedDate', endDisplayProp = 'endDisplayedDate', onMouseDown, displayProp = 'displayedDate', getTitle, getWeekDayText, renderPanelSideContent, }: { isRange?: boolean valueProp?: 'value' isSelectingProp?: 'isSelecting' firstSelectedDateProp?: 'firstSelectedDate' secondHoveringDateProp?: 'secondHoveringDate' startValueProp?: 'startValue' endValueProp?: 'endValue' isStartPanel?: boolean startDisplayProp?: 'startDisplayedDate' endDisplayProp?: 'endDisplayedDate' onMouseDown?: (e: MouseEvent) => void displayProp?: 'displayedDate' getTitle: (date: Dayjs) => string getWeekDayText: Type_GetWeekDayTitle renderPanelSideContent?: () => ReactNode } ) { const _comp = comp as PanelComp const initOpts = createInitOpts('DatePanel') const displayedSwitcher = createDisplayedDateSwitcher( comp, { displayProp: !isRange ? displayProp : isStartPanel ? startDisplayProp : endDisplayProp, } ) const [ getState, setState, ] = createStateOperator(_comp) function getDisplayed() { if (isRange) { if (isStartPanel) { return getState(startDisplayProp) } return getState(endDisplayProp) } return getState(displayProp) } function getPanelTitleText(): string { return getTitle(getDisplayed() as Dayjs) } const handleZoomOut = () => { initOpts.get().onZoomOut() } const handleSelectDate = (selectedTimestamp?: number) => { const selectedDate = dayjs(selectedTimestamp) _comp.setState({ [valueProp]: selectedDate, } as DatePickerState & RangePickerState) initOpts.get().onSelected!(selectedDate) } const handleRangeClickOnItem = (timestamp?: number) => { const isSelecting = getState(isSelectingProp) const clickedDate = dayjs(timestamp).startOf('day') 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) } } const setRangeHoveredDate = (timestamp?: number) => { if (getState(isSelectingProp)) { setState({ [secondHoveringDateProp]: dayjs(timestamp).startOf('day'), }) } } const handleClickOutside = (timestamp?: number) => { _comp.setState({ [displayProp]: dayjs(timestamp).startOf('month'), }) } function renderContentItem( { today, selectedValue, isRange = false, rangeStart, rangeEnd, }: { today: Dayjs selectedValue?: number | null isRange?: boolean rangeStart?: Dayjs rangeEnd?: Dayjs }, item: TypeLayoutItem ): TypeLayoutRenderedItem { const data = item.data! let disabled = false if (comp.props.getDisabledItem) { disabled = comp.props.getDisabledItem(data.date, today) } const itemDate = item.data!.date // 处理已选择情况 let selected if (isRange) { selected = false } else if (selectedValue) { selected = itemDate.isSame(selectedValue, 'date') } // 处理范围样式 & 间隙的样式 let rangeType: TypeItemStatus['rangeType'] let prevGapClassName const shouldSetGapClass = !data.outside && itemDate.get('date') !== 1 if (isRange && (rangeStart || rangeEnd)) { const { normalizedRangeStart, normalizedRangeEnd, } = normalizeRange(rangeStart, rangeEnd) /** * 日历渲染范围 (range) 中, 对显示的类型进行匹配处理, * 非 Outside 类别的 item, 会有特定的样式处理 * * - St : Start * - Ed : End * - In : Inside * - Sg : Single * - X : Outside * * 1. 当 rangeStart < rangeEnd 时 * * X X X [St] In In In In [Ed] X X X * * 2. 当 rangeStart === rangeEnd 时 * * X X X [Sg] X X X */ 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: , } } function renderPanelSideContentWrapper(sideContent?: ReactNode | null) { if (!sideContent) { return null } return (
{sideContent}
) } function renderPanelContent(sideContent?: ReactNode | null) { const displayed = getDisplayed() as Dayjs return ( <> {renderPanelSideContentWrapper(sideContent)} ) } function renderRangePanelContent(): ReturnType { const displayed = getDisplayed() as Dayjs 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 ( ) } function renderPanelTitle() { const { goBackwardYear, goForwardYear, goBackwardMonth, goForwardMonth, } = displayedSwitcher const [startDisplay, endDisplay] = getState( [startDisplayProp, endDisplayProp] ) as [Dayjs, Dayjs] return ( ) } return { init( panelOpts: TypeInitOpts, switcherOpts?: TypeDisplayedDateSwitcherInitOpts ) { initOpts.init(panelOpts) if (switcherOpts) { displayedSwitcher.init(switcherOpts) } }, getRenderElem() { const renderedElem = { titleElem: renderPanelTitle(), contentElem: isRange ? renderRangePanelContent() : renderPanelContent(), contentType: 'date', asideElem: undefined, } return renderedElem as { titleElem: ReturnType contentElem: ReturnType contentType: TypePickerContentType asideElem: undefined } }, renderPanel() { const sideContent = (!isRange && renderPanelSideContent) ? renderPanelSideContent() : null const panelClassName = sideContent ? 'panel-with-side-content' : '' return ( ) }, } }