fullcalendar
Version:
FullCalendar Vanilla JS package for rendering a calendar
1,120 lines (1,102 loc) • 87.5 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 './23b3908a.js';
import { m as memoize, a as memoizeObjArg, N as NowTimerRunner, g as generateClassName, P as PureComponent, b as buildViewContext, c as getIsHeightAuto, V as ViewContextType, B as BaseComponent, C as ContentContainer } from './66c53e04.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 './2add5508.js';
import { m as mergeCalendarOptions, i as isMergedPropsEqual, a as mergeViewOptionsMap } from './f0fa24ec.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 './69261bb4.js';
import { D as DateProfileGenerator } from './ad0c00be.js';
import { r as reduceEventStore, a as rezoneEventStoreDates, I as Interaction, p as parseInteractionSettings, i as interactionSettingsStore } from './d5a70381.js';
import { E as Emitter } from './56f74c4a.js';
import { c as classNames } from './4a45af02.js';
import { Component, flushSync, 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?
this.nowAnchorDate = dateEnv.toDate(nowInput
? dateEnv.createMarker(nowInput)
: dateEnv.createNowMarker());
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.nowAnchorDate
? this.dateEnv.timestampToMarker(this.nowAnchorDate.valueOf() +
(Date.now() - this.nowAnchorQueried))
: this.dateEnv.createMarker(this.nowFn());
}
addResetListener(handler) {
this.resetListeners.add(handler);
}
removeResetListener(handler) {
this.resetListeners.delete(handler);
}
}
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 resu