UNPKG

@fullcalendar/web-component

Version:
363 lines (358 loc) 20 kB
import { R as RefMap, f as DayTableSlicer, e as dayMicroWidth, a as Ruler, d as DayGridHeaderRow, D as DayGridRows, T as TableDateProfileGenerator, g as buildDayTableModel, i as createDayHeaderFormatter, t as buildDateRowConfig, u as buildDayTableRenderRange } from './bd0d9fb1.js'; import { jsx, jsxs } from 'preact/jsx-runtime'; import { createRef, createElement } from 'preact/compat'; import { j as joinClassNames, f as fracToCssDim } from './423b7bc6.js'; import { o as DateComponent, a as memoize, f as afterSize, s as setRef, v as buildNavLinkAttrs, g as generateClassName, w as watchHeight, d as getIsHeightAuto, n as NowTimer, h as ViewContainer, S as Scroller } from './fa7e597f.js'; import { joinDateTimeFormatParts, formatIsoMonthStr, createDuration, intersectRanges } from '@full-ui/headless-calendar'; import { I as computeViewBorderless, F as classNames, c as createFormatter } from './69b11357.js'; class SingleMonth extends DateComponent { constructor() { super(...arguments); this.state = {}; // memo this.buildDayTableModel = memoize(buildDayTableModel); this.createDayHeaderFormatter = memoize(createDayHeaderFormatter); this.buildDateRowConfig = memoize(buildDateRowConfig); // ref this.titleElRef = createRef(); this.tableHeaderElRef = createRef(); this.rowHeightRefMap = new RefMap(() => { afterSize(this.handleHeights); }); this.slicer = new DayTableSlicer(); this.handleEl = (el) => { const { options } = this.context; if (el) { this.rootEl = el; options.singleMonthDidMount?.({ el: this.rootEl, ...this.renderProps, }); } }; this.handleGridWidth = (gridWidth) => { if (this._isUnmounting) return; this.setState({ gridWidth }); }; this.handleHeights = () => { if (this._isUnmounting) return; setRef(this.props.heightsRef, { titleHeight: this.titleHeight, tableHeaderHeight: this.tableHeaderHeight, rowHeightMap: this.rowHeightRefMap.current, cellRows: this.cellRows, }); }; } get titleId() { return this.context.baseId + 'month-' + this.props.isoDateStr; } render() { const { props, state, context } = this; const { dateProfile, forPrint } = props; const { options, dateEnv } = context; const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options); const dayTableModel = this.buildDayTableModel(dateProfile, context.dateProfileGenerator, dateEnv); const slicedProps = this.slicer.sliceProps(props, dateProfile, options.nextDayThreshold, context, dayTableModel); const dayHeaderFormat = this.createDayHeaderFormatter(options.dayHeaderFormat, false, // datesRepDistinctDays dayTableModel.colCount); const rowConfig = this.buildDateRowConfig(dayTableModel.headerDates, false, // datesRepDistinctDays dateProfile, props.todayRange, dayHeaderFormat, context); this.cellRows = dayTableModel.cellRows; const isTitleAndHeaderSticky = !forPrint && props.colCount === 1; const isAspectRatio = !forPrint || props.hasLateralSiblings; const cellColCnt = dayTableModel.cellRows[0].length; const colWidth = state.gridWidth != null ? state.gridWidth / cellColCnt : undefined; const cellIsMicro = colWidth != null && colWidth <= dayMicroWidth; const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth); const rowHeightGuess = state.gridWidth != null ? (1 / options.aspectRatio) * state.gridWidth / 6 : undefined; const headerStickyBottom = isTitleAndHeaderSticky ? rowHeightGuess : undefined; const titleStickyBottom = isTitleAndHeaderSticky && rowHeightGuess != null && state.tableHeaderHeight != null ? rowHeightGuess + state.tableHeaderHeight + 1 : undefined; const businessHourSegs = forPrint ? [] : slicedProps.businessHourSegs; const dateSelectionSegs = forPrint ? [] : slicedProps.dateSelectionSegs; const eventDrag = forPrint ? null : slicedProps.eventDrag; const eventResize = forPrint ? null : slicedProps.eventResize; const hasNavLink = options.navLinks && props.colCount > 1; const headerRenderProps = { multiMonthColumns: props.colCount || 0, isSticky: isTitleAndHeaderSticky, isNarrow: cellIsNarrow, hasNavLink, }; const monthStartDate = props.dateProfile.currentRange.start; const navLinkAttrs = hasNavLink ? buildNavLinkAttrs(context, monthStartDate, 'month', props.isoDateStr) : {}; return (jsx("div", { role: 'listitem', style: { width: props.width }, children: jsxs("div", { role: 'grid', "aria-labelledby": this.titleId, "data-date": props.isoDateStr, className: joinClassNames(generateClassName(options.singleMonthClass, { isFirst: props.isFirst, isLast: props.isLast, multiMonthColumns: props.colCount || 0, }), classNames.flexCol, props.hasLateralSiblings && classNames.breakInsideAvoid), children: [jsx(Ruler, { widthRef: this.handleGridWidth }), jsx("div", { id: this.titleId, ref: this.titleElRef, className: joinClassNames(generateClassName(options.singleMonthHeaderClass, headerRenderProps), isTitleAndHeaderSticky && classNames.stickyT, classNames.flexCol), style: { // HACK to keep zIndex above table-header, // because in Chrome, something about position:sticky on this title div // causes its bottom border to no be considered part of its mass, // and would get overlapped and hidden by the table-header div zIndex: isTitleAndHeaderSticky ? 3 : undefined, // TODO: className? marginBottom: titleStickyBottom, }, children: jsx("div", { ...navLinkAttrs, className: joinClassNames(generateClassName(options.singleMonthHeaderInnerClass, headerRenderProps), navLinkAttrs.className), children: joinDateTimeFormatParts(dateEnv.formatToParts(monthStartDate, props.titleFormat)) }) }), jsxs("div", { className: joinClassNames(generateClassName(options.tableClass, { borderlessX, borderlessTop, borderlessBottom, multiMonthColumns: props.colCount || 0, }), classNames.flexCol), style: { marginTop: titleStickyBottom != null ? -titleStickyBottom : undefined, }, children: [jsxs("div", { ref: this.tableHeaderElRef, className: joinClassNames(generateClassName(options.tableHeaderClass, { isSticky: isTitleAndHeaderSticky, borderlessX, borderlessTop, borderlessBottom, multiMonthColumns: props.colCount || 0, }), classNames.flexCol, isTitleAndHeaderSticky && classNames.sticky), style: { zIndex: isTitleAndHeaderSticky ? 2 : undefined, // TODO: className? top: isTitleAndHeaderSticky ? state.titleHeight : 0, marginBottom: headerStickyBottom, }, children: [jsx(DayGridHeaderRow, { ...rowConfig, role: 'row', borderBottom: false, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowLevel: 0 }), jsx("div", { className: generateClassName(options.dayHeaderDividerClass, { isSticky: isTitleAndHeaderSticky, multiMonthColumns: props.colCount || 0, options: { allDaySlot: Boolean(options.allDaySlot) }, }) })] }), jsx("div", { className: joinClassNames(generateClassName(options.tableBodyClass, { borderlessX, borderlessTop, borderlessBottom, multiMonthColumns: props.colCount || 0, }), classNames.flexCol, isAspectRatio && classNames.rel), style: { zIndex: isTitleAndHeaderSticky ? 1 : undefined, // TODO: className? marginTop: headerStickyBottom != null ? -headerStickyBottom : undefined, aspectRatio: isAspectRatio ? String(options.aspectRatio) : undefined, }, children: jsx(DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: dayTableModel.cellRows, className: isAspectRatio ? classNames.fill : '', forPrint: forPrint && !props.hasLateralSiblings, dayMaxEventRows: (forPrint && props.hasLateralSiblings) ? 1 // for side-by-side multimonths, limit to one row : true // otherwise, always do +more link, never expand rows , // content fgEventSegs: slicedProps.fgEventSegs, bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: businessHourSegs, dateSelectionSegs: dateSelectionSegs, eventDrag: eventDrag, eventResize: eventResize, eventSelection: slicedProps.eventSelection, // dimensions visibleWidth: state.gridWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowHeightRefMap: this.rowHeightRefMap }) })] })] }) })); } componentDidMount() { this._isUnmounting = false; this.disconnectTitleHeight = watchHeight(this.titleElRef.current, (height) => { this.setState({ titleHeight: this.titleHeight = height }); afterSize(this.handleHeights); }); this.disconnectTableHeaderHeight = watchHeight(this.tableHeaderElRef.current, (height) => { this.setState({ tableHeaderHeight: this.tableHeaderHeight = height }); afterSize(this.handleHeights); }); } componentWillUnmount() { const { options } = this.context; this._isUnmounting = true; this.disconnectTitleHeight(); this.disconnectTableHeaderHeight(); options.singleMonthWillUnmount?.({ el: this.rootEl, ...this.renderProps, }); } } class MultiMonthView extends DateComponent { constructor() { super(...arguments); this.state = {}; // memo this.splitDateProfileByMonth = memoize(splitDateProfileByMonth); this.buildMonthFormat = memoize(buildMonthFormat); // ref this.scrollerRef = createRef(); this.tilesElRef = createRef(); this.scrollState = {}; // Scrolling // ----------------------------------------------------------------------------------------------- this.handleInnerWidth = (innerWidth) => { if (this._isUnmounting) return; this.setState({ innerWidth }); }; this.handleScrollStart = () => { this.scrollState.date = undefined; this.scrollState.top = undefined; }; this.handleScrollEnd = (isDevice) => { const scroller = this.scrollerRef.current; if (isDevice && scroller) { this.scrollState.top = scroller.y; this.scrollState.date = undefined; } }; } render() { const { context, props, state } = this; const { options } = context; const verticalScrolling = !props.forPrint && !getIsHeightAuto(options); const monthDateProfiles = this.splitDateProfileByMonth(context.dateProfileGenerator, props.dateProfile, context.dateEnv, options.fixedWeekCount, options.showNonCurrentDates); const monthTitleFormat = this.buildMonthFormat(options.singleMonthTitleFormat, monthDateProfiles); const { multiMonthMaxColumns, singleMonthMinWidth } = options; const { innerWidth } = state; let cols; let cssMonthWidth; let hasLateralSiblings = false; if (innerWidth != null) { cols = Math.max(1, Math.min(multiMonthMaxColumns, Math.floor(innerWidth / singleMonthMinWidth))); if (props.forPrint) { cols = Math.min(cols, 2); } cssMonthWidth = fracToCssDim(1 / cols); hasLateralSiblings = cols > 1; } return (jsx(NowTimer, { unit: "day", children: (nowDate, todayRange) => (jsxs(ViewContainer, { viewSpec: context.viewSpec, className: joinClassNames( // HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard: // https://stackoverflow.com/a/60256345 !props.forPrint && classNames.flexCol, props.className), children: [jsx(Scroller, { vertical: verticalScrolling, className: verticalScrolling ? classNames.liquid : '', ref: this.scrollerRef, children: jsx("div", { role: 'list', ref: this.tilesElRef, "aria-labelledby": props.labelId, "aria-label": props.labelStr, className: classNames.safeTiles, children: monthDateProfiles.map((monthDateProfile, i) => { const monthStr = formatIsoMonthStr(monthDateProfile.currentRange.start); return (createElement(SingleMonth, { ...props, key: monthStr, todayRange: todayRange, isoDateStr: monthStr, titleFormat: monthTitleFormat, dateProfile: monthDateProfile, width: cssMonthWidth, colCount: cols, isFirst: !i, isLast: i === monthDateProfiles.length - 1, hasLateralSiblings: hasLateralSiblings })); }) }) }), jsx(Ruler, { widthRef: this.handleInnerWidth })] })) })); } // Lifecycle // ----------------------------------------------------------------------------------------------- componentDidMount() { this._isUnmounting = false; this.scrollState.date = this.props.dateProfile.currentDate; this.scrollerRef.current.addScrollStartListener(this.handleScrollStart); this.scrollerRef.current.addScrollEndListener(this.handleScrollEnd); // this.applyScroll() // definitely not ready yet b/c doesn't have state.innerWidth // workaround for off-by-a-few-pixels on first time when multiMonthMaxColumns=1, not sure why setTimeout(() => { this.applyScroll(); }, 0); } componentDidUpdate(prevProps, prevState) { if (prevProps.dateProfile !== this.props.dateProfile) { if (this.context.options.scrollTimeReset) { this.resetScroll(); } else { this.applyScroll(); } } else if (prevState.innerWidth !== this.state.innerWidth) { this.applyScroll(); } } componentWillUnmount() { this._isUnmounting = true; this.scrollerRef.current.removeScrollStartListener(this.handleScrollStart); this.scrollerRef.current.removeScrollEndListener(this.handleScrollEnd); } resetScroll() { this.scrollState.date = this.props.dateProfile.currentDate; this.scrollState.top = undefined; this.applyScroll(); } applyScroll() { const scroller = this.scrollerRef.current; const top = this.computeScrollTop(); if (scroller && top != null) { scroller.scrollTo({ y: top }); } } computeScrollTop() { const { scrollState } = this; if (scrollState.top != null) { return scrollState.top; } if (scrollState.date != null) { const tilesEl = this.tilesElRef.current; const monthEl = tilesEl?.querySelector(`[data-date="${formatIsoMonthStr(scrollState.date)}"]`); const monthWrapEl = monthEl?.parentElement; if (tilesEl && monthWrapEl) { // rounding required for proper alignment const monthTop = Math.round(monthWrapEl.getBoundingClientRect().top); const originTop = Math.round(tilesEl.getBoundingClientRect().top); return monthTop - originTop; } } } } // date profile // ------------------------------------------------------------------------------------------------- const oneMonthDuration = createDuration(1, 'month'); function splitDateProfileByMonth(dateProfileGenerator, dateProfile, dateEnv, fixedWeekCount, showNonCurrentDates) { const { start, end } = dateProfile.currentRange; let monthStart = start; const monthDateProfiles = []; while (monthStart.valueOf() < end.valueOf()) { const monthEnd = dateEnv.add(monthStart, oneMonthDuration); const currentRange = { // yuck start: dateProfileGenerator.skipHiddenDays(monthStart), end: dateProfileGenerator.skipHiddenDays(monthEnd, -1, true), }; let renderRange = buildDayTableRenderRange({ currentRange, snapToWeek: true, fixedWeekCount, dateEnv, }); renderRange = { // yuck start: dateProfileGenerator.skipHiddenDays(renderRange.start), end: dateProfileGenerator.skipHiddenDays(renderRange.end, -1, true), }; const activeRange = dateProfile.activeRange ? intersectRanges(dateProfile.activeRange, showNonCurrentDates ? renderRange : currentRange) : null; monthDateProfiles.push({ currentDate: dateProfile.currentDate, isValid: dateProfile.isValid, validRange: dateProfile.validRange, renderRange, activeRange, currentRange, currentRangeUnit: 'month', isRangeAllDay: true, dateIncrement: dateProfile.dateIncrement, slotMinTime: dateProfile.slotMaxTime, slotMaxTime: dateProfile.slotMinTime, }); monthStart = monthEnd; } return monthDateProfiles; } // date formatting // ------------------------------------------------------------------------------------------------- const YEAR_MONTH_FORMATTER = createFormatter({ year: 'numeric', month: 'long' }); const YEAR_FORMATTER = createFormatter({ month: 'long' }); function buildMonthFormat(formatOverride, monthDateProfiles) { return formatOverride || ((monthDateProfiles[0].currentRange.start.getUTCFullYear() !== monthDateProfiles[monthDateProfiles.length - 1].currentRange.start.getUTCFullYear()) ? YEAR_MONTH_FORMATTER : YEAR_FORMATTER); } var multiMonthPlugin = { name: 'multimonth', initialView: 'multiMonthYear', views: { multiMonth: { component: MultiMonthView, dateProfileGeneratorClass: TableDateProfileGenerator, multiMonthMaxColumns: 3, singleMonthMinWidth: 350, }, multiMonthYear: { type: 'multiMonth', duration: { years: 1 }, fixedWeekCount: true, // TODO: apply to all multi-col layouts? showNonCurrentDates: false, // TODO: looks bad when single-col layout }, }, }; export { multiMonthPlugin as m };