@fullcalendar/web-component
Version:
Custom Element for FullCalendar
361 lines (352 loc) • 19.8 kB
JavaScript
import { jsx, jsxs, Fragment } from 'preact/jsx-runtime';
import { B as BaseComponent, f as afterSize, h as ViewContainer, g as generateClassName, s as setRef, d as getIsHeightAuto, j as getTableHeaderSticky, S as Scroller, k as getFooterScrollbarSticky, l as getScrollerSyncerClass, w as watchHeight, a as memoize, n as NowTimer } from './fa7e597f.js';
import { R as RefMap, c as computeTopFromDate, D as DayGridRows, a as Ruler, b as computeColWidth, d as DayGridHeaderRow, e as dayMicroWidth, f as DayTableSlicer, T as TableDateProfileGenerator, g as buildDayTableModel, h as buildDateRowConfigs, i as createDayHeaderFormatter } from './bd0d9fb1.js';
import { j as joinClassNames } from './423b7bc6.js';
import { createRef, createElement } from 'preact/compat';
import { I as computeViewBorderless, F as classNames } from './69b11357.js';
/*
TODO: kill this class in favor of DayGridHeaderRows?
*/
class DayGridHeader extends BaseComponent {
render() {
const { props } = this;
const { headerTiers } = props;
return (jsx("div", { role: 'rowgroup', className: joinClassNames(props.className, classNames.flexCol, props.width == null && classNames.liquid), style: {
width: props.width,
}, children: headerTiers.map((rowConfig, i) => (createElement(DayGridHeaderRow, { ...rowConfig, key: i, role: 'row', borderBottom: i < headerTiers.length - 1, colWidth: props.colWidth, viewportWidth: props.viewportWidth, cellIsNarrow: props.cellIsNarrow, cellIsMicro: props.cellIsMicro, rowLevel: headerTiers.length - i - 1 }))) }));
}
}
class DayGridLayoutNormal extends BaseComponent {
constructor() {
super(...arguments);
this.state = {};
this.handleScroller = (scroller) => {
setRef(this.props.scrollerRef, scroller);
};
this.handleTotalWidth = (totalWidth) => {
if (this._isUnmounting)
return;
this.setState({ totalWidth });
};
this.handleClientWidth = (clientWidth) => {
if (this._isUnmounting)
return;
this.setState({ clientWidth });
};
}
render() {
const { props, state, context } = this;
const { options } = context;
const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
const { totalWidth, clientWidth } = state;
let endScrollbarWidth = (totalWidth != null && clientWidth != null)
? totalWidth - clientWidth
: undefined;
// HACK when clientWidth does NOT include body-border, compared to totalWidth
if (endScrollbarWidth < 3) {
endScrollbarWidth = 0;
}
const verticalScrollbars = !props.forPrint && !getIsHeightAuto(options);
const tableHeaderSticky = !props.forPrint && getTableHeaderSticky(options);
const colCount = props.cellRows[0].length;
const cellWidth = clientWidth != null ? clientWidth / colCount : undefined;
const cellIsMicro = cellWidth != null && cellWidth <= dayMicroWidth;
const cellIsNarrow = cellIsMicro || (cellWidth != null && cellWidth <= options.dayNarrowWidth);
return (jsxs(Fragment, { children: [options.dayHeaders && (jsxs("div", { className: joinClassNames(generateClassName(options.tableHeaderClass, {
isSticky: tableHeaderSticky,
borderlessX,
borderlessTop,
borderlessBottom,
multiMonthColumns: 0,
}), classNames.printHeader, // either flexCol or table-header-group
tableHeaderSticky && classNames.tableHeaderSticky), children: [jsxs("div", { className: classNames.flexRow, children: [jsx(DayGridHeader, { headerTiers: props.headerTiers, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro }), Boolean(endScrollbarWidth) && (jsx("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: true }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }), jsx("div", { className: generateClassName(options.dayHeaderDividerClass, {
isSticky: tableHeaderSticky,
multiMonthColumns: 0,
options: { allDaySlot: Boolean(options.allDaySlot) },
}) })] })), jsx(Scroller, { vertical: verticalScrollbars, className: joinClassNames(generateClassName(options.tableBodyClass, {
borderlessX,
borderlessTop,
borderlessBottom,
multiMonthColumns: 0,
}),
// 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, verticalScrollbars && classNames.liquid), ref: this.handleScroller, clientWidthRef: this.handleClientWidth, children: jsx(DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: props.cellRows, forPrint: props.forPrint, isHitComboAllowed: props.isHitComboAllowed, className: classNames.grow, dayMaxEvents: props.forPrint ? undefined : options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows,
// content
fgEventSegs: props.fgEventSegs, bgEventSegs: props.bgEventSegs, businessHourSegs: props.businessHourSegs, dateSelectionSegs: props.dateSelectionSegs, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection,
// dimensions
visibleWidth: totalWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro,
// refs
rowHeightRefMap: props.rowHeightRefMap }) }), jsx(Ruler, { widthRef: this.handleTotalWidth })] }));
}
componentDidMount() {
this._isUnmounting = false;
}
componentWillUnmount() {
this._isUnmounting = true;
}
}
class FooterScrollbar extends BaseComponent {
constructor() {
super(...arguments);
this.rootElRef = createRef();
}
render() {
const { props } = this;
// NOTE: we need a wrapper around the Scroller because if scrollbars appear/hide,
// the outer dimensions change, but the inner dimensions do not. The Scroller's
// dimension-watching, when used in ponyfill-mode, can't fire on border-box change, so we
// workaround it by monitoring dimensions of a wrapper instead
return (jsx("div", { ref: this.rootElRef, className: joinClassNames(classNames.footerScrollbar, props.isSticky && classNames.footerScrollbarSticky), children: jsx(Scroller, { horizontal: true, ref: props.scrollerRef, children: jsx("div", { style: { minWidth: props.canvasWidth } }) }) }));
}
componentDidMount() {
this._isUnmounting = false;
this.disconnectHeight = watchHeight(this.rootElRef.current, (height) => {
if (this._isUnmounting)
return;
setRef(this.props.scrollbarWidthRef, height);
});
}
componentWillUnmount() {
this._isUnmounting = true;
this.disconnectHeight();
setRef(this.props.scrollbarWidthRef, null);
}
}
class DayGridLayoutPannable extends BaseComponent {
constructor() {
super(...arguments);
this.state = {};
this.headerScrollerRef = createRef();
this.bodyScrollerRef = createRef();
this.footerScrollerRef = createRef();
// Sizing
// -----------------------------------------------------------------------------------------------
this.handleTotalWidth = (totalWidth) => {
if (this._isUnmounting)
return;
this.setState({ totalWidth });
};
this.handleClientWidth = (clientWidth) => {
if (this._isUnmounting)
return;
this.setState({ clientWidth });
};
}
render() {
const { props, state, context } = this;
const { options } = context;
const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
const { totalWidth, clientWidth } = state;
const endScrollbarWidth = (totalWidth != null && clientWidth != null)
? totalWidth - clientWidth
: undefined;
const verticalScrollbars = !props.forPrint && !getIsHeightAuto(options);
const tableHeaderSticky = !props.forPrint && getTableHeaderSticky(options);
const footerScrollbarSticky = !props.forPrint && getFooterScrollbarSticky(options);
const colCount = props.cellRows[0].length;
const [canvasWidth, colWidth] = computeColWidth(colCount, props.dayMinWidth, clientWidth);
const cellIsMicro = colWidth != null && colWidth <= dayMicroWidth;
const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth);
return (jsxs(Fragment, { children: [options.dayHeaders && (jsxs("div", { className: joinClassNames(generateClassName(options.tableHeaderClass, {
isSticky: tableHeaderSticky,
borderlessX,
borderlessTop,
borderlessBottom,
multiMonthColumns: 0,
}), classNames.printHeader, // either flexCol or table-header-group
tableHeaderSticky && classNames.tableHeaderSticky), children: [jsxs(Scroller, { horizontal: true, hideScrollbars: true, className: classNames.flexRow, ref: this.headerScrollerRef, children: [jsx(DayGridHeader, { headerTiers: props.headerTiers, colWidth: colWidth, viewportWidth: clientWidth, width: canvasWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro }), Boolean(endScrollbarWidth) && (jsx("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: true }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }), jsx("div", { className: generateClassName(options.dayHeaderDividerClass, {
isSticky: tableHeaderSticky,
multiMonthColumns: 0,
options: { allDaySlot: Boolean(options.allDaySlot) },
}) })] })), jsx(Scroller, { vertical: verticalScrollbars, horizontal: true, hideScrollbars: footerScrollbarSticky ||
props.forPrint // prevents blank space in print-view on Safari
, className: joinClassNames(generateClassName(options.tableBodyClass, {
borderlessX,
borderlessTop,
borderlessBottom,
multiMonthColumns: 0,
}),
// 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, verticalScrollbars && classNames.liquid), ref: this.bodyScrollerRef, clientWidthRef: this.handleClientWidth, children: jsx(DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: props.cellRows, forPrint: props.forPrint, isHitComboAllowed: props.isHitComboAllowed, className: classNames.grow, dayMaxEvents: props.forPrint ? undefined : options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows,
// content
fgEventSegs: props.fgEventSegs, bgEventSegs: props.bgEventSegs, businessHourSegs: props.businessHourSegs, dateSelectionSegs: props.dateSelectionSegs, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection,
// dimensions
colWidth: colWidth, width: canvasWidth, visibleWidth: totalWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro,
// refs
rowHeightRefMap: props.rowHeightRefMap }) }), Boolean(footerScrollbarSticky) && (jsx(FooterScrollbar, { isSticky: true, canvasWidth: canvasWidth, scrollerRef: this.footerScrollerRef })), jsx(Ruler, { widthRef: this.handleTotalWidth })] }));
}
// Lifecycle
// -----------------------------------------------------------------------------------------------
componentDidMount() {
this._isUnmounting = false;
// scroller
const ScrollerSyncer = getScrollerSyncerClass(this.context.pluginHooks);
this.syncedScroller = new ScrollerSyncer(true); // horizontal=true
setRef(this.props.scrollerRef, this.syncedScroller);
this.updateSyncedScroller();
}
componentDidUpdate() {
// scroller
this.updateSyncedScroller();
}
componentWillUnmount() {
this._isUnmounting = true;
// scroller
this.syncedScroller.destroy();
}
// Scrolling
// -----------------------------------------------------------------------------------------------
updateSyncedScroller() {
this.syncedScroller.handleChildren([
this.headerScrollerRef.current,
this.bodyScrollerRef.current,
this.footerScrollerRef.current,
]);
}
}
class DayGridLayout extends BaseComponent {
constructor() {
super(...arguments);
// ref
this.scrollerRef = createRef();
this.rowHeightRefMap = new RefMap(() => {
afterSize(this.updateScrollY);
});
this.scrollDate = null;
this.updateScrollY = () => {
if (this._isUnmounting)
return;
const rowHeightMap = this.rowHeightRefMap.current;
const scroller = this.scrollerRef.current;
// Since updateScrollY is called by rowHeightRefMap, could be called with null during cleanup,
// and the scroller might not exist
if (scroller && this.scrollDate) {
let scrollTop = computeTopFromDate(this.scrollDate, this.props.cellRows, rowHeightMap);
if (scrollTop != null) {
if (scrollTop) {
scrollTop++; // clear top border
}
scroller.scrollTo({ y: scrollTop });
}
}
};
this.handleScrollEnd = (isDevice) => {
if (isDevice) {
this.scrollDate = null;
}
};
}
render() {
const { props, context } = this;
const { options } = context;
const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
const businessHourSegs = props.forPrint ? [] : props.businessHourSegs;
const dateSelectionSegs = props.forPrint ? [] : props.dateSelectionSegs;
const eventDrag = props.forPrint ? null : props.eventDrag;
const eventResize = props.forPrint ? null : props.eventResize;
const commonLayoutProps = {
...props,
businessHourSegs,
dateSelectionSegs,
eventDrag,
eventResize,
scrollerRef: this.scrollerRef,
rowHeightRefMap: this.rowHeightRefMap,
};
return (jsx(ViewContainer, { viewSpec: context.viewSpec, attrs: {
role: 'grid',
'aria-rowcount': props.headerTiers.length + props.cellRows.length,
'aria-colcount': props.cellRows[0].length,
'aria-labelledby': props.labelId,
'aria-label': props.labelStr,
}, className: joinClassNames(props.className, classNames.printRoot, // either flexCol or table
generateClassName(options.tableClass, {
borderlessX,
borderlessTop,
borderlessBottom,
multiMonthColumns: 0,
})), children: options.dayMinWidth ? (jsx(DayGridLayoutPannable, { ...commonLayoutProps, dayMinWidth: options.dayMinWidth })) : (jsx(DayGridLayoutNormal, { ...commonLayoutProps })) }));
}
// Lifecycle
// -----------------------------------------------------------------------------------------------
componentDidMount() {
this._isUnmounting = false;
this.resetScroll();
this.scrollerRef.current.addScrollEndListener(this.handleScrollEnd);
}
componentDidUpdate(prevProps) {
if (prevProps.dateProfile !== this.props.dateProfile && this.context.options.scrollTimeReset) {
this.resetScroll();
}
}
componentWillUnmount() {
this._isUnmounting = true;
this.scrollerRef.current.removeScrollEndListener(this.handleScrollEnd);
}
// Scrolling
// -----------------------------------------------------------------------------------------------
resetScroll() {
this.scrollDate = this.props.dateProfile.currentDate;
this.updateScrollY();
const scroller = this.scrollerRef.current;
scroller.scrollTo({ x: 0 });
}
}
class DayGridView extends BaseComponent {
constructor() {
super(...arguments);
// memo
this.buildDayTableModel = memoize(buildDayTableModel);
this.buildDateRowConfigs = memoize(buildDateRowConfigs);
this.createDayHeaderFormatter = memoize(createDayHeaderFormatter);
// internal
this.slicer = new DayTableSlicer();
}
render() {
const { props, context } = this;
const { dateProfile } = props;
const { options, dateEnv } = context;
const dayTableModel = this.buildDayTableModel(dateProfile, context.dateProfileGenerator, dateEnv);
const datesRepDistinctDays = dayTableModel.rowCount === 1;
const dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, datesRepDistinctDays, dayTableModel.colCount);
const slicedProps = this.slicer.sliceProps(props, dateProfile, options.nextDayThreshold, context, dayTableModel);
return (jsx(NowTimer, { unit: "day", children: (nowDate, todayRange) => {
const headerTiers = this.buildDateRowConfigs(dayTableModel.headerDates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context);
return (jsx(DayGridLayout, { labelId: props.labelId, labelStr: props.labelStr, dateProfile: dateProfile, todayRange: todayRange, cellRows: dayTableModel.cellRows, forPrint: props.forPrint, className: props.className,
// header content
headerTiers: headerTiers,
// body content
fgEventSegs: slicedProps.fgEventSegs, bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: slicedProps.businessHourSegs, dateSelectionSegs: slicedProps.dateSelectionSegs, eventDrag: slicedProps.eventDrag, eventResize: slicedProps.eventResize, eventSelection: slicedProps.eventSelection }));
} }));
}
}
var dayGridPlugin = {
name: 'daygrid',
initialView: 'dayGridMonth',
views: {
dayGrid: {
component: DayGridView,
dateProfileGeneratorClass: TableDateProfileGenerator,
},
dayGridDay: {
type: 'dayGrid',
duration: { days: 1 },
},
dayGridWeek: {
type: 'dayGrid',
duration: { weeks: 1 },
},
dayGridMonth: {
type: 'dayGrid',
duration: { months: 1 },
fixedWeekCount: true,
},
dayGridYear: {
type: 'dayGrid',
duration: { years: 1 },
},
},
};
export { FooterScrollbar as F, dayGridPlugin as d };