/* eslint-disable camelcase */ import c from 'classnames' import {ConfiguredComponent} from '@befe/brick-core' import {InputWrapper} from '@befe/brick-comp-input' import {Icon} from '@befe/brick-comp-icon' import {SvgCalendar, SvgDiscX, SvgMarkX} from '@befe/brick-icon' import {Popper, PopperTrigger} from '@befe/brick-comp-popper' import {getClosestMappedSize, safeInvoke} from '@befe/brick-utils' import dayjs, {Dayjs} from 'dayjs' import {DatePickerProps} from '../create-inner-date-picker' import { TypeDatePickerMode, TypePickerSize, TypePickerStatus, } from '../prop-types' import {extendDayjsPlugins} from '../../utils/extend-dayjs-plugins' import {MultiDatePanel} from './multi-date-panel' import {MultiMonthPanel} from './multi-month-panel' import {MultiQuarterPanel} from './multi-quarter-panel' const CONST_className_selectedItem = 'brick-multi-date-picker-selected-popper-item' const CONST_className_closeablePickerIcon = 'brick-multi-date-picker-closeable-icon' const CONST_className_calendarIcon = 'brick-multi-date-picker-calendar-icon' const CONST_className_closeIcon = 'brick-multi-date-picker-close-icon' export interface MultiDatePickerProps { /** * 自定义 class */ className?: string /** * 选中的日期值列表 (控制属性) * **注意: 此项值与 DatePicker 不一样, 单个值为 string, 而非 Dayjs 格式** * * e.g. * 日期格式 : ['2023-01-01', '2024-10-10', ...] * 月份格式 : ['2023-01', '2024-10', ...] * 季度格式 : ['2023-Q1', '2024-Q4', ...] * * @type string[] */ value?: string[] /** * 选中的日期值 (非控制属性) * 值的说明见 value 属性 * * @type string[] */ defaultValue?: string[] disabled?: boolean /** * 整体大小控制 : xs, sm, md, lg, xl * 会影响 "输入框" & "面板" 的尺寸 */ size?: TypePickerSize /** * 是否当前出于出错状态 */ status?: TypePickerStatus /** * 选择器的取值类型 */ mode?: TypeDatePickerMode /** * 选中的值, 在 "输入框" 中展示的内容 * 注意, 这个和 DatePicker 的 itemFormat (dayjs format 模板) 不同 */ formatItem?: (dateText: string) => string /** * @param date 当前计算 disabled 的日期 or 月份 or 季度 (**的第一天**) * @param firstDayOfCurrentUnit 今天的日期 or 月份 or 季度 (**的第一天**) */ getDisabledItem?: (date: Dayjs, firstDayOfCurrentUnit: Dayjs) => boolean /** * 当日期面板打开时, 设置其 "显示的面板位置" * * 这个可能主要在当没有选择日期时, 用于确定面板月份的显示策略 ( 缺省方案是显示 "当月" 或 "当年" ) */ getDefaultDisplayDate?: (today: Dayjs) => Dayjs /** * 是否显示 : 今日 (注意, 本项在 `月份`, `季度` 模式下无效) * * @default false */ hideNow?: boolean hideSummary?: boolean /** * 面板的附加 className */ popupClassName?: string /** * 当选择中一个日期时, 会触发该回调 * * @param selectedList 选中的日期列表, 具体格式见 value 属性 */ onChange?: (selectedList: string[]) => void /** * 日期选择器 input 框的 placeholder */ placeholder?: string } interface MultiDatePickerState { innerValue: string[] isZooming: boolean displayedDate: Dayjs | null } const CONST_rgxQuarter = /^(\d{4})-Q(\d)$/ /** */ export class MultiDatePicker extends ConfiguredComponent { static defaultProps: MultiDatePickerProps = { mode: 'date', hideNow: false, } get size(): DatePickerProps['size'] { return this.getDefaultValueUsingTheme('size', 'baseSize') } get className() { const {className} = this.props const size = this.size return c( 'brick-multi-date-picker', { [`brick-date-picker-size-${size || ''}`]: size, }, className ) } get popupClassName() { const {popupClassName} = this.props const size = getClosestMappedSize(this.size) return c( 'brick-date-picker-popper', { [`brick-date-picker-popper-size-${size || ''}`]: size, }, popupClassName ) } state: MultiDatePickerState = { innerValue: [], isZooming: false, displayedDate: null, } constructor(props: MultiDatePickerProps) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument super(props) extendDayjsPlugins() const initValue = typeof this.props.defaultValue === 'undefined' ? this.props.value : this.props.defaultValue this.state.innerValue = initValue || [] const firstDateText = this.state.innerValue[0] const today = dayjs() this.state.displayedDate = this.parseDateText(firstDateText) || today } static getDerivedStateFromProps( nextProps: MultiDatePickerProps, prevState: MultiDatePickerState ) { const nextValue = nextProps.value const nextInnerValue = typeof nextValue !== 'undefined' ? nextValue : prevState.innerValue let hasStateAltered = false const partialState: Partial = {} if (nextInnerValue !== prevState.innerValue) { hasStateAltered = true partialState.innerValue = nextInnerValue } if (hasStateAltered) { return partialState } return null } render() { const disabled = this.props.disabled const elemInput = ( {this.renderInputContent()} ) if (disabled) { return elemInput } return ( {elemInput} {this.renderPanelContent()} ) } private renderInputContent() { const innerValue = this.state.innerValue const CONST_className_multiDate_inputContent = 'brick-multi-date-picker-input-content' const CONST_className_multiDate_placeholder = 'brick-multi-date-picker-placeholder' if (innerValue.length === 0) { return (
{this.props.placeholder || '\u00A0'}
) } const inputValueText = innerValue .map( dateText => { if (this.props.formatItem) { return this.props.formatItem(dateText) } return dateText } ).join(', ') return (
{inputValueText}
) } private readonly dayjsCached = new Map() private readonly parseDateText = ( dayText?: string ) => { if (!dayText) { return null } const { mode, } = this.props const dayjsValue = this.dayjsCached.get(dayText) if (dayjsValue) { return dayjsValue } if (mode === 'date') { const dayjsValue = dayjs(dayText) this.dayjsCached.set(dayText, dayjsValue) return dayjsValue } if (mode === 'month') { const dayjsValue = dayjs(dayText + '-01') this.dayjsCached.set(dayText, dayjsValue) return dayjsValue } if (mode === 'quarter') { const quarterMatch = CONST_rgxQuarter.exec(dayText) if (!quarterMatch) { throw new Error(`Invalid quarter format: ${dayText}`) } const year = quarterMatch[1] const quarter = quarterMatch[2] const firstMonthOfQuarter = quarter === '1' ? '01' : quarter === '2' ? '04' : quarter === '3' ? '07' : '10' const firstDayOfQuarter = dayjs( `${year}-${firstMonthOfQuarter}-01` ) this.dayjsCached.set(dayText, firstDayOfQuarter) return firstDayOfQuarter } throw new Error(`Invalid mode: ${mode}`) } private renderPickerIcon() { if (this.props.hideSummary) { return this.renderPickerIcon_plainIcon() } return this.renderPickerIcon_withSelectedList() } private renderPickerIcon_withSelectedList() { const dateCount = this.state.innerValue.length if (dateCount > 0) { return ( e.stopPropagation()}> {dateCount} {this.renderSelectedList_clearAll(dateCount)} {this.renderSelectedList()} ) } return ( ) } private renderSelectedList_clearAll(dateCount: number) { const { disabled, } = this.props if (disabled) { return null } return (
清除 {dateCount} 个选择
) } private renderPickerIcon_plainIcon() { const { disabled, } = this.props if (disabled) { return } const { innerValue, } = this.state if (innerValue.length === 0) { return } return ( ) } private renderSelectedList() { const disabled = this.props.disabled return this.state .innerValue .slice() .sort() .map(dateText => { const renderDeleteIcon = () => { if (disabled) { return null } return ( this.removeSelectedDate(dateText)} /> ) } return (
{dateText} {renderDeleteIcon()}
) }) } private renderPanelContent() { const { mode, } = this.props const { isZooming, } = this.state if (mode === 'date') { if (isZooming) { return this.renderDateZoomedMonthPanel() } return this.renderDatePanel() } if (mode === 'month') { return this.renderMonthPanel() } return this.renderQuarterPanel() } private renderQuarterPanel() { return ( ) } private renderDatePanel() { return ( ) } private renderMonthPanel() { return ( ) } private renderDateZoomedMonthPanel() { const selectedDisplayed = this.state.displayedDate ? [this.state.displayedDate.startOf('month').valueOf()] : [] return ( ) } private getValueList() { return this.state.innerValue.map( date => this.parseDateText(date)!.valueOf() ) } private readonly handleDateForZoomingClick = (dateValue?: number) => { if (!dateValue) { return } const displayedDate = dayjs(dateValue) this.setState({ isZooming: false, displayedDate, }) } private readonly handleDateClick = (dateValue?: number) => { if (!dateValue) { return } const doesExist = this.state.innerValue.some( dateText => { return this.parseDateText(dateText)!.valueOf() === dateValue } ) if (doesExist) { const nextInnerValue = this.state.innerValue.filter( dateText => { return this.parseDateText(dateText)!.valueOf() !== dateValue } ) this.handleChange(nextInnerValue) return } this.handleChange([ this.toDateText(dateValue), ...this.state.innerValue, ]) } private toDateText(dateValue: number) { const mode = this.props.mode if (mode === 'date') { return dayjs(dateValue).format('YYYY-MM-DD') } if (mode === 'month') { return dayjs(dateValue).format('YYYY-MM') } return dayjs(dateValue).format('YYYY-[Q]Q') } private readonly handlePrevDisplayedYear = () => { const nextDisplayedDate = this.state.displayedDate!.subtract(1, 'year') this.setState({ displayedDate: nextDisplayedDate, }) } private readonly handleNextDisplayedYear = () => { const nextDisplayedDate = this.state.displayedDate!.add(1, 'year') this.setState({ displayedDate: nextDisplayedDate, }) } private readonly handlePrevDisplayedMonth = () => { const nextDisplayedDate = this.state.displayedDate!.subtract(1, 'month') this.setState({ displayedDate: nextDisplayedDate, }) } private readonly handleNextDisplayedMonth = () => { const nextDisplayedDate = this.state.displayedDate!.add(1, 'month') this.setState({ displayedDate: nextDisplayedDate, }) } private readonly handleToggleZooming = () => { const nextIsZooming = !this.state.isZooming this.setState({ isZooming: nextIsZooming, }) } private readonly handleTogglePopper = (visible: boolean) => { if (visible) { const today = dayjs() const firstDateText = this.state.innerValue[0] const nextDisplayedDate = this.parseDateText(firstDateText) || (this.props.getDefaultDisplayDate && this.props.getDefaultDisplayDate(today)) || today this.setState({ displayedDate: nextDisplayedDate, }) } } private readonly handleDisplayedDateChange = (date: Dayjs) => { this.setState({ displayedDate: date, }) } private removeSelectedDate(dateText: string) { const nextInnerValue = this.state.innerValue.filter( eachDateText => eachDateText !== dateText ) this.handleChange(nextInnerValue) } private handleChange(nextInnerValue: string[]) { this.setState({ innerValue: nextInnerValue, }) safeInvoke( this.props.onChange, nextInnerValue ) } private readonly clearAllSelected = () => { this.handleChange([]) } }