UNPKG

fullcalendar

Version:

FullCalendar Vanilla JS package for rendering a calendar

1,120 lines (1,102 loc) 87.5 kB
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