@fullcalendar/multimonth 7.0.0-beta.3 → 7.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/index.js ADDED
@@ -0,0 +1,314 @@
1
+ import { joinClassNames, createPlugin } from '@fullcalendar/core';
2
+ import { buildDayTableModel, createDayHeaderFormatter, buildDateRowConfig, DayTableSlicer, DayGridHeaderRow, DayGridRows, dayMicroWidth, buildDayTableRenderRange, TableDateProfileGenerator } from '@fullcalendar/daygrid/internal';
3
+ import { DateComponent, memoize, getUniqueDomId, buildNavLinkAttrs, generateClassName, joinArrayishClassNames, fracToCssDim, watchWidth, watchHeight, getIsHeightAuto, NowTimer, ViewContainer, Scroller, formatIsoMonthStr, afterSize, createDuration, intersectRanges, createFormatter, identity } from '@fullcalendar/core/internal';
4
+ import classNames from '@fullcalendar/core/internal-classnames';
5
+ import { createRef, createElement } from '@fullcalendar/core/preact';
6
+
7
+ class SingleMonth extends DateComponent {
8
+ constructor() {
9
+ super(...arguments);
10
+ // memo
11
+ this.buildDayTableModel = memoize(buildDayTableModel);
12
+ this.createDayHeaderFormatter = memoize(createDayHeaderFormatter);
13
+ this.buildDateRowConfig = memoize(buildDateRowConfig);
14
+ // ref
15
+ this.gridElRef = createRef();
16
+ this.titleElRef = createRef();
17
+ this.tableHeaderElRef = createRef();
18
+ // internal
19
+ this.slicer = new DayTableSlicer();
20
+ this.titleId = getUniqueDomId();
21
+ this.handleEl = (el) => {
22
+ var _a;
23
+ const { options } = this.context;
24
+ if (el) {
25
+ this.rootEl = el;
26
+ (_a = options.singleMonthDidMount) === null || _a === void 0 ? void 0 : _a.call(options, Object.assign({ el: this.rootEl }, this.renderProps));
27
+ }
28
+ };
29
+ }
30
+ render() {
31
+ const { props, state, context } = this;
32
+ const { dateProfile, forPrint } = props;
33
+ const { options, dateEnv } = context;
34
+ const dayTableModel = this.buildDayTableModel(dateProfile, context.dateProfileGenerator, dateEnv);
35
+ const slicedProps = this.slicer.sliceProps(props, dateProfile, options.nextDayThreshold, context, dayTableModel);
36
+ const dayHeaderFormat = this.createDayHeaderFormatter(options.dayHeaderFormat, false, // datesRepDistinctDays
37
+ dayTableModel.colCount);
38
+ const rowConfig = this.buildDateRowConfig(dayTableModel.headerDates, false, // datesRepDistinctDays
39
+ dateProfile, props.todayRange, dayHeaderFormat, context);
40
+ const isTitleAndHeaderSticky = !forPrint && props.colCount === 1;
41
+ const isAspectRatio = !forPrint || props.hasLateralSiblings;
42
+ const invAspectRatio = 1 / options.aspectRatio;
43
+ const cellColCnt = dayTableModel.cellRows[0].length;
44
+ const colWidth = state.gridWidth != null ? state.gridWidth / cellColCnt : undefined;
45
+ const cellIsMicro = colWidth != null && colWidth <= dayMicroWidth;
46
+ const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth);
47
+ const rowHeightGuess = state.gridWidth != null
48
+ ? invAspectRatio * state.gridWidth / 6
49
+ : undefined;
50
+ const headerStickyBottom = isTitleAndHeaderSticky
51
+ ? rowHeightGuess
52
+ : undefined;
53
+ const titleStickyBottom = isTitleAndHeaderSticky && rowHeightGuess != null && state.tableHeaderHeight != null
54
+ ? rowHeightGuess + state.tableHeaderHeight + 1
55
+ : undefined;
56
+ const hasNavLink = options.navLinks && props.colCount > 1;
57
+ const headerRenderProps = {
58
+ colCount: props.colCount,
59
+ isSticky: isTitleAndHeaderSticky,
60
+ isNarrow: cellIsNarrow,
61
+ hasNavLink,
62
+ };
63
+ const monthStartDate = props.dateProfile.currentRange.start;
64
+ const navLinkAttrs = hasNavLink
65
+ ? buildNavLinkAttrs(context, monthStartDate, 'month', props.isoDateStr)
66
+ : {};
67
+ return (createElement("div", { role: 'listitem', style: { width: props.width } },
68
+ createElement("div", { ref: this.gridElRef, role: 'grid', "aria-labelledby": this.titleId, "data-date": props.isoDateStr, className: joinClassNames(generateClassName(options.singleMonthClass, {
69
+ colCount: props.colCount || 0,
70
+ }), props.borderlessX && classNames.borderlessX, props.borderlessTop && classNames.borderlessTop, props.borderlessBottom && classNames.borderlessBottom, props.colCount === 1 && classNames.noMargin, classNames.flexCol, props.hasLateralSiblings && classNames.breakInsideAvoid) },
71
+ createElement("div", { id: this.titleId, ref: this.titleElRef, className: joinClassNames(generateClassName(options.singleMonthHeaderClass, headerRenderProps), isTitleAndHeaderSticky && classNames.stickyT, classNames.flexCol), style: {
72
+ // HACK to keep zIndex above table-header,
73
+ // because in Chrome, something about position:sticky on this title div
74
+ // causes its bottom border to no be considered part of its mass,
75
+ // and would get overlapped and hidden by the table-header div
76
+ zIndex: isTitleAndHeaderSticky ? 3 : undefined,
77
+ marginBottom: titleStickyBottom,
78
+ } },
79
+ createElement("div", Object.assign({}, navLinkAttrs, { className: joinClassNames(generateClassName(options.singleMonthHeaderInnerClass, headerRenderProps), navLinkAttrs.className) }), dateEnv.format(monthStartDate, props.titleFormat))),
80
+ createElement("div", { className: joinArrayishClassNames(options.tableClass, props.borderlessX && classNames.borderlessX, isTitleAndHeaderSticky && classNames.borderlessTop, props.borderlessBottom && classNames.borderlessBottom, classNames.flexCol), style: {
81
+ marginTop: titleStickyBottom != null ? -titleStickyBottom : undefined,
82
+ } },
83
+ createElement("div", { ref: this.tableHeaderElRef, className: joinClassNames(generateClassName(options.tableHeaderClass, {
84
+ isSticky: isTitleAndHeaderSticky,
85
+ }), props.borderlessX && classNames.borderlessX, classNames.flexCol, isTitleAndHeaderSticky && classNames.sticky), style: {
86
+ zIndex: isTitleAndHeaderSticky ? 2 : undefined,
87
+ top: isTitleAndHeaderSticky ? state.titleHeight : 0,
88
+ marginBottom: headerStickyBottom,
89
+ } },
90
+ createElement(DayGridHeaderRow, Object.assign({}, rowConfig, { role: 'row', borderBottom: false, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowLevel: 0 })),
91
+ createElement("div", { className: generateClassName(options.dayHeaderDividerClass, {
92
+ isSticky: isTitleAndHeaderSticky,
93
+ options: { allDaySlot: Boolean(options.allDaySlot) },
94
+ }) })),
95
+ createElement("div", { className: joinArrayishClassNames(options.tableBodyClass, classNames.flexCol, isAspectRatio && classNames.rel, props.borderlessX && classNames.borderlessX, isTitleAndHeaderSticky && classNames.borderlessTop, (isTitleAndHeaderSticky || props.noEdgeEffects) && classNames.noEdgeEffects), style: {
96
+ zIndex: isTitleAndHeaderSticky ? 1 : undefined,
97
+ marginTop: headerStickyBottom != null ? -headerStickyBottom : undefined,
98
+ paddingBottom: isAspectRatio ? fracToCssDim(invAspectRatio) : undefined,
99
+ } },
100
+ createElement(DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: dayTableModel.cellRows, className: isAspectRatio ? classNames.fill : '', forPrint: forPrint && !props.hasLateralSiblings, dayMaxEventRows: (forPrint && props.hasLateralSiblings)
101
+ ? 1 // for side-by-side multimonths, limit to one row
102
+ : true // otherwise, always do +more link, never expand rows
103
+ ,
104
+ // content
105
+ fgEventSegs: slicedProps.fgEventSegs, bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: slicedProps.businessHourSegs, dateSelectionSegs: slicedProps.dateSelectionSegs, eventDrag: slicedProps.eventDrag, eventResize: slicedProps.eventResize, eventSelection: slicedProps.eventSelection,
106
+ // dimensions
107
+ visibleWidth: state.gridWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro }))))));
108
+ }
109
+ componentDidMount() {
110
+ this.disconnectGridWidth = watchWidth(this.gridElRef.current, (width) => {
111
+ this.setState({ gridWidth: width });
112
+ });
113
+ this.disconnectTitleHeight = watchHeight(this.titleElRef.current, (height) => {
114
+ this.setState({ titleHeight: height });
115
+ });
116
+ this.disconnectTableHeaderHeight = watchHeight(this.tableHeaderElRef.current, (height) => {
117
+ this.setState({ tableHeaderHeight: height });
118
+ });
119
+ }
120
+ componentWillUnmount() {
121
+ var _a;
122
+ const { options } = this.context;
123
+ this.disconnectGridWidth();
124
+ this.disconnectTitleHeight();
125
+ this.disconnectTableHeaderHeight();
126
+ (_a = options.singleMonthWillUnmount) === null || _a === void 0 ? void 0 : _a.call(options, Object.assign({ el: this.rootEl }, this.renderProps));
127
+ }
128
+ }
129
+
130
+ class MultiMonthView extends DateComponent {
131
+ constructor() {
132
+ super(...arguments);
133
+ // memo
134
+ this.splitDateProfileByMonth = memoize(splitDateProfileByMonth);
135
+ this.buildMonthFormat = memoize(buildMonthFormat);
136
+ // ref
137
+ this.scrollerRef = createRef();
138
+ this.innerElRef = createRef();
139
+ this.scrollDate = null;
140
+ this.handleScrollEnd = (isUser) => {
141
+ if (isUser) {
142
+ this.scrollDate = null;
143
+ }
144
+ };
145
+ }
146
+ render() {
147
+ const { context, props, state } = this;
148
+ const { options } = context;
149
+ const verticalScrolling = !props.forPrint && !getIsHeightAuto(options);
150
+ const monthDateProfiles = this.splitDateProfileByMonth(context.dateProfileGenerator, props.dateProfile, context.dateEnv, options.fixedWeekCount, options.showNonCurrentDates);
151
+ const monthTitleFormat = this.buildMonthFormat(options.singleMonthTitleFormat, monthDateProfiles);
152
+ const { multiMonthMaxColumns, singleMonthMinWidth } = options;
153
+ const { innerWidth } = state;
154
+ let cols;
155
+ let cssMonthWidth;
156
+ let hasLateralSiblings = false;
157
+ if (innerWidth != null) {
158
+ cols = Math.max(1, Math.min(multiMonthMaxColumns, Math.floor(innerWidth / singleMonthMinWidth)));
159
+ if (props.forPrint) {
160
+ cols = Math.min(cols, 2);
161
+ }
162
+ cssMonthWidth = fracToCssDim(1 / cols);
163
+ hasLateralSiblings = cols > 1;
164
+ }
165
+ return (createElement(NowTimer, { unit: "day" }, (nowDate, todayRange) => (createElement(ViewContainer, { viewSpec: context.viewSpec, className: joinClassNames(
166
+ // HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
167
+ // https://stackoverflow.com/a/60256345
168
+ !props.forPrint && classNames.flexCol, props.className), borderlessX: props.borderlessX, borderlessTop: props.borderlessTop, borderlessBottom: props.borderlessBottom, noEdgeEffects: props.noEdgeEffects },
169
+ createElement(Scroller, { vertical: verticalScrolling, className: verticalScrolling ? classNames.liquid : '', ref: this.scrollerRef },
170
+ createElement("div", { role: 'list', "aria-labelledby": props.labelId, "aria-label": props.labelStr, className: classNames.safeTiles, ref: this.innerElRef }, monthDateProfiles.map((monthDateProfile, i) => {
171
+ const monthStr = formatIsoMonthStr(monthDateProfile.currentRange.start);
172
+ return (createElement(SingleMonth, Object.assign({}, props, { key: monthStr, todayRange: todayRange, isoDateStr: monthStr, titleFormat: monthTitleFormat, dateProfile: monthDateProfile, width: cssMonthWidth, colCount: cols,
173
+ // when single-col, kill X border on all items
174
+ borderlessX: cols === 1,
175
+ // when single-col, kill top border on all items
176
+ borderlessTop: cols === 1,
177
+ // when single-col, kill bottom border on last item
178
+ borderlessBottom: cols === 1 && i === monthDateProfiles.length - 1, hasLateralSiblings: hasLateralSiblings })));
179
+ })))))));
180
+ }
181
+ // Lifecycle
182
+ // -----------------------------------------------------------------------------------------------
183
+ componentDidMount() {
184
+ this.resetScroll();
185
+ this.scrollerRef.current.addScrollEndListener(this.handleScrollEnd);
186
+ this.disconnectInnerWidth = watchWidth(this.innerElRef.current, (innerWidth) => {
187
+ afterSize(() => {
188
+ this.setState({ innerWidth });
189
+ });
190
+ });
191
+ }
192
+ componentDidUpdate(prevProps) {
193
+ if (prevProps.dateProfile !== this.props.dateProfile && this.context.options.scrollTimeReset) {
194
+ this.resetScroll();
195
+ }
196
+ else {
197
+ // NOT optimal to update so often
198
+ // TODO: isolate dependencies of scroll coordinate
199
+ this.applyScroll();
200
+ }
201
+ }
202
+ componentWillUnmount() {
203
+ this.scrollerRef.current.removeScrollEndListener(this.handleScrollEnd);
204
+ this.disconnectInnerWidth();
205
+ }
206
+ // Scrolling
207
+ // -----------------------------------------------------------------------------------------------
208
+ resetScroll() {
209
+ this.scrollDate = this.props.dateProfile.currentDate;
210
+ this.applyScroll();
211
+ }
212
+ applyScroll() {
213
+ if (this.scrollDate != null &&
214
+ this.state.innerWidth != null // render completed?
215
+ ) {
216
+ const scroller = this.scrollerRef.current;
217
+ const innerEl = this.innerElRef.current;
218
+ const monthEl = innerEl.querySelector(`[data-date="${formatIsoMonthStr(this.scrollDate)}"]`);
219
+ const scrollTop = Math.ceil(// for fractions, err on the side of scrolling further
220
+ monthEl.parentNode.getBoundingClientRect().top -
221
+ innerEl.getBoundingClientRect().top);
222
+ scroller.scrollTo({ y: scrollTop });
223
+ }
224
+ }
225
+ }
226
+ // date profile
227
+ // -------------------------------------------------------------------------------------------------
228
+ const oneMonthDuration = createDuration(1, 'month');
229
+ function splitDateProfileByMonth(dateProfileGenerator, dateProfile, dateEnv, fixedWeekCount, showNonCurrentDates) {
230
+ const { start, end } = dateProfile.currentRange;
231
+ let monthStart = start;
232
+ const monthDateProfiles = [];
233
+ while (monthStart.valueOf() < end.valueOf()) {
234
+ const monthEnd = dateEnv.add(monthStart, oneMonthDuration);
235
+ const currentRange = {
236
+ // yuck
237
+ start: dateProfileGenerator.skipHiddenDays(monthStart),
238
+ end: dateProfileGenerator.skipHiddenDays(monthEnd, -1, true),
239
+ };
240
+ let renderRange = buildDayTableRenderRange({
241
+ currentRange,
242
+ snapToWeek: true,
243
+ fixedWeekCount,
244
+ dateEnv,
245
+ });
246
+ renderRange = {
247
+ // yuck
248
+ start: dateProfileGenerator.skipHiddenDays(renderRange.start),
249
+ end: dateProfileGenerator.skipHiddenDays(renderRange.end, -1, true),
250
+ };
251
+ const activeRange = dateProfile.activeRange ?
252
+ intersectRanges(dateProfile.activeRange, showNonCurrentDates ? renderRange : currentRange) :
253
+ null;
254
+ monthDateProfiles.push({
255
+ currentDate: dateProfile.currentDate,
256
+ isValid: dateProfile.isValid,
257
+ validRange: dateProfile.validRange,
258
+ renderRange,
259
+ activeRange,
260
+ currentRange,
261
+ currentRangeUnit: 'month',
262
+ isRangeAllDay: true,
263
+ dateIncrement: dateProfile.dateIncrement,
264
+ slotMinTime: dateProfile.slotMaxTime,
265
+ slotMaxTime: dateProfile.slotMinTime,
266
+ });
267
+ monthStart = monthEnd;
268
+ }
269
+ return monthDateProfiles;
270
+ }
271
+ // date formatting
272
+ // -------------------------------------------------------------------------------------------------
273
+ const YEAR_MONTH_FORMATTER = createFormatter({ year: 'numeric', month: 'long' });
274
+ const YEAR_FORMATTER = createFormatter({ month: 'long' });
275
+ function buildMonthFormat(formatOverride, monthDateProfiles) {
276
+ return formatOverride ||
277
+ ((monthDateProfiles[0].currentRange.start.getUTCFullYear() !==
278
+ monthDateProfiles[monthDateProfiles.length - 1].currentRange.start.getUTCFullYear())
279
+ ? YEAR_MONTH_FORMATTER
280
+ : YEAR_FORMATTER);
281
+ }
282
+
283
+ const OPTION_REFINERS = {
284
+ multiMonthMaxColumns: Number,
285
+ singleMonthMinWidth: Number,
286
+ singleMonthTitleFormat: createFormatter,
287
+ singleMonthDidMount: identity,
288
+ singleMonthWillUnmount: identity,
289
+ singleMonthClass: identity,
290
+ singleMonthHeaderClass: identity,
291
+ singleMonthHeaderInnerClass: identity,
292
+ };
293
+
294
+ var index = createPlugin({
295
+ name: '@fullcalendar/multimonth',
296
+ initialView: 'multiMonthYear',
297
+ optionRefiners: OPTION_REFINERS,
298
+ views: {
299
+ multiMonth: {
300
+ component: MultiMonthView,
301
+ dateProfileGeneratorClass: TableDateProfileGenerator,
302
+ multiMonthMaxColumns: 3,
303
+ singleMonthMinWidth: 350,
304
+ },
305
+ multiMonthYear: {
306
+ type: 'multiMonth',
307
+ duration: { years: 1 },
308
+ fixedWeekCount: true,
309
+ showNonCurrentDates: false, // TODO: looks bad when single-col layout
310
+ },
311
+ },
312
+ });
313
+
314
+ export { index as default };
package/global.js ADDED
@@ -0,0 +1,328 @@
1
+ /*!
2
+ FullCalendar Multi-Month Plugin v7.0.0-beta.5
3
+ Docs & License: https://fullcalendar.io/docs/multimonth-grid
4
+ (c) 2025 Adam Shaw
5
+ */
6
+ FullCalendar.MultiMonth = (function (exports, core, internal$1, internal, classNames, preact) {
7
+ 'use strict';
8
+
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
10
+
11
+ var classNames__default = /*#__PURE__*/_interopDefault(classNames);
12
+
13
+ class SingleMonth extends internal.DateComponent {
14
+ constructor() {
15
+ super(...arguments);
16
+ // memo
17
+ this.buildDayTableModel = internal.memoize(internal$1.buildDayTableModel);
18
+ this.createDayHeaderFormatter = internal.memoize(internal$1.createDayHeaderFormatter);
19
+ this.buildDateRowConfig = internal.memoize(internal$1.buildDateRowConfig);
20
+ // ref
21
+ this.gridElRef = preact.createRef();
22
+ this.titleElRef = preact.createRef();
23
+ this.tableHeaderElRef = preact.createRef();
24
+ // internal
25
+ this.slicer = new internal$1.DayTableSlicer();
26
+ this.titleId = internal.getUniqueDomId();
27
+ this.handleEl = (el) => {
28
+ var _a;
29
+ const { options } = this.context;
30
+ if (el) {
31
+ this.rootEl = el;
32
+ (_a = options.singleMonthDidMount) === null || _a === void 0 ? void 0 : _a.call(options, Object.assign({ el: this.rootEl }, this.renderProps));
33
+ }
34
+ };
35
+ }
36
+ render() {
37
+ const { props, state, context } = this;
38
+ const { dateProfile, forPrint } = props;
39
+ const { options, dateEnv } = context;
40
+ const dayTableModel = this.buildDayTableModel(dateProfile, context.dateProfileGenerator, dateEnv);
41
+ const slicedProps = this.slicer.sliceProps(props, dateProfile, options.nextDayThreshold, context, dayTableModel);
42
+ const dayHeaderFormat = this.createDayHeaderFormatter(options.dayHeaderFormat, false, // datesRepDistinctDays
43
+ dayTableModel.colCount);
44
+ const rowConfig = this.buildDateRowConfig(dayTableModel.headerDates, false, // datesRepDistinctDays
45
+ dateProfile, props.todayRange, dayHeaderFormat, context);
46
+ const isTitleAndHeaderSticky = !forPrint && props.colCount === 1;
47
+ const isAspectRatio = !forPrint || props.hasLateralSiblings;
48
+ const invAspectRatio = 1 / options.aspectRatio;
49
+ const cellColCnt = dayTableModel.cellRows[0].length;
50
+ const colWidth = state.gridWidth != null ? state.gridWidth / cellColCnt : undefined;
51
+ const cellIsMicro = colWidth != null && colWidth <= internal$1.dayMicroWidth;
52
+ const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth);
53
+ const rowHeightGuess = state.gridWidth != null
54
+ ? invAspectRatio * state.gridWidth / 6
55
+ : undefined;
56
+ const headerStickyBottom = isTitleAndHeaderSticky
57
+ ? rowHeightGuess
58
+ : undefined;
59
+ const titleStickyBottom = isTitleAndHeaderSticky && rowHeightGuess != null && state.tableHeaderHeight != null
60
+ ? rowHeightGuess + state.tableHeaderHeight + 1
61
+ : undefined;
62
+ const hasNavLink = options.navLinks && props.colCount > 1;
63
+ const headerRenderProps = {
64
+ colCount: props.colCount,
65
+ isSticky: isTitleAndHeaderSticky,
66
+ isNarrow: cellIsNarrow,
67
+ hasNavLink,
68
+ };
69
+ const monthStartDate = props.dateProfile.currentRange.start;
70
+ const navLinkAttrs = hasNavLink
71
+ ? internal.buildNavLinkAttrs(context, monthStartDate, 'month', props.isoDateStr)
72
+ : {};
73
+ return (preact.createElement("div", { role: 'listitem', style: { width: props.width } },
74
+ preact.createElement("div", { ref: this.gridElRef, role: 'grid', "aria-labelledby": this.titleId, "data-date": props.isoDateStr, className: core.joinClassNames(internal.generateClassName(options.singleMonthClass, {
75
+ colCount: props.colCount || 0,
76
+ }), props.borderlessX && classNames__default["default"].borderlessX, props.borderlessTop && classNames__default["default"].borderlessTop, props.borderlessBottom && classNames__default["default"].borderlessBottom, props.colCount === 1 && classNames__default["default"].noMargin, classNames__default["default"].flexCol, props.hasLateralSiblings && classNames__default["default"].breakInsideAvoid) },
77
+ preact.createElement("div", { id: this.titleId, ref: this.titleElRef, className: core.joinClassNames(internal.generateClassName(options.singleMonthHeaderClass, headerRenderProps), isTitleAndHeaderSticky && classNames__default["default"].stickyT, classNames__default["default"].flexCol), style: {
78
+ // HACK to keep zIndex above table-header,
79
+ // because in Chrome, something about position:sticky on this title div
80
+ // causes its bottom border to no be considered part of its mass,
81
+ // and would get overlapped and hidden by the table-header div
82
+ zIndex: isTitleAndHeaderSticky ? 3 : undefined,
83
+ marginBottom: titleStickyBottom,
84
+ } },
85
+ preact.createElement("div", Object.assign({}, navLinkAttrs, { className: core.joinClassNames(internal.generateClassName(options.singleMonthHeaderInnerClass, headerRenderProps), navLinkAttrs.className) }), dateEnv.format(monthStartDate, props.titleFormat))),
86
+ preact.createElement("div", { className: internal.joinArrayishClassNames(options.tableClass, props.borderlessX && classNames__default["default"].borderlessX, isTitleAndHeaderSticky && classNames__default["default"].borderlessTop, props.borderlessBottom && classNames__default["default"].borderlessBottom, classNames__default["default"].flexCol), style: {
87
+ marginTop: titleStickyBottom != null ? -titleStickyBottom : undefined,
88
+ } },
89
+ preact.createElement("div", { ref: this.tableHeaderElRef, className: core.joinClassNames(internal.generateClassName(options.tableHeaderClass, {
90
+ isSticky: isTitleAndHeaderSticky,
91
+ }), props.borderlessX && classNames__default["default"].borderlessX, classNames__default["default"].flexCol, isTitleAndHeaderSticky && classNames__default["default"].sticky), style: {
92
+ zIndex: isTitleAndHeaderSticky ? 2 : undefined,
93
+ top: isTitleAndHeaderSticky ? state.titleHeight : 0,
94
+ marginBottom: headerStickyBottom,
95
+ } },
96
+ preact.createElement(internal$1.DayGridHeaderRow, Object.assign({}, rowConfig, { role: 'row', borderBottom: false, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowLevel: 0 })),
97
+ preact.createElement("div", { className: internal.generateClassName(options.dayHeaderDividerClass, {
98
+ isSticky: isTitleAndHeaderSticky,
99
+ options: { allDaySlot: Boolean(options.allDaySlot) },
100
+ }) })),
101
+ preact.createElement("div", { className: internal.joinArrayishClassNames(options.tableBodyClass, classNames__default["default"].flexCol, isAspectRatio && classNames__default["default"].rel, props.borderlessX && classNames__default["default"].borderlessX, isTitleAndHeaderSticky && classNames__default["default"].borderlessTop, (isTitleAndHeaderSticky || props.noEdgeEffects) && classNames__default["default"].noEdgeEffects), style: {
102
+ zIndex: isTitleAndHeaderSticky ? 1 : undefined,
103
+ marginTop: headerStickyBottom != null ? -headerStickyBottom : undefined,
104
+ paddingBottom: isAspectRatio ? internal.fracToCssDim(invAspectRatio) : undefined,
105
+ } },
106
+ preact.createElement(internal$1.DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: dayTableModel.cellRows, className: isAspectRatio ? classNames__default["default"].fill : '', forPrint: forPrint && !props.hasLateralSiblings, dayMaxEventRows: (forPrint && props.hasLateralSiblings)
107
+ ? 1 // for side-by-side multimonths, limit to one row
108
+ : true // otherwise, always do +more link, never expand rows
109
+ ,
110
+ // content
111
+ fgEventSegs: slicedProps.fgEventSegs, bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: slicedProps.businessHourSegs, dateSelectionSegs: slicedProps.dateSelectionSegs, eventDrag: slicedProps.eventDrag, eventResize: slicedProps.eventResize, eventSelection: slicedProps.eventSelection,
112
+ // dimensions
113
+ visibleWidth: state.gridWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro }))))));
114
+ }
115
+ componentDidMount() {
116
+ this.disconnectGridWidth = internal.watchWidth(this.gridElRef.current, (width) => {
117
+ this.setState({ gridWidth: width });
118
+ });
119
+ this.disconnectTitleHeight = internal.watchHeight(this.titleElRef.current, (height) => {
120
+ this.setState({ titleHeight: height });
121
+ });
122
+ this.disconnectTableHeaderHeight = internal.watchHeight(this.tableHeaderElRef.current, (height) => {
123
+ this.setState({ tableHeaderHeight: height });
124
+ });
125
+ }
126
+ componentWillUnmount() {
127
+ var _a;
128
+ const { options } = this.context;
129
+ this.disconnectGridWidth();
130
+ this.disconnectTitleHeight();
131
+ this.disconnectTableHeaderHeight();
132
+ (_a = options.singleMonthWillUnmount) === null || _a === void 0 ? void 0 : _a.call(options, Object.assign({ el: this.rootEl }, this.renderProps));
133
+ }
134
+ }
135
+
136
+ class MultiMonthView extends internal.DateComponent {
137
+ constructor() {
138
+ super(...arguments);
139
+ // memo
140
+ this.splitDateProfileByMonth = internal.memoize(splitDateProfileByMonth);
141
+ this.buildMonthFormat = internal.memoize(buildMonthFormat);
142
+ // ref
143
+ this.scrollerRef = preact.createRef();
144
+ this.innerElRef = preact.createRef();
145
+ this.scrollDate = null;
146
+ this.handleScrollEnd = (isUser) => {
147
+ if (isUser) {
148
+ this.scrollDate = null;
149
+ }
150
+ };
151
+ }
152
+ render() {
153
+ const { context, props, state } = this;
154
+ const { options } = context;
155
+ const verticalScrolling = !props.forPrint && !internal.getIsHeightAuto(options);
156
+ const monthDateProfiles = this.splitDateProfileByMonth(context.dateProfileGenerator, props.dateProfile, context.dateEnv, options.fixedWeekCount, options.showNonCurrentDates);
157
+ const monthTitleFormat = this.buildMonthFormat(options.singleMonthTitleFormat, monthDateProfiles);
158
+ const { multiMonthMaxColumns, singleMonthMinWidth } = options;
159
+ const { innerWidth } = state;
160
+ let cols;
161
+ let cssMonthWidth;
162
+ let hasLateralSiblings = false;
163
+ if (innerWidth != null) {
164
+ cols = Math.max(1, Math.min(multiMonthMaxColumns, Math.floor(innerWidth / singleMonthMinWidth)));
165
+ if (props.forPrint) {
166
+ cols = Math.min(cols, 2);
167
+ }
168
+ cssMonthWidth = internal.fracToCssDim(1 / cols);
169
+ hasLateralSiblings = cols > 1;
170
+ }
171
+ return (preact.createElement(internal.NowTimer, { unit: "day" }, (nowDate, todayRange) => (preact.createElement(internal.ViewContainer, { viewSpec: context.viewSpec, className: core.joinClassNames(
172
+ // HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
173
+ // https://stackoverflow.com/a/60256345
174
+ !props.forPrint && classNames__default["default"].flexCol, props.className), borderlessX: props.borderlessX, borderlessTop: props.borderlessTop, borderlessBottom: props.borderlessBottom, noEdgeEffects: props.noEdgeEffects },
175
+ preact.createElement(internal.Scroller, { vertical: verticalScrolling, className: verticalScrolling ? classNames__default["default"].liquid : '', ref: this.scrollerRef },
176
+ preact.createElement("div", { role: 'list', "aria-labelledby": props.labelId, "aria-label": props.labelStr, className: classNames__default["default"].safeTiles, ref: this.innerElRef }, monthDateProfiles.map((monthDateProfile, i) => {
177
+ const monthStr = internal.formatIsoMonthStr(monthDateProfile.currentRange.start);
178
+ return (preact.createElement(SingleMonth, Object.assign({}, props, { key: monthStr, todayRange: todayRange, isoDateStr: monthStr, titleFormat: monthTitleFormat, dateProfile: monthDateProfile, width: cssMonthWidth, colCount: cols,
179
+ // when single-col, kill X border on all items
180
+ borderlessX: cols === 1,
181
+ // when single-col, kill top border on all items
182
+ borderlessTop: cols === 1,
183
+ // when single-col, kill bottom border on last item
184
+ borderlessBottom: cols === 1 && i === monthDateProfiles.length - 1, hasLateralSiblings: hasLateralSiblings })));
185
+ })))))));
186
+ }
187
+ // Lifecycle
188
+ // -----------------------------------------------------------------------------------------------
189
+ componentDidMount() {
190
+ this.resetScroll();
191
+ this.scrollerRef.current.addScrollEndListener(this.handleScrollEnd);
192
+ this.disconnectInnerWidth = internal.watchWidth(this.innerElRef.current, (innerWidth) => {
193
+ internal.afterSize(() => {
194
+ this.setState({ innerWidth });
195
+ });
196
+ });
197
+ }
198
+ componentDidUpdate(prevProps) {
199
+ if (prevProps.dateProfile !== this.props.dateProfile && this.context.options.scrollTimeReset) {
200
+ this.resetScroll();
201
+ }
202
+ else {
203
+ // NOT optimal to update so often
204
+ // TODO: isolate dependencies of scroll coordinate
205
+ this.applyScroll();
206
+ }
207
+ }
208
+ componentWillUnmount() {
209
+ this.scrollerRef.current.removeScrollEndListener(this.handleScrollEnd);
210
+ this.disconnectInnerWidth();
211
+ }
212
+ // Scrolling
213
+ // -----------------------------------------------------------------------------------------------
214
+ resetScroll() {
215
+ this.scrollDate = this.props.dateProfile.currentDate;
216
+ this.applyScroll();
217
+ }
218
+ applyScroll() {
219
+ if (this.scrollDate != null &&
220
+ this.state.innerWidth != null // render completed?
221
+ ) {
222
+ const scroller = this.scrollerRef.current;
223
+ const innerEl = this.innerElRef.current;
224
+ const monthEl = innerEl.querySelector(`[data-date="${internal.formatIsoMonthStr(this.scrollDate)}"]`);
225
+ const scrollTop = Math.ceil(// for fractions, err on the side of scrolling further
226
+ monthEl.parentNode.getBoundingClientRect().top -
227
+ innerEl.getBoundingClientRect().top);
228
+ scroller.scrollTo({ y: scrollTop });
229
+ }
230
+ }
231
+ }
232
+ // date profile
233
+ // -------------------------------------------------------------------------------------------------
234
+ const oneMonthDuration = internal.createDuration(1, 'month');
235
+ function splitDateProfileByMonth(dateProfileGenerator, dateProfile, dateEnv, fixedWeekCount, showNonCurrentDates) {
236
+ const { start, end } = dateProfile.currentRange;
237
+ let monthStart = start;
238
+ const monthDateProfiles = [];
239
+ while (monthStart.valueOf() < end.valueOf()) {
240
+ const monthEnd = dateEnv.add(monthStart, oneMonthDuration);
241
+ const currentRange = {
242
+ // yuck
243
+ start: dateProfileGenerator.skipHiddenDays(monthStart),
244
+ end: dateProfileGenerator.skipHiddenDays(monthEnd, -1, true),
245
+ };
246
+ let renderRange = internal$1.buildDayTableRenderRange({
247
+ currentRange,
248
+ snapToWeek: true,
249
+ fixedWeekCount,
250
+ dateEnv,
251
+ });
252
+ renderRange = {
253
+ // yuck
254
+ start: dateProfileGenerator.skipHiddenDays(renderRange.start),
255
+ end: dateProfileGenerator.skipHiddenDays(renderRange.end, -1, true),
256
+ };
257
+ const activeRange = dateProfile.activeRange ?
258
+ internal.intersectRanges(dateProfile.activeRange, showNonCurrentDates ? renderRange : currentRange) :
259
+ null;
260
+ monthDateProfiles.push({
261
+ currentDate: dateProfile.currentDate,
262
+ isValid: dateProfile.isValid,
263
+ validRange: dateProfile.validRange,
264
+ renderRange,
265
+ activeRange,
266
+ currentRange,
267
+ currentRangeUnit: 'month',
268
+ isRangeAllDay: true,
269
+ dateIncrement: dateProfile.dateIncrement,
270
+ slotMinTime: dateProfile.slotMaxTime,
271
+ slotMaxTime: dateProfile.slotMinTime,
272
+ });
273
+ monthStart = monthEnd;
274
+ }
275
+ return monthDateProfiles;
276
+ }
277
+ // date formatting
278
+ // -------------------------------------------------------------------------------------------------
279
+ const YEAR_MONTH_FORMATTER = internal.createFormatter({ year: 'numeric', month: 'long' });
280
+ const YEAR_FORMATTER = internal.createFormatter({ month: 'long' });
281
+ function buildMonthFormat(formatOverride, monthDateProfiles) {
282
+ return formatOverride ||
283
+ ((monthDateProfiles[0].currentRange.start.getUTCFullYear() !==
284
+ monthDateProfiles[monthDateProfiles.length - 1].currentRange.start.getUTCFullYear())
285
+ ? YEAR_MONTH_FORMATTER
286
+ : YEAR_FORMATTER);
287
+ }
288
+
289
+ const OPTION_REFINERS = {
290
+ multiMonthMaxColumns: Number,
291
+ singleMonthMinWidth: Number,
292
+ singleMonthTitleFormat: internal.createFormatter,
293
+ singleMonthDidMount: internal.identity,
294
+ singleMonthWillUnmount: internal.identity,
295
+ singleMonthClass: internal.identity,
296
+ singleMonthHeaderClass: internal.identity,
297
+ singleMonthHeaderInnerClass: internal.identity,
298
+ };
299
+
300
+ var plugin = core.createPlugin({
301
+ name: '@fullcalendar/multimonth',
302
+ initialView: 'multiMonthYear',
303
+ optionRefiners: OPTION_REFINERS,
304
+ views: {
305
+ multiMonth: {
306
+ component: MultiMonthView,
307
+ dateProfileGeneratorClass: internal$1.TableDateProfileGenerator,
308
+ multiMonthMaxColumns: 3,
309
+ singleMonthMinWidth: 350,
310
+ },
311
+ multiMonthYear: {
312
+ type: 'multiMonth',
313
+ duration: { years: 1 },
314
+ fixedWeekCount: true,
315
+ showNonCurrentDates: false, // TODO: looks bad when single-col layout
316
+ },
317
+ },
318
+ });
319
+
320
+ core.globalPlugins.push(plugin);
321
+
322
+ exports["default"] = plugin;
323
+
324
+ Object.defineProperty(exports, '__esModule', { value: true });
325
+
326
+ return exports;
327
+
328
+ })({}, FullCalendar, FullCalendar.DayGrid.Internal, FullCalendar.Internal, FullCalendar.InternalClassNames, FullCalendar.Preact);