fullcalendar
Version:
FullCalendar Vanilla JS package for rendering a calendar
2,030 lines • 89.3 kB
JavaScript
import { o as organizeRawLocales, b as buildLocale, i as initEventSources, r as reduceEventSources, c as computeEventSourcesLoading, d as reduceEventSourcesNewTimeZone, a as globalPlugins, p as parseEventSource } from './71d3d952.js';
import { m as memoize, a as memoizeObjArg, N as NowTimerRunner, f as flushSyncWithSizeBatching, g as generateClassName, P as PureComponent, b as buildViewContext, c as getIsHeightAuto, V as ViewContextType, B as BaseComponent, C as ContentContainer } from './2d6c825e.js';
import { p as parseEvents, j as createEmptyEventStore, B as BASE_OPTION_DEFAULTS, k as BASE_OPTION_REFINERS, C as CALENDAR_LISTENER_REFINERS, l as CALENDAR_ONLY_OPTION_REFINERS, m as COMPLEX_OPTION_COMPARATORS, V as VIEW_ONLY_OPTION_REFINERS, n as mapHash, c as createEventUi, w as warn, e as createFormatter, o as parseDateSpan, t as triggerDateSelect, q as triggerDateUnselect, u as EventImpl, v as eventTupleToStore, x as EventSourceImpl, h as hashValuesToArray, y as parseEvent, z as eventApiToStore, a as buildEventApis, A as formatWithOrdinals, D as getElEventRange, F as listenBySelector, G as listenToHoverBySelector, H as isArraysEqual, I as isPropsEqualShallow, J as computeViewBorderless } from './642eba18.js';
import { m as mergeCalendarOptions, i as isMergedPropsEqual, a as mergeViewOptionsMap } from './28b4e459.js';
import { rangeContainsMarker, DateEnv, createDuration, joinDateTimeFormatParts, greatestDurationDenominator, diffWholeDays } from '@full-ui/headless-calendar';
import { jsxs, jsx, Fragment } from 'preact/jsx-runtime';
import { j as joinClassNames } from './c912f986.js';
import { D as DateProfileGenerator } from './54e239b5.js';
import { r as reduceEventStore, a as rezoneEventStoreDates, I as Interaction, p as parseInteractionSettings, i as interactionSettingsStore } from './b00ce53f.js';
import { E as Emitter } from './9e9ef97a.js';
import { c as classNames } from './3b4e987d.js';
import { Component, Fragment as Fragment$1, createElement } from 'preact/compat';
// TODO: easier way to add new hooks? need to update a million things
function refinePluginDef(input) {
return {
name: input.name,
premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate) : undefined,
reducers: input.reducers || [],
isLoadingFuncs: input.isLoadingFuncs || [],
contextInit: [].concat(input.contextInit || []),
eventRefiners: input.eventRefiners || {},
eventDefMemberAdders: input.eventDefMemberAdders || [],
eventSourceRefiners: input.eventSourceRefiners || {},
isDraggableTransformers: input.isDraggableTransformers || [],
eventDragMutationMassagers: input.eventDragMutationMassagers || [],
eventDefMutationAppliers: input.eventDefMutationAppliers || [],
dateSelectionTransformers: input.dateSelectionTransformers || [],
datePointTransforms: input.datePointTransforms || [],
dateSpanTransforms: input.dateSpanTransforms || [],
views: input.views || {},
viewPropsTransformers: input.viewPropsTransformers || [],
isPropsValid: input.isPropsValid || null,
externalDefTransforms: input.externalDefTransforms || [],
viewContainerAppends: input.viewContainerAppends || [],
eventDropTransformers: input.eventDropTransformers || [],
componentInteractions: input.componentInteractions || [],
calendarInteractions: input.calendarInteractions || [],
eventSourceDefs: input.eventSourceDefs || [],
cmdFormatter: input.cmdFormatter,
recurringTypes: input.recurringTypes || [],
initialView: input.initialView || '',
elementDraggingImpl: input.elementDraggingImpl,
optionChangeHandlers: input.optionChangeHandlers || {},
scrollerSyncerClass: input.scrollerSyncerClass || null,
listenerRefiners: input.listenerRefiners || {},
optionRefiners: input.optionRefiners || {},
optionDefaults: input.optionDefaults ? [input.optionDefaults] : [],
propSetHandlers: input.propSetHandlers || {},
};
}
function buildPluginHooks(pluginDefs, globalDefs) {
let pluginsByName = {};
let hooks = {
premiumReleaseDate: undefined,
reducers: [],
isLoadingFuncs: [],
contextInit: [],
eventRefiners: {},
eventDefMemberAdders: [],
eventSourceRefiners: {},
isDraggableTransformers: [],
eventDragMutationMassagers: [],
eventDefMutationAppliers: [],
dateSelectionTransformers: [],
datePointTransforms: [],
dateSpanTransforms: [],
views: {},
viewPropsTransformers: [],
isPropsValid: null,
externalDefTransforms: [],
viewContainerAppends: [],
eventDropTransformers: [],
componentInteractions: [],
calendarInteractions: [],
eventSourceDefs: [],
cmdFormatter: null,
recurringTypes: [],
initialView: '',
elementDraggingImpl: null,
optionChangeHandlers: {},
scrollerSyncerClass: null,
listenerRefiners: {},
optionRefiners: {},
optionDefaults: [],
propSetHandlers: {},
};
/*
IDs/names, etc
*/
function addDefs(defs) {
for (let unrefinedDef of defs) {
const { name } = unrefinedDef;
if (!name) {
throw new Error('Plugin must specify a name');
}
if (!pluginsByName[name]) {
const def = pluginsByName[name] = refinePluginDef(unrefinedDef);
hooks = combineHooks(hooks, def);
addDefs(unrefinedDef.deps || []);
}
}
}
if (pluginDefs) { // how could this be undefined?
addDefs(pluginDefs);
}
addDefs(globalDefs); // GLOBAL plugins
return hooks;
}
function buildBuildPluginHooks() {
let currentOverrideDefs = [];
let currentGlobalDefs = [];
let currentHooks;
return (overrideDefs, globalDefs) => {
if (!currentHooks || !isArraysEqual(overrideDefs, currentOverrideDefs) || !isArraysEqual(globalDefs, currentGlobalDefs)) {
currentHooks = buildPluginHooks(overrideDefs, globalDefs);
}
currentOverrideDefs = overrideDefs;
currentGlobalDefs = globalDefs;
return currentHooks;
};
}
function combineHooks(hooks0, hooks1) {
return {
premiumReleaseDate: compareOptionalDates(hooks0.premiumReleaseDate, hooks1.premiumReleaseDate),
reducers: hooks0.reducers.concat(hooks1.reducers),
isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs),
contextInit: hooks0.contextInit.concat(hooks1.contextInit),
eventRefiners: { ...hooks0.eventRefiners, ...hooks1.eventRefiners },
eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders),
eventSourceRefiners: { ...hooks0.eventSourceRefiners, ...hooks1.eventSourceRefiners },
isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers),
eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers),
eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers),
dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers),
datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms),
dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms),
views: mergeViewOptionsMap(hooks0.views, hooks1.views),
viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends),
eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),
eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
initialView: hooks0.initialView || hooks1.initialView, // put earlier plugins FIRST
elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers },
scrollerSyncerClass: hooks0.scrollerSyncerClass || hooks1.scrollerSyncerClass,
listenerRefiners: { ...hooks0.listenerRefiners, ...hooks1.listenerRefiners },
optionRefiners: { ...hooks0.optionRefiners, ...hooks1.optionRefiners },
optionDefaults: hooks0.optionDefaults.concat(hooks1.optionDefaults),
propSetHandlers: { ...hooks0.propSetHandlers, ...hooks1.propSetHandlers },
};
}
function compareOptionalDates(date0, date1) {
if (date0 === undefined) {
return date1;
}
if (date1 === undefined) {
return date0;
}
return new Date(Math.max(date0.valueOf(), date1.valueOf()));
}
function compileViewDefs(defaultConfigs, overrideConfigs) {
let hash = {};
let viewType;
for (viewType in defaultConfigs) {
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
}
for (viewType in overrideConfigs) {
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
}
return hash;
}
function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
if (hash[viewType]) {
return hash[viewType];
}
let viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs);
if (viewDef) {
hash[viewType] = viewDef;
}
return viewDef;
}
function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
let defaultConfig = defaultConfigs[viewType];
let overrideConfig = overrideConfigs[viewType];
let queryProp = (name) => ((defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] :
((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null));
let theComponent = queryProp('component');
let superType = queryProp('superType');
let superDef = null;
if (superType) {
if (superType === viewType) {
throw new Error('Can\'t have a custom view type that references itself');
}
superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs);
}
if (!theComponent && superDef) {
theComponent = superDef.component;
}
if (!theComponent) {
return null; // don't throw a warning, might be settings for a single-unit view
}
return {
type: viewType,
component: theComponent,
defaults: mergeCalendarOptions(superDef ? superDef.defaults : {}, defaultConfig ? defaultConfig.rawOptions : {}),
overrides: mergeCalendarOptions(superDef ? superDef.overrides : {}, overrideConfig ? overrideConfig.rawOptions : {}),
};
}
function parseViewConfigs(inputs) {
return mapHash(inputs, parseViewConfig);
}
function parseViewConfig(input) {
let rawOptions = typeof input === 'function' ?
{ component: input } :
input;
let { component } = rawOptions;
if (rawOptions.content) {
component = createViewHookComponent(rawOptions.content);
}
else if (component && !(component.prototype instanceof BaseComponent)) {
// WHY?: people were using `component` property for `content`
// TODO: converge on one setting name
component = createViewHookComponent(component);
}
return {
superType: rawOptions.type,
component: component,
rawOptions, // includes type and component too :(
};
}
/*
TODO: converge with ViewContainer
*/
function createViewHookComponent(contentGenerator) {
return (viewProps) => (jsx(ViewContextType.Consumer, { children: (context) => {
const { options, viewSpec } = context;
const renderProps = {
// the "extra" props, for sliceEvents...
...viewProps,
nextDayThreshold: options.nextDayThreshold,
// ViewDisplayInfo...
...computeViewBorderless(options),
options: { headerToolbar: options.headerToolbar, footerToolbar: options.footerToolbar },
isHeightAuto: getIsHeightAuto(options),
view: context.viewApi,
};
return (jsx(ContentContainer, { tag: "div", className: joinClassNames(generateClassName(options.viewClass, renderProps),
// WORKAROUND for way calendar's className would get merged into view's className
generateClassName(viewSpec.optionDefaults.class, renderProps), generateClassName(viewSpec.optionDefaults.className, renderProps), generateClassName(viewSpec.optionOverrides.class, renderProps), generateClassName(viewSpec.optionOverrides.className, renderProps)), renderProps: renderProps, generatorName: undefined, customGenerator: contentGenerator, didMount: options.didMount || options.viewDidMount, willUnmount: options.willUnmount || options.viewWillUnmount }));
} }));
}
function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides) {
let defaultConfigs = parseViewConfigs(defaultInputs);
let overrideConfigs = parseViewConfigs(optionOverrides.views);
let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
return mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides));
}
function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides) {
let durationInput = viewDef.overrides.duration ||
viewDef.defaults.duration ||
dynamicOptionOverrides.duration ||
optionOverrides.duration;
let duration = null;
let durationUnit = '';
let singleUnit = '';
let singleUnitOverrides = {};
if (durationInput) {
duration = createDurationCached(durationInput);
if (duration) { // valid?
let denom = greatestDurationDenominator(duration);
durationUnit = denom.unit;
if (denom.value === 1) {
singleUnit = durationUnit;
singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {};
}
}
}
return {
type: viewDef.type,
component: viewDef.component,
duration,
durationUnit,
singleUnit,
optionDefaults: viewDef.defaults,
optionOverrides: { ...singleUnitOverrides, ...viewDef.overrides },
};
}
// hack to get memoization working
let durationInputMap = {};
function createDurationCached(durationInput) {
let json = JSON.stringify(durationInput);
let res = durationInputMap[json];
if (res === undefined) {
res = createDuration(durationInput);
durationInputMap[json] = res;
}
return res;
}
function reduceViewType(viewType, action) {
switch (action.type) {
case 'CHANGE_VIEW_TYPE':
viewType = action.viewType;
}
return viewType;
}
function reduceCurrentDate(currentDate, action) {
switch (action.type) {
case 'CHANGE_DATE':
return action.dateMarker;
default:
return currentDate;
}
}
// should be initialized once and stay constant
// this will change too
function getInitialDate(options, dateEnv, nowManager) {
let initialDateInput = options.initialDate;
// compute the initial ambig-timezone date
if (initialDateInput != null) {
return dateEnv.createMarker(initialDateInput);
}
return nowManager.getDateMarker();
}
function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) {
switch (action.type) {
case 'SET_OPTION':
return { ...dynamicOptionOverrides, [action.optionName]: action.rawOptionValue };
default:
return dynamicOptionOverrides;
}
}
function reduceDateProfile(currentDateProfile, action, currentDate, nowDate, dateProfileGenerator) {
let dp;
switch (action.type) {
case 'CHANGE_VIEW_TYPE':
return dateProfileGenerator.build(action.dateMarker || currentDate, nowDate);
case 'CHANGE_DATE':
return dateProfileGenerator.build(action.dateMarker, nowDate);
case 'PREV':
dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate, nowDate);
if (dp.isValid) {
return dp;
}
break;
case 'NEXT':
dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate, nowDate);
if (dp.isValid) {
return dp;
}
break;
}
return currentDateProfile;
}
function reduceDateSelection(currentSelection, action) {
switch (action.type) {
case 'UNSELECT_DATES':
return null;
case 'SELECT_DATES':
return action.selection;
default:
return currentSelection;
}
}
function reduceSelectedEvent(currentInstanceId, action) {
switch (action.type) {
case 'UNSELECT_EVENT':
return '';
case 'SELECT_EVENT':
return action.eventInstanceId;
default:
return currentInstanceId;
}
}
function reduceEventDrag(currentDrag, action) {
let newDrag;
switch (action.type) {
case 'UNSET_EVENT_DRAG':
return null;
case 'SET_EVENT_DRAG':
newDrag = action.state;
return {
affectedEvents: newDrag.affectedEvents,
mutatedEvents: newDrag.mutatedEvents,
isEvent: newDrag.isEvent,
};
default:
return currentDrag;
}
}
function reduceEventResize(currentResize, action) {
let newResize;
switch (action.type) {
case 'UNSET_EVENT_RESIZE':
return null;
case 'SET_EVENT_RESIZE':
newResize = action.state;
return {
affectedEvents: newResize.affectedEvents,
mutatedEvents: newResize.mutatedEvents,
isEvent: newResize.isEvent,
};
default:
return currentResize;
}
}
function parseToolbars(calendarOptions, viewSpecs, calendarApi) {
let header = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, viewSpecs, calendarApi) : null;
let footer = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, viewSpecs, calendarApi) : null;
return { header, footer };
}
function parseToolbar(sectionStrHash, calendarOptions, viewSpecs, calendarApi) {
let isRtl = calendarOptions.direction === 'rtl';
let viewsWithButtons = [];
let hasTitle = false;
function processSectionStr(sectionStr) {
let sectionRes = parseSection(sectionStr, calendarOptions, viewSpecs, calendarApi);
viewsWithButtons.push(...sectionRes.viewsWithButtons);
hasTitle = hasTitle || sectionRes.hasTitle;
return sectionRes.widgets;
}
const sectionWidgets = {
start: processSectionStr(sectionStrHash[isRtl ? 'right' : 'left'] || sectionStrHash.start || ''),
center: processSectionStr(sectionStrHash.center || ''),
end: processSectionStr(sectionStrHash[isRtl ? 'left' : 'right'] || sectionStrHash.end || ''),
};
return {
sectionWidgets,
viewsWithButtons,
hasTitle,
};
}
/*
BAD: querying icons and text here. should be done at render time
*/
function parseSection(sectionStr, calendarOptions, viewSpecs, calendarApi) {
let calendarButtons = calendarOptions.buttons || {};
let customElements = calendarOptions.toolbarElements || {};
let sectionSubstrs = sectionStr ? sectionStr.split(' ') : [];
let viewsWithButtons = [];
let hasTitle = false;
let widgets = sectionSubstrs.map((buttonGroupStr) => (buttonGroupStr.split(',').map((name) => {
if (name === 'title') {
hasTitle = true;
return { name };
}
if (customElements[name]) {
return { name, customElement: customElements[name] };
}
let viewSpec;
let buttonInput = calendarButtons[name] || {};
let buttonText;
let buttonHint;
let buttonClick;
if ((viewSpec = viewSpecs[name])) {
viewsWithButtons.push(name);
const buttonTextKey = viewSpec.optionDefaults.buttonTextKey;
buttonText = buttonInput.text ||
(buttonTextKey ? calendarOptions[buttonTextKey] : '') ||
(viewSpec.singleUnit
? (calendarOptions[viewSpec.singleUnit + 'TextLong'] ||
calendarOptions[viewSpec.singleUnit + 'Text'])
: '') ||
name;
/*
buttons{}.hint(viewButtonText, viewName)
viewHint(viewButtonText, viewName)
*/
buttonHint = formatWithOrdinals(buttonInput.hint || calendarOptions.viewHint, [buttonText, name], // ordinal arguments
buttonText);
buttonClick = (ev) => {
buttonInput?.click?.(ev);
if (!ev.defaultPrevented) {
calendarApi.changeView(name);
}
};
}
else {
buttonText = buttonInput.text ||
calendarOptions[name + 'TextLong'] ||
calendarOptions[name + 'Text'] ||
name;
/*
buttons{}.hint(currentUnitText, currentUnit)
prevHint(currentUnitUnitext, currentUnit)
nextHint -- same
todayHint -- same
*/
if (name === 'prevYear') {
buttonHint = formatWithOrdinals(buttonInput.hint || calendarOptions.prevHint, [calendarOptions.yearText, 'year'], buttonText);
}
else if (name === 'nextYear') {
buttonHint = formatWithOrdinals(buttonInput.hint || calendarOptions.nextHint, [calendarOptions.yearText, 'year'], buttonText);
}
else {
buttonHint = (currentUnit) => {
return formatWithOrdinals(buttonInput.hint || calendarOptions[name + 'Hint'], // todayHint/prevHint/nextHint
[
calendarOptions[currentUnit + 'TextLong'] ||
calendarOptions[currentUnit + 'Text'],
currentUnit
], buttonText);
};
}
buttonClick = (ev) => {
buttonInput?.click?.(ev);
if (!ev.defaultPrevented) {
calendarApi[name]?.();
}
};
}
return {
name,
isView: Boolean(viewSpec),
buttonText,
buttonHint,
buttonDisplay: buttonInput.display,
buttonIconClass: buttonInput.iconClass,
buttonIconContent: buttonInput.iconContent,
buttonClick,
buttonIsPrimary: buttonInput.isPrimary || false,
buttonClass: buttonInput.class ?? buttonInput.className,
buttonDidMount: buttonInput.didMount,
buttonWillUnmount: buttonInput.willUnmount,
};
})));
return { widgets, viewsWithButtons, hasTitle };
}
// always represents the current view. otherwise, it'd need to change value every time date changes
class ViewImpl {
constructor(type, getCurrentData, dateEnv) {
this.type = type;
this.getCurrentData = getCurrentData;
this.dateEnv = dateEnv;
}
get calendar() {
return this.getCurrentData().calendarApi;
}
get title() {
return this.getCurrentData().viewTitle;
}
get activeStart() {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start);
}
get activeEnd() {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end);
}
get currentStart() {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start);
}
get currentEnd() {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end);
}
getOption(name) {
return this.getCurrentData().options[name]; // are the view-specific options
}
}
const DEF_DEFAULTS = {
startTime: '09:00',
endTime: '17:00',
daysOfWeek: [1, 2, 3, 4, 5], // monday - friday
display: 'inverse-background',
className: '', // TODO: remove
groupId: '_businessHours', // so multiple defs get grouped
};
/*
TODO: pass around as EventDefHash!!!
*/
function parseBusinessHours(input, context) {
return parseEvents(refineInputs(input), null, context);
}
function refineInputs(input) {
let rawDefs;
if (input === true) {
rawDefs = [{}]; // will get DEF_DEFAULTS verbatim
}
else if (Array.isArray(input)) {
// if specifying an array, every sub-definition NEEDS a day-of-week
rawDefs = input.filter((rawDef) => rawDef.daysOfWeek);
}
else if (typeof input === 'object' && input) { // non-null object
rawDefs = [input];
}
else { // is probably false
rawDefs = [];
}
rawDefs = rawDefs.map((rawDef) => ({ ...DEF_DEFAULTS, ...rawDef }));
return rawDefs;
}
// Computes what the title at the top of the calendarApi should be for this view
function buildTitle(dateProfile, viewOptions, dateEnv) {
let range;
// for views that span a large unit of time, show the proper interval, ignoring stray days before and after
if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
range = dateProfile.currentRange;
}
else { // for day units or smaller, use the actual day range
range = dateProfile.activeRange;
}
let parts;
const options = { isEndExclusive: dateProfile.isRangeAllDay };
if (viewOptions.titleFormat) {
parts = dateEnv.formatRangeToParts(range.start, range.end, createFormatter(viewOptions.titleFormat), options);
}
else {
parts = dateEnv.formatRangeToParts(range.start, range.end, createFormatter(buildTitleFormat(dateProfile, viewOptions.disallowAmbigTitle, 'long')), options);
if (hasTwoMonths(parts)) {
parts = dateEnv.formatRangeToParts(range.start, range.end, createFormatter(buildTitleFormat(dateProfile, viewOptions.disallowAmbigTitle, 'short')), options);
}
}
return joinDateTimeFormatParts(parts);
}
// Generates the format string that should be used to generate the title for the current date range.
// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
function buildTitleFormat(dateProfile, disallowAmbigTitle, monthFormat) {
const { currentRangeUnit } = dateProfile;
if (currentRangeUnit === 'year') {
return { year: 'numeric' };
}
if (currentRangeUnit === 'month') {
return { year: 'numeric', month: monthFormat };
}
if (!disallowAmbigTitle) {
const days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
if (days !== null && days > 1) {
return {
year: 'numeric',
month: monthFormat,
};
}
}
// one day. longer, like "September 9 2014"
return { year: 'numeric', month: 'long', day: 'numeric' };
}
function hasTwoMonths(parts) {
let hasStartMonth = false;
let hasEndMonth = false;
for (const part of parts) {
if (part.type === 'month') {
if (part.source === 'startRange')
hasStartMonth = true;
if (part.source === 'endRange')
hasEndMonth = true;
}
}
return hasStartMonth && hasEndMonth;
}
/*
TODO: test switching timezones when NO timezone plugin
*/
class CalendarNowManager {
constructor() {
this.resetListeners = new Set();
}
handleInput(dateEnv, // will change if timezone setup changed
nowInput) {
const oldDateEnv = this.dateEnv;
if (dateEnv !== oldDateEnv) {
if (typeof nowInput === 'function') {
this.nowFn = nowInput;
}
else if (!oldDateEnv) { // first time?
// inputs that express an exact instant (ISO with offset, Date, epoch ms) keep their
// exact epoch (marker round-trips are ambiguous during DST folds). civil inputs
// resolve deterministically to the first occurrence.
this.nowAnchorDate = nowInput
? resolveInputToDate(nowInput, dateEnv)
: new Date();
this.nowAnchorQueried = Date.now();
}
this.dateEnv = dateEnv;
// not first time? fire reset handlers
if (oldDateEnv) {
for (const resetListener of this.resetListeners.values()) {
resetListener();
}
}
}
}
getDateMarker() {
return this.dateEnv.timestampToMarker(this.getEpochMs());
}
/*
The exact instant of "now". Unlike a DateMarker, unambiguous during DST transitions.
When `now` was supplied as a function returning a civil time, resolves deterministically.
*/
getEpochMs() {
return this.nowAnchorDate
? this.nowAnchorDate.valueOf() + (Date.now() - this.nowAnchorQueried)
: resolveInputToDate(this.nowFn(), this.dateEnv).valueOf();
}
addResetListener(handler) {
this.resetListeners.add(handler);
}
removeResetListener(handler) {
this.resetListeners.delete(handler);
}
}
/*
Resolves a date input to an exact-instant Date. Prefers the instant the input itself
expressed (unambiguous during DST folds); falls back to first-occurrence resolution.
*/
function resolveInputToDate(input, dateEnv) {
const meta = dateEnv.createMarkerMeta(input);
return meta.instantMs != null
? new Date(meta.instantMs)
: dateEnv.toDate(meta.marker);
}
class CalendarDataManager {
constructor(config) {
this.computeCurrentViewData = memoize(this._computeCurrentViewData);
this.organizeRawLocales = memoize(organizeRawLocales);
this.buildLocale = memoize(buildLocale);
this.buildPluginHooks = buildBuildPluginHooks();
this.buildDateEnv = memoize(buildDateEnv);
this.parseToolbars = memoize(parseToolbars);
this.buildViewSpecs = memoize(buildViewSpecs);
this.buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator);
this.buildViewApi = memoize(buildViewApi);
this.buildViewUiProps = memoizeObjArg(buildViewUiProps);
this.buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqualShallow);
this.buildEventUiBases = memoize(buildEventUiBases);
this.parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours);
this.buildToolbarProps = memoize(buildToolbarProps);
this.buildTitle = memoize(buildTitle);
this.nowManager = new CalendarNowManager();
this.isDrainingActionQueue = false;
this.actionQueue = [];
this.optionOverrides = {};
// used by CalendarApiImpl
this.emitter = new Emitter();
this.currentCalendarOptionsRefiners = {};
this.currentCalendarOptionsInput = {};
this.currentCalendarOptionsRefined = {};
this.currentViewOptionsInput = {};
this.currentViewOptionsRefined = {};
this.optionsForRefining = [];
this.optionsForHandling = [];
this.getCurrentData = () => this.data;
this.handleNowChange = () => {
this.dispatch({ type: 'UPDATE_NOW' });
};
this.dispatch = (action) => {
this.actionQueue.push(action);
if (!this.isDrainingActionQueue) {
this.drainActionQueue();
}
};
this.config = config;
this.nowManager = new CalendarNowManager();
this.nowTimer = new NowTimerRunner(this.handleNowChange);
}
destroy() {
this.nowTimer.destroy();
}
/*
Will NOT trigger onDataChange unless there were other actions in the queue
*/
update(optionOverrides) {
this.optionOverrides = optionOverrides;
this.actionQueue.push({ type: 'IDLE' }); // ensure reducer gets called
this.drainActionQueue();
return this.data;
}
/*
WILL trigger onDataChange
*/
resetOptions(optionOverrides, changedOptionNames) {
if (changedOptionNames === undefined) {
this.optionOverrides = optionOverrides;
}
else {
this.optionOverrides = { ...this.optionOverrides, ...optionOverrides };
this.optionsForRefining.push(...changedOptionNames);
}
this.dispatch({ type: 'RESET_OPTIONS' });
}
drainActionQueue() {
let calendarContext;
let { state, data } = this;
const isInit = !state;
const { actionQueue } = this;
const actionsComplete = []; // non-idle
this.isDrainingActionQueue = true;
while (actionQueue.length) {
const action = actionQueue.shift();
({ state, data, calendarContext } = this.reduce(state, data, action));
this.state = state;
this.data = data;
if (action.type !== 'IDLE') {
actionsComplete.push(action);
}
}
this.isDrainingActionQueue = false;
if (isInit) {
const controllerOption = calendarContext.options.controller;
if (controllerOption) {
controllerOption._setApi(this.config.calendarApi);
}
}
if (!isInit && actionsComplete.length) {
const { onDataChange } = this.config;
if (onDataChange) {
onDataChange(this.data, actionsComplete);
}
}
}
reduce(prevState, prevData, action) {
let { config } = this;
let isInit = !prevState;
// === Compute options and view data ===
let dynamicOptionOverrides = isInit
? {}
: reduceDynamicOptionOverrides(prevState.dynamicOptionOverrides, action);
let optionsData = this.computeOptionsData(this.optionOverrides, dynamicOptionOverrides, config.calendarApi);
let currentViewType = isInit
? (optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView)
: reduceViewType(prevState.currentViewType, action);
let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, this.optionOverrides, dynamicOptionOverrides);
// === Wire things up ===
config.calendarApi.currentDataManager = this;
this.emitter.setThisContext(config.calendarApi);
this.emitter.setOptions(currentViewData.options);
// === Build calendarContext ===
let calendarContext = {
nowManager: this.nowManager,
dateEnv: optionsData.dateEnv,
options: optionsData.calendarOptions,
pluginHooks: optionsData.pluginHooks,
calendarApi: config.calendarApi,
dispatch: this.dispatch,
emitter: this.emitter,
getCurrentData: this.getCurrentData,
};
// === Update now timer ===
let { nowDate } = this.nowTimer.update({
unit: 'day',
unitValue: 1,
nowIndicatorSnap: 'auto',
nowManager: this.nowManager,
dateEnv: optionsData.dateEnv,
});
// === Compute currentDate ===
let currentDate = isInit
? getInitialDate(optionsData.calendarOptions, optionsData.dateEnv, this.nowManager)
: reduceCurrentDate(prevState.currentDate, action);
// === Compute dateProfile ===
let dateProfile;
if (isInit) {
dateProfile = currentViewData.dateProfileGenerator.build(currentDate, nowDate);
}
else {
dateProfile = prevState.dateProfile;
// Check for generator change
if (prevData && prevData.dateProfileGenerator !== currentViewData.dateProfileGenerator) {
dateProfile = currentViewData.dateProfileGenerator.build(currentDate, nowDate);
}
dateProfile = reduceDateProfile(dateProfile, action, currentDate, nowDate, currentViewData.dateProfileGenerator);
}
// === Adjust currentDate if out of range ===
if ((action && (action.type === 'PREV' || action.type === 'NEXT')) ||
!rangeContainsMarker(dateProfile.activeRange, currentDate)) {
currentDate = dateProfile.currentRange.start;
}
// === Compute eventSources, eventStore ===
let eventSources = isInit
? initEventSources(optionsData.calendarOptions, dateProfile, calendarContext)
: reduceEventSources(prevState.eventSources, action, dateProfile, calendarContext);
let eventStore = isInit
? createEmptyEventStore()
: reduceEventStore(prevState.eventStore, action, eventSources, dateProfile, calendarContext);
// === Compute renderableEventStore ===
let isEventsLoading = computeEventSourcesLoading(eventSources);
let renderableEventStore = isInit
? createEmptyEventStore()
: (isEventsLoading && !currentViewData.options.progressiveEventRendering)
? (prevState.renderableEventStore || eventStore)
: eventStore;
// === UI computation ===
let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext);
let eventUiBySource = this.buildEventUiBySource(eventSources);
let eventUiBases = isInit
? {}
: this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
// === Build new state ===
let newState = {
dynamicOptionOverrides,
currentViewType,
currentDate,
dateProfile,
eventSources,
eventStore,
renderableEventStore,
selectionConfig,
eventUiBases,
businessHours: this.parseContextBusinessHours(calendarContext),
dateSelection: isInit ? null : reduceDateSelection(prevState.dateSelection, action),
eventSelection: isInit ? '' : reduceSelectedEvent(prevState.eventSelection, action),
eventDrag: isInit ? null : reduceEventDrag(prevState.eventDrag, action),
eventResize: isInit ? null : reduceEventResize(prevState.eventResize, action),
nowDate,
};
// === Plugin reducers ===
let contextAndState = { ...calendarContext, ...newState };
for (let reducer of optionsData.pluginHooks.reducers) {
Object.assign(newState, reducer(prevState, action, contextAndState));
}
// === Loading state emission ===
let wasLoading = prevState ? computeIsLoading(prevState, calendarContext) : false;
let isLoading = computeIsLoading(newState, calendarContext);
if (!wasLoading && isLoading) {
this.emitter.trigger('loading', true);
}
else if (wasLoading && !isLoading) {
this.emitter.trigger('loading', false);
}
// === Build CalendarData ===
let viewTitle = this.buildTitle(dateProfile, currentViewData.options, optionsData.dateEnv);
let toolbarProps = this.buildToolbarProps(currentViewData.viewSpec, dateProfile, currentViewData.dateProfileGenerator, currentDate, nowDate, viewTitle);
let newData = {
viewTitle,
nowManager: this.nowManager,
calendarApi: config.calendarApi,
dispatch: this.dispatch,
emitter: this.emitter,
getCurrentData: this.getCurrentData,
toolbarProps,
...optionsData,
...currentViewData,
...newState,
};
// === Handle option changes ===
let changeHandlers = optionsData.pluginHooks.optionChangeHandlers;
let prevCalendarOptions = prevData && prevData.calendarOptions;
let newCalendarOptions = optionsData.calendarOptions;
if (prevCalendarOptions && prevCalendarOptions !== newCalendarOptions) {
if (prevCalendarOptions.timeZone !== newCalendarOptions.timeZone) {
// HACK
newState.eventSources = newData.eventSources = reduceEventSourcesNewTimeZone(newData.eventSources, dateProfile, newData);
newState.eventStore = newData.eventStore = rezoneEventStoreDates(newData.eventStore, prevData.dateEnv, newData.dateEnv);
newState.renderableEventStore = newData.renderableEventStore = rezoneEventStoreDates(newData.renderableEventStore, prevData.dateEnv, newData.dateEnv);
}
for (let optionName in changeHandlers) {
if (this.optionsForHandling.indexOf(optionName) !== -1 ||
prevCalendarOptions[optionName] !== newCalendarOptions[optionName]) {
changeHandlers[optionName](newCalendarOptions[optionName], newData);
}
}
}
this.optionsForHandling = [];
return { state: newState, data: newData, calendarContext };
}
computeOptionsData(optionOverrides, dynamicOptionOverrides, calendarApi) {
// TODO: blacklist options that are handled by optionChangeHandlers
if (!this.optionsForRefining.length &&
optionOverrides === this.stableOptionOverrides &&
dynamicOptionOverrides === this.stableDynamicOptionOverrides) {
return this.stableCalendarOptionsData;
}
let { refinedOptions, pluginHooks, localeDefaults, availableLocaleData, } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides);
let dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekTextLong, refinedOptions.weekTextShort, pluginHooks, availableLocaleData);
let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides);
let toolbarConfig = this.parseToolbars(refinedOptions, viewSpecs, calendarApi);
return this.stableCalendarOptionsData = {
calendarOptions: refinedOptions,
pluginHooks,
dateEnv,
viewSpecs,
toolbarConfig,
localeDefaults,
availableRawLocales: availableLocaleData.map,
};
}
// always called from behind a memoizer
processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) {
let { locales, locale } = mergeCalendarOptions(BASE_OPTION_DEFAULTS, optionOverrides, dynamicOptionOverrides);
let availableLocaleData = this.organizeRawLocales(locales);
let availableRawLocales = availableLocaleData.map;
let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options;
let pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins);
let refiners = this.currentCalendarOptionsRefiners = {
...BASE_OPTION_REFINERS,
...CALENDAR_LISTENER_REFINERS,
...CALENDAR_ONLY_OPTION_REFINERS,
...pluginHooks.listenerRefiners,
...pluginHooks.optionRefiners,
};
let raw = mergeCalendarOptions(BASE_OPTION_DEFAULTS, ...pluginHooks.optionDefaults, localeDefaults, filterKnownOptions(mergeCalendarOptions(optionOverrides, dynamicOptionOverrides), refiners));
let refined = {};
let currentRaw = this.currentCalendarOptionsInput;
let currentRefined = this.currentCalendarOptionsRefined;
let anyChanges = false;
for (let optionName in raw) {
if (this.optionsForRefining.indexOf(optionName) === -1 && (raw[optionName] === currentRaw[optionName] || (COMPLEX_OPTION_COMPARATORS[optionName] &&
(optionName in currentRaw) &&
COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName])) || isMergedPropsEqual(currentRaw[optionName], raw[optionName]))) {
refined[optionName] = currentRefined[optionName];
}
else if (refiners[optionName]) {
refined[optionName] = refiners[optionName](raw[optionName], optionName);
anyChanges = true;
}
}
if (anyChanges) {
this.currentCalendarOptionsInput = raw;
this.currentCalendarOptionsRefined = refined;
this.stableOptionOverrides = optionOverrides;
this.stableDynamicOptionOverrides = dynamicOptionOverrides;
}
this.optionsForHandling.push(...this.optionsForRefining);
this.optionsForRefining = [];
return {
rawOptions: this.currentCalendarOptionsInput,
refinedOptions: this.currentCalendarOptionsRefined,
pluginHooks,
availableLocaleData,
localeDefaults,
};
}
_computeCurrentViewData(viewType, optionsData, optionOverrides, dynamicOptionOverrides) {
let viewSpec = optionsData.viewSpecs[viewType];
if (!viewSpec) {
throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`);
}
let { refinedOptions } = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides);
this.nowManager.handleInput(optionsData.dateEnv, refinedOptions.now);
let dateProfileGenerator = this.buildDateProfileGenerator({
dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass,
duration: viewSpec.duration,
durationUnit: viewSpec.durationUnit,
usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime,
dateEnv: optionsData.dateEnv,
calendarApi: this.config.calendarApi,
slotMinTime: refinedOptions.slotMinTime,
slotMaxTime: refinedOptions.slotMaxTime,
showNonCurrentDates: refinedOptions.showNonCurrentDates,
dayCount: refinedOptions.dayCount,
dateAlignment: refinedOptions.dateAlignment,
dateIncrement: refinedOptions.dateIncrement,
hiddenDays: refinedOptions.hiddenDays,
weekends: refinedOptions.weekends,
validRangeInput: refinedOptions.validRange,
visibleRangeInput: refinedOptions.visibleRange,
fixedWeekCount: refinedOptions.fixedWeekCount,
});
let viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv);
return { viewSpec, options: refinedOptions, dateProfileGenerator, viewApi };
}
processRawViewOptions(viewSpec, pluginHooks, localeDefaults, optionOverrides, dynamicOptionOverrides) {
let refiners = {
...BASE_OPTION_REFINERS,
...CALENDAR_LISTENER_REFINERS,
...CALENDAR_ONLY_OPTION_REFINERS,
...VIEW_ONLY_OPTION_REFINERS,
...pluginHooks.listenerRefiners,
...pluginHooks.optionRefiners,
};
let raw = mergeCalendarOptions(BASE_OPTION_DEFAULTS, ...pluginHooks.optionDefaults, viewSpec.optionDefaults, localeDefaults, filterKnownOptions(mergeCalendarOptions(optionOverrides, viewSpec.optionOverrides, dynamicOptionOverrides), refiners));
let refined = {};
let currentRaw = this.currentViewOptionsInput;
let currentRefined = this.currentViewOptionsRefined;
let anyChanges = false;
for (let optionName in raw) {
if (raw[optionName] === currentRaw[optionName] || (COMPLEX_OPTION_COMPARATORS[optionName] &&
COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName])) || isMergedPropsEqual(currentRaw[optionName], raw[optionName])) {
refined[optionName] = currentRefined[optionName];
}
else {
if (raw[optionName] === this.currentCalendarOptionsInput[optionName] ||
(COMPLEX_OPTION_COMPARATORS[optionName] &&
COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], this.currentCalendarOptionsInput[optionName]))) {
if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop
refined[optionName] = this.currentCalendarOptionsRefined[optionName];
}
}
else if (refiners[optionName]) {
refined[optionName] = refiners[optionName](raw[optionName], optionName);
}
anyChanges = true;
}
}
if (anyChanges) {
this.currentViewOptionsInput = raw;
this.currentViewOptionsRefined = refined;
}
return {
rawOptions: this.currentViewOptionsInput,
refinedOptions: this.currentViewOptionsRefined,
};
}
}
function buildDateEnv(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekTextLong, weekTextShort, pluginHooks, availableLocaleData) {
let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map);
return new DateEnv({
calendarSystem: 'gregory', // TODO: make this a setting
timeZone,
locale,
weekNumberCalculation,
firstDay,
weekTextLong,
weekTextShort,
cmdFormatter: pluginHooks.cmdFormatter,
});
}
function buildDateProfileGenerator(props) {
let DateProfileGeneratorClass = props.dateProfileGeneratorClass || DateProfileGenerator;
return new DateProfileGeneratorClass(props);
}
function buildViewApi(type, getCurrentData, dateEnv) {
return new ViewImpl(type, getCurrentData, dateEnv);
}
function buildEventUiBySource(eventSources) {
return mapHash(eventSources, (eventSource) => eventSource.ui);
}
/*
The result of this is processed by compileEventUi
*/
function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
let eventUiBases = {
'': eventUiSingleBase, // fallback
};
for (let defId in eventDefs) {
let def = eventDefs[defId];
if (def.sourceId && eventUiBySource[def.sourceId]) {
eventUiBases[defId] = eventUiBySource[def.sourceId];
}
}
return eventUiBases;
}
function buildViewUiProps(calendarContext) {
const { options } = calendarContext;
return {
eventUiSingleBase: createEventUi({
display: options.eventDisplay,
editable: options.editable, // without "event" at start
startEditable: options.eventStartEditable,
durationEditable: options.eventDurationEditable,
constraint: options.eventConstraint,
overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined,
allow: options.eventAllow,
// color: options.eventColor, // StandardEvent/BgEvent will handle this
// contrastColor: options.eventContrastColor, // StandardEvent/BgEvent will handle this
// className: options.eventClass // render hook will handle this
}, calendarContext),
selectionConfig: createEventUi({
constraint: options.selectConstraint,
overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined,
allow: options.selectAllow,
}, calendarContext),
};
}
function computeIsLoading(state, context) {
for (let isLoadingFunc of context.pluginHooks.isLoadingFuncs) {
if (isLoadingFunc(state)) {
return true;
}
}
return false;
}
function parseContextBusinessHours(calendarContext) {
return parseBusinessHours(calendarContext.options.businessHours, calendarContext);
}
const warnedUnknownOptions = {};
function filterKnownOptions(options, optionRefiners) {
const knownOptions = {};
for (const optionName in options) {
if (optionRefiners[optionName]) {
knownOptions[optionName] = options[optionName];
}
else if (!warnedUnknownOptions[optionName]) {
warn(`Unknown option \`${optionName}\`.`);
warnedUnknownOptions[optionName] = true;
}
}
return knownOptions;
}
function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, nowDate, title) {
// don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid
let todayInfo = dateProfileGenerator.build(nowDate, nowDate, undefined, /* forceToValid = */ false);
let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, nowDate, /* forceToValid = */ false);
let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, nowDate, /* forceToValid = */ false);
return {
title,
selectedButton: viewSpec.type,
navUnit: viewSpec.singleUnit,
isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, nowDate),
isPrevEnabled: prevInfo.isValid,
isNextEnabled: nextInfo.isValid,
};
}
class CalendarApiImpl {
getCurrentData() {
return this.currentDataManager.getCurrentData();
}
dispatch(action) {
this.currentDataManager.dispatch(action);
}
get view() { return this.getCurrentData().viewApi; }
batchRendering(callback) {
callback();
}
// Options
// -----------------------------------------------------------------------------------------------------------------
setOption(name, val) {
this.dispatch({
type: 'SET_OPTION',
optionName: name,
rawOptionValue: val,
});
}
getOption(name) {
return this.currentDataManager.currentCalendarOptionsInput[name];
}
getAvailableLocaleCodes() {
return Object.keys(this.getCurrentData().availableRawLocales);
}
// Trigger
// -----------------------------------------------------------------------------------------------------------------
on(handlerName, handler) {
let { currentDataManager } = this;
if (currentDataManager.currentCalendarOptionsRefiners[handlerName]) {
currentDataManager.emitter.on(handlerName, handler);
}
else {
warn(`Unknown listener \`${handlerName}\`.`);
}
}
off(handlerName, handler) {
this.currentDataManager.emitter.off(handlerName, handler);
}
// not meant for public use
trigger(handlerName, ...args) {
this.currentDataManager.emitter.trigger(handlerName, ...args);
}
// View
// -----------------------------------------------------------------------------------------------------------------
changeView(viewType, dateOrRange) {
this.batchRendering(() => {
this.unselect();
if (dateOrRange) {
if (dateOrRange.start && dateOrRange.end) { // a range
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType,
});
this.dispatch({
type: 'SET_OPTION',
optionName: 'visibleRange',
rawOptionValue: dateOrRange,
});
}
else {
let { dateEnv } = this.getCurrentData();
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType,
dateMarker: dateEnv.createMarker(dateOrRange),
});
}
}
else {
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType,
});
}
});
}
// Forces navigation to a view for the given date.
// `viewType` can be a specific view name or a generic one like "week" or "day".
// needs to change
zoomTo(dateMarker, viewType) {
let state = this.getCurrentData();
let spec;
viewType = viewType || 'day'; // day is default zoom
spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType);
this.unselect();
if (spec) {
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType: spec.type,
dateMarker,
});
}
else {
this.dispatch({
type: 'CHANGE_DATE',
dateMarker,
});
}
}
// Given a duration singular unit, like "week" or "day", finds a matching view spec.
// Preference is given to views that have corresponding buttons.
getUnitViewSpec(unit) {
let { viewSpecs, toolbarConfig } = this.getCurrentData();
let viewTypes = [].concat(toolbarConfig.header ? toolbarConfig.header.viewsWithButtons : [], toolbarConfig.footer ? toolbarConfig.footer.viewsWithButtons : []);
let i;
let spec;
for (let viewType in viewSpecs) {
viewTypes.push(viewType);
}
for (i = 0; i < viewTypes.length; i += 1) {
spec = viewSpecs[viewTypes[i]];
if (spec) {
if (spec.singleUnit === unit) {
return spec;
}
}
}
return null;
}
// Current Date
// -----------------------------------------------------------------------------------------------------------------
prev() {
this.unselect();
this.dispatch({ type: 'PREV' });
}
next() {
this.unselect();
this.dispatch({ type: 'NEXT' });
}
prevYear() {
let state = this.getCurrentData();
this.unselect();
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.addYears(state.currentDate, -1),
});
}
nextYear() {
let state = this.getCurrentData();
this.unselect();
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.addYears(state.currentDate, 1),
});
}
today() {
let state = this.getCurrentData();
this.unselect();
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.nowManager.getDateMarker(),
});
}
gotoDate(zonedDateInput) {
let state = this.getCurrentData();
this.unselect();
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.createMarker(zonedDateInput),
});
}
incrementDate(deltaInput) {
let state = this.getCurrentData();
let delta = createDuration(deltaInput);
if (delta) { // else, warn about invalid input?
this.unselect();
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.add(state.currentDate, delta),
});
}
}
getDate() {
let state = this.getCurrentData();
return state.dateEnv.toDate(state.currentDate);
}
// Date Formatting Utils
// -----------------------------------------------------------------------------------------------------------------
formatDate(d, formatter) {
let { dateEnv } = this.getCurrentData();
let dateMeta = dateEnv.createMarkerMeta(d);
return joinDateTimeFormatParts(dateEnv.formatToParts(dateMeta.marker, createFormatter(formatter), { instantMs: dateMeta.instantMs }));
}
// `settings` is for formatter AND isEndExclusive
formatRange(d0, d1, settings) {
let { dateEnv } = this.getCurrentData();
let startMeta = dateEnv.createMarkerMeta(d0);
let endMeta = dateEnv.createMarkerMeta(d1);
return joinDateTimeFormatParts(dateEnv.formatRangeToParts(startMeta.marker, endMeta.marker, createFormatter(settings), {
isEndExclusive: settings.isEndExclusive,
startInstantMs: startMeta.instantMs,
endInstantMs: endMeta.instantMs,
}));
}
formatIso(d, omitTime) {
let { dateEnv } = this.getCurrentData();
return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime });
}
// Date Selection / Event Selection / DayClick
// -----------------------------------------------------------------------------------------------------------------
select(dateOrObj, endDate) {
let selectionInput;
if (endDate == null) {
if (dateOrObj.start != null) {
selectionInput = dateOrObj;
}
else {
selectionInput = {
start: dateOrObj,
end: null,
};
}
}
else {
selectionInput = {
start: dateOrObj,
end: endDate,
};
}
let state = this.getCurrentData();
let selection = parseDateSpan(selectionInput, state.dateEnv, createDuration({ days: 1 }));
if (selection) { // throw parse error otherwise?
this.dispatch({ type: 'SELECT_DATES', selection });
triggerDateSelect(selection, null, state);
}
}
unselect(pev) {
let state = this.getCurrentData();
if (state.dateSelection) {
this.dispatch({ type: 'UNSELECT_DATES' });
triggerDateUnselect(pev, state);
}
}
// Public Events API
// -----------------------------------------------------------------------------------------------------------------
addEvent(eventInput, sourceInput) {
if (eventInput instanceof EventImpl) {
let def = eventInput._def;
let instance = eventInput._instance;
let currentData = this.getCurrentData();
// not already present? don't want to add an old snapshot
if (!currentData.eventStore.defs[def.defId]) {
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore({ def, instance }), // TODO: better util for two args?
});
this.triggerEventAdd(eventInput);
}
return eventInput;
}
let state = this.getCurrentData();
let eventSource;
if (sourceInput instanceof EventSourceImpl) {
eventSource = sourceInput.internalEventSource;
}
else if (typeof sourceInput === 'boolean') {
if (sourceInput) { // true. part of the first event source
[eventSource] = hashValuesToArray(state.eventSources);
}
}
else if (sourceInput != null) { // an ID. accepts a number too
let sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function
if (!sourceApi) {
warn(`Unknown event source ID \`${sourceInput}\`.`); // TODO: test
return null;
}
eventSource = sourceApi.internalEventSource;
}
let tuple = parseEvent(eventInput, eventSource, state, false);
if (tuple) {
let newEventApi = new EventImpl(state, tuple.def, tuple.def.recurringDef ? null : tuple.instance);
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore(tuple),
});
this.triggerEventAdd(newEventApi);
return newEventApi;
}
return null;
}
triggerEventAdd(eventApi) {
let { emitter } = this.getCurrentData();
emitter.trigger('eventAdd', {
event: eventApi,
relatedEvents: [],
revert: () => {
this.dispatch({
type: 'REMOVE_EVENTS',
eventStore: eventApiToStore(eventApi),
});
},
});
}
// TODO: optimize
getEventById(id) {
let state = this.getCurrentData();
let { defs, instances } = state.eventStore;
id = String(id);
for (let defId in defs) {
let def = defs[defId];
if (def.publicId === id) {
if (def.recurringDef) {
return new EventImpl(state, def, null);
}
for (let instanceId in instances) {
let instance = instances[instanceId];
if (instance.defId === def.defId) {
return new EventImpl(state, def, instance);
}
}
}
}
return null;
}
getEvents() {
let currentData = this.getCurrentData();
return buildEventApis(currentData.eventStore, currentData);
}
removeAllEvents() {
this.dispatch({ type: 'REMOVE_ALL_EVENTS' });
}
// Public Event Sources API
// -----------------------------------------------------------------------------------------------------------------
getEventSources() {
let state = this.getCurrentData();
let sourceHash = state.eventSources;
let sourceApis = [];
for (let internalId in sourceHash) {
sourceApis.push(new EventSourceImpl(state, sourceHash[internalId]));
}
return sourceApis;
}
getEventSourceById(id) {
let state = this.getCurrentData();
let sourceHash = state.eventSources;
id = String(id);
for (let sourceId in sourceHash) {
if (sourceHash[sourceId].publicId === id) {
return new EventSourceImpl(state, sourceHash[sourceId]);
}
}
return null;
}
addEventSource(sourceInput) {
let state = this.getCurrentData();
if (sourceInput instanceof EventSourceImpl) {
// not already present? don't want to add an old snapshot
if (!state.eventSources[sourceInput.internalEventSource.sourceId]) {
this.dispatch({
type: 'ADD_EVENT_SOURCES',
sources: [sourceInput.internalEventSource],
});
}
return sourceInput;
}
let eventSource = parseEventSource(sourceInput, state);
if (eventSource) { // TODO: error otherwise?
this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] });
return new EventSourceImpl(state, eventSource);
}
return null;
}
removeAllEventSources() {
this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' });
}
refetchEvents() {
this.dispatch({ type: 'FETCH_EVENT_SOURCES', isRefetch: true });
}
// Scroll
// -----------------------------------------------------------------------------------------------------------------
scrollToTime(timeInput) {
let time = createDuration(timeInput);
if (time) {
this.trigger('_timeScrollRequest', time);
}
}
// Button State
// -----------------------------------------------------------------------------------------------------------------
getButtonState() {
const currentData = this.getCurrentData();
const { toolbarProps } = currentData;
const options = currentData.calendarOptions;
const buttonConfigs = options.buttons || {};
const viewSpecs = currentData.viewSpecs;
const currentUnit = currentData.viewSpec.singleUnit;
const currentHintOrdinal = [
currentUnit ? getSingleUnitText(currentUnit, options) : '',
currentUnit,
];
const buttonState = {
today: {
text: options.todayText,
hint: formatWithOrdinals(options.todayHint, currentHintOrdinal, options.todayText),
isDisabled: !toolbarProps.isTodayEnabled,
},
prev: {
text: options.prevText,
hint: formatWithOrdinals(options.prevHint, currentHintOrdinal, options.prevText),
isDisabled: !toolbarProps.isPrevEnabled,
},
next: {
text: options.nextText,
hint: formatWithOrdinals(options.nextHint, currentHintOrdinal, options.nextText),
isDisabled: !toolbarProps.isNextEnabled,
},
prevYear: {
text: options.prevYearText,
hint: formatWithOrdinals(options.prevHint, [options.yearText, 'year'], options.prevYearText),
isDisabled: false,
},
nextYear: {
text: options.prevYearText,
hint: formatWithOrdinals(options.nextHint, [options.yearText, 'year'], options.nextYearText),
isDisabled: false,
},
};
for (const viewSpecName in viewSpecs) {
const viewSpec = viewSpecs[viewSpecName];
const { singleUnit } = viewSpec;
const buttonTextKey = viewSpec.optionDefaults.buttonTextKey;
const buttonText = buttonConfigs[viewSpecName]?.text ||
(buttonTextKey ? options[buttonTextKey] : '') ||
(singleUnit ? getSingleUnitText(singleUnit, options) : '') ||
viewSpecName;
const buttonHint = formatWithOrdinals(options.viewHint, [buttonText, viewSpecName], // ordinal arguments
buttonText);
buttonState[viewSpecName] = {
text: buttonText,
hint: buttonHint,
};
}
return buttonState;
}
}
function getSingleUnitText(singleUnit, options) {
return options[singleUnit + 'TextLong'] || options[singleUnit + 'Text'];
}
class CalendarMediaRoot extends Component {
constructor() {
super(...arguments);
this.state = {
forPrint: false,
};
this.handleBeforePrint = () => {
// The synchronous commit mounts print-only DOM during this beforeprint
// task. Watchers registering during the bracket measure immediately, and
// their layout recomputations settle in one batched drain before the
// native event returns.
flushSyncWithSizeBatching(() => {
this.setState({ forPrint: true });
});
};
this.handleAfterPrint = () => {
// No synchronous commit needed: nothing else listens to _afterprint, and
// the ordinary microtask-batched re-render restores the screen DOM before
// the next paint. Screen watchers keep their async-first measurement.
this.setState({ forPrint: false });
};
}
render() {
return this.props?.children(this.state.forPrint);
}
componentDidMount() {
const { props } = this;
const { emitter } = props;
emitter.on('_beforeprint', this.handleBeforePrint);
emitter.on('_afterprint', this.handleAfterPrint);
}
componentWillUnmount() {
const { props } = this;
const { emitter } = props;
emitter.off('_beforeprint', this.handleBeforePrint);
emitter.off('_afterprint', this.handleAfterPrint);
}
}
function computeRootClassName(options, forPrint) {
let borderlessX = options.borderlessX ?? options.borderless;
let borderlessTop = options.borderlessTop ?? options.borderless;
let borderlessBottom = options.borderlessBottom ?? options.borderless;
const calendarDisplayData = {
borderlessX: Boolean(borderlessX),
borderlessTop: Boolean(borderlessTop),
borderlessBottom: Boolean(borderlessBottom),
};
return joinClassNames(generateClassName(options.class, calendarDisplayData), generateClassName(options.className, calendarDisplayData), classNames.borderBoxRoot, classNames.isolate, classNames.flexCol, forPrint ? classNames.calendarPrintRoot : classNames.calendarScreenRoot);
}
class ButtonIcon extends BaseComponent {
render() {
const { contentGenerator, className } = this.props;
if (contentGenerator) {
// TODO: somehow give className to the svg?
return (jsx(ContentContainer, { tag: 'span', style: { display: 'contents' }, attrs: { 'aria-hidden': true }, renderProps: {}, generatorName: undefined, customGenerator: contentGenerator }));
}
if (className !== undefined) {
return (jsx("span", { "aria-hidden": true, className: className }));
}
}
}
class ToolbarSection extends BaseComponent {
render() {
let { props } = this;
let { options } = this.context;
let children = props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup));
return createElement('div', {
className: generateClassName(options.toolbarSectionClass, { name: props.name }),
}, ...children);
}
renderWidgetGroup(widgetGroup) {
let { props, context } = this;
let { options } = context;
let children = [];
let isOnlyButtons = true;
let isOnlyView = true;
for (const widget of widgetGroup) {
const { name, isView } = widget;
if (name === 'title') {
isOnlyButtons = false;
}
else if (!isView) {
isOnlyView = false;
}
}
for (let widget of widgetGroup) {
let { name, customElement, buttonHint } = widget;
if (name === 'title') {
children.push(jsx("div", { role: 'heading', "aria-level": options.headingLevel, id: props.titleId, className: joinClassNames(options.toolbarTitleClass), children: props.title }));
}
else if (customElement) {
children.push(jsx(ContentContainer, { tag: 'span', style: { display: 'contents' }, renderProps: {}, generatorName: undefined, customGenerator: customElement }));
}
else {
let isSelected = name === props.selectedButton;
let isDisabled = (!props.isTodayEnabled && name === 'today') ||
(!props.isPrevEnabled && name === 'prev') ||
(!props.isNextEnabled && name === 'next');
let buttonDisplay = widget.buttonDisplay ?? options.buttonDisplay;
if (buttonDisplay === 'auto') {
buttonDisplay = (widget.buttonIconContent || widget.buttonIconClass)
? 'icon'
: 'text';
}
let iconNode;
if (buttonDisplay !== 'text') {
iconNode = (jsx(ButtonIcon, { className: widget.buttonIconClass, contentGenerator: widget.buttonIconContent }));
}
let inGroup = widgetGroup.length > 1 && isOnlyButtons;
let buttonGroup = inGroup ? { hasSelection: isOnlyView } : null;
let renderProps = {
name,
text: widget.buttonText,
isPrimary: widget.buttonIsPrimary,
isSelected,
isDisabled,
isIconOnly: buttonDisplay === 'icon',
buttonGroup,
};
children.push(jsx(ContentContainer, { tag: 'button', attrs: {
type: 'button',
disabled: isDisabled,
...((isOnlyButtons && isOnlyView)
? { 'role': 'tab', 'aria-selected': isSelected }
: { 'aria-pressed': isSelected }),
'aria-label': typeof buttonHint === 'function'
? buttonHint(props.navUnit)
: buttonHint,
onClick: widget.buttonClick,
}, className: joinClassNames(generateClassName(options.buttonClass, renderProps), !isDisabled && classNames.cursorPointer, inGroup && joinClassNames(isSelected ? classNames.z1 : classNames.z0, classNames.focusZ2)), renderProps: renderProps, generatorName: undefined, classNameGenerator: widget.buttonClass, didMount: widget.buttonDidMount, willUnmount: widget.buttonWillUnmount, children: () => (buttonDisplay === 'text'
? widget.buttonText
: buttonDisplay === 'icon'
? iconNode
: buttonDisplay === 'icon-text'
? (jsxs(Fragment, { children: [iconNode, widget.buttonText] }))
: (jsxs(Fragment, { children: [widget.buttonText, iconNode] })) // text-icon
) }));
}
}
if (children.length > 1) {
return createElement('div', {
role: (isOnlyButtons && isOnlyView) ? 'tablist' : undefined,
'aria-label': (isOnlyButtons && isOnlyView) ? options.viewChangeHint : undefined,
className: joinClassNames(generateClassName(options.buttonGroupClass, { hasSelection: isOnlyView }), classNames.isolate),
}, ...children);
}
return children[0];
}
}
class Toolbar extends BaseComponent {
render() {
let { props } = this;
let options = this.context.options;
let { sectionWidgets } = props.model;
const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
const toolbarClassOption = props.isHeader ? options.headerToolbarClass : options.footerToolbarClass;
return (jsxs("div", { className: joinClassNames(generateClassName(toolbarClassOption, { borderlessX, borderlessTop, borderlessBottom }), generateClassName(options.toolbarClass, { borderlessX, borderlessTop, borderlessBottom })), children: [this.renderSection('start', sectionWidgets.start), this.renderSection('center', sectionWidgets.center), this.renderSection('end', sectionWidgets.end)] }));
}
renderSection(name, widgetGroups) {
let { props } = this;
return (jsx(ToolbarSection, { name: name, widgetGroups: widgetGroups, title: props.title, titleId: props.titleId, navUnit: props.navUnit, selectedButton: props.selectedButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled }, name));
}
}
/*
Detects when the user clicks on an event within a DateComponent
*/
class EventClicking extends Interaction {
constructor(settings) {
super(settings);
this.handleSegClick = (ev, segEl) => {
let { component } = this;
let { context } = component;
let eventRange = getElEventRange(segEl);
if (eventRange && // might be the <div> surrounding the more link
component.isValidSegDownEl(ev.target)) {
context.emitter.trigger('eventClick', {
el: segEl,
event: new EventImpl(component.context, eventRange.def, eventRange.instance),
jsEvent: ev, // Is this always a mouse event? See #4655
view: context.viewApi,
});
}
};
this.destroy = listenBySelector(settings.el, 'click', `.${classNames.internalEvent}`, // on both fg and bg events
this.handleSegClick);
}
}
/*
Triggers events and adds/removes core classNames when the user's pointer
enters/leaves event-elements of a component.
*/
class EventHovering extends Interaction {
constructor(settings) {
super(settings);
// for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
this.handleEventElRemove = (el) => {
if (el === this.currentSegEl) {
this.handleSegLeave(null, this.currentSegEl);
}
};
this.handleSegEnter = (ev, segEl) => {
if (getElEventRange(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper
this.currentSegEl = segEl;
this.triggerEvent('eventMouseEnter', ev, segEl);
}
};
this.handleSegLeave = (ev, segEl) => {
if (this.currentSegEl) {
this.currentSegEl = null;
this.triggerEvent('eventMouseLeave', ev, segEl);
}
};
this.removeHoverListeners = listenToHoverBySelector(settings.el, `.${classNames.internalEvent}`, // on both fg and bg events
this.handleSegEnter, this.handleSegLeave);
}
destroy() {
this.removeHoverListeners();
}
triggerEvent(publicEvName, ev, segEl) {
let { component } = this;
let { context } = component;
let eventRange = getElEventRange(segEl);
if (!ev || component.isValidSegDownEl(ev.target)) {
context.emitter.trigger(publicEvName, {
el: segEl,
event: new EventImpl(context, eventRange.def, eventRange.instance),
jsEvent: ev, // Is this always a mouse event? See #4655
view: context.viewApi,
});
}
}
}
class CalendarInner extends PureComponent {
constructor() {
super(...arguments);
this.buildViewContext = memoize(buildViewContext);
this.buildViewPropTransformers = memoize(buildViewPropTransformers);
this.interactionsStore = {};
this.calendarInteractions = [];
this.registerInteractiveComponent = (component, settingsInput) => {
let settings = parseInteractionSettings(component, settingsInput);
let DEFAULT_INTERACTIONS = [
EventClicking,
EventHovering,
];
let interactionClasses = DEFAULT_INTERACTIONS;
if (!settingsInput.disableHits) {
interactionClasses = interactionClasses.concat(this.props.pluginHooks.componentInteractions);
}
let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings));
this.interactionsStore[component.uid] = interactions;
interactionSettingsStore[component.uid] = settings;
};
this.unregisterInteractiveComponent = (component) => {
let listeners = this.interactionsStore[component.uid];
if (listeners) {
for (let listener of listeners) {
listener.destroy();
}
delete this.interactionsStore[component.uid];
}
delete interactionSettingsStore[component.uid];
};
}
get viewTitleId() {
return this.props.baseId + 'title';
}
render() {
const { props } = this;
let { toolbarConfig, options } = props;
let viewHeight;
let viewHeightLiquid = false;
let viewAspectRatio;
if (props.forPrint || getIsHeightAuto(options)) ;
else if (options.height != null) {
viewHeightLiquid = true;
}
else if (options.contentHeight != null) {
viewHeight = options.contentHeight;
}
else {
viewAspectRatio = Math.max(options.aspectRatio, 0.5); // prevent from getting too tall
}
let viewContext = this.buildViewContext(props.viewSpec, props.viewApi, props.options, props.dateProfileGenerator, props.dateEnv, props.nowManager, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, props.baseId, this.registerInteractiveComponent, this.unregisterInteractiveComponent);
return (jsxs(ViewContextType.Provider, { value: viewContext, children: [toolbarConfig.header && (jsx(Toolbar, { model: toolbarConfig.header, isHeader: true, titleId: this.viewTitleId, ...props.toolbarProps })), jsxs("div", { className: joinClassNames(classNames.flexCol, classNames.rel,
// prevents browsers' "scroll anchoring behavior", which cause scroll thrashing
// when clicking "Next" for month-view, because rows would flex-grow while other rows
// temporarily removed. This behavior probably universally unhelpful for our uses,
// esp with virtualization, but maybe in future put on more specific row-based parents
classNames.overflowAnchorNone,
// workaround for Safari pushing content area extremely wide after returning from
// print-view. probably a good idea regardless, to circumvent 'auto' dimentions
classNames.minHeight0, viewHeightLiquid && classNames.liquid), style: {
height: viewHeight,
aspectRatio: viewAspectRatio != null ? String(viewAspectRatio) : undefined,
}, children: [this.renderView(joinClassNames((viewHeightLiquid || viewHeight) && classNames.liquid, viewAspectRatio != null && classNames.fill, classNames.internalView)), this.buildAppendContent()] }), toolbarConfig.footer && (jsx(Toolbar, { model: toolbarConfig.footer, isHeader: false, ...props.toolbarProps }))] }));
}
renderView(className) {
const { props } = this;
const { pluginHooks, viewSpec, toolbarConfig, toolbarProps } = props;
let viewProps = {
className,
dateProfile: props.dateProfile,
businessHours: props.businessHours,
eventStore: props.renderableEventStore, // !
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize,
forPrint: props.forPrint,
labelId: toolbarConfig.header && toolbarConfig.header.hasTitle ? this.viewTitleId : undefined,
labelStr: toolbarConfig.header && toolbarConfig.header.hasTitle ? undefined : toolbarProps.title,
};
let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers);
let contentProps = {
...props,
toolbarProps,
forPrint: props.forPrint,
};
for (let transformer of transformers) {
Object.assign(viewProps, transformer.transform(viewProps, contentProps));
}
let ViewComponent = viewSpec.component;
return (jsx(ViewComponent, { ...viewProps }));
}
buildAppendContent() {
const { props } = this;
return (jsx(Fragment, { children: props.pluginHooks.viewContainerAppends.map((buildAppendContent, i) => (jsx(Fragment$1, { children: buildAppendContent(props) }, i))) }));
}
// BE AWARE React StrictMode might execute this twice
componentDidMount() {
const { props } = this;
this.calendarInteractions = props.pluginHooks.calendarInteractions
.map((CalendarInteractionClass) => new CalendarInteractionClass(props));
let { propSetHandlers } = props.pluginHooks;
for (let propName in propSetHandlers) {
propSetHandlers[propName](props[propName], props);
}
// call contextInit
for (let callback of props.pluginHooks.contextInit) {
callback(props);
}
}
componentDidUpdate(prevProps) {
const { props } = this;
let { propSetHandlers } = props.pluginHooks;
for (let propName in propSetHandlers) {
if (props[propName] !== prevProps[propName]) {
propSetHandlers[propName](props[propName], props);
}
}
}
// BE AWARE React StrictMode might execute this twice
componentWillUnmount() {
const { props } = this;
for (let interaction of this.calendarInteractions) {
interaction.destroy();
}
this.calendarInteractions = [];
// will likely undo what was done by contextInit
props.emitter.trigger('_unmount');
}
}
function buildViewPropTransformers(theClasses) {
return theClasses.map((TheClass) => new TheClass());
}
export { CalendarApiImpl as C, CalendarMediaRoot as a, CalendarInner as b, computeRootClassName as c, CalendarDataManager as d, parseBusinessHours as p };