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