'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var internalCommon = require('./internal-common.cjs'); var preact = require('preact'); require('preact/compat'); const globalLocales = []; const MINIMAL_RAW_EN_LOCALE = { code: 'en', week: { dow: 0, doy: 4, // 4 days need to be within the year to be considered the first week }, direction: 'ltr', buttonText: { prev: 'prev', next: 'next', prevYear: 'prev year', nextYear: 'next year', year: 'year', today: 'today', month: 'month', week: 'week', day: 'day', list: 'list', }, weekText: 'W', weekTextLong: 'Week', closeHint: 'Close', timeHint: 'Time', eventHint: 'Event', allDayText: 'all-day', moreLinkText: 'more', noEventsText: 'No events to display', }; const RAW_EN_LOCALE = Object.assign(Object.assign({}, MINIMAL_RAW_EN_LOCALE), { // Includes things we don't want other locales to inherit, // things that derive from other translatable strings. buttonHints: { prev: 'Previous $0', next: 'Next $0', today(buttonText, unit) { return (unit === 'day') ? 'Today' : `This ${buttonText}`; }, }, viewHint: '$0 view', navLinkHint: 'Go to $0', moreLinkHint(eventCnt) { return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`; } }); function organizeRawLocales(explicitRawLocales) { let defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en'; let allRawLocales = globalLocales.concat(explicitRawLocales); let rawLocaleMap = { en: RAW_EN_LOCALE, }; for (let rawLocale of allRawLocales) { rawLocaleMap[rawLocale.code] = rawLocale; } return { map: rawLocaleMap, defaultCode, }; } function buildLocale(inputSingular, available) { if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) { return parseLocale(inputSingular.code, [inputSingular.code], inputSingular); } return queryLocale(inputSingular, available); } function queryLocale(codeArg, available) { let codes = [].concat(codeArg || []); // will convert to array let raw = queryRawLocale(codes, available) || RAW_EN_LOCALE; return parseLocale(codeArg, codes, raw); } function queryRawLocale(codes, available) { for (let i = 0; i < codes.length; i += 1) { let parts = codes[i].toLocaleLowerCase().split('-'); for (let j = parts.length; j > 0; j -= 1) { let simpleId = parts.slice(0, j).join('-'); if (available[simpleId]) { return available[simpleId]; } } } return null; } function parseLocale(codeArg, codes, raw) { let merged = internalCommon.mergeProps([MINIMAL_RAW_EN_LOCALE, raw], ['buttonText']); delete merged.code; // don't want this part of the options let { week } = merged; delete merged.week; return { codeArg, codes, week, simpleNumberFormat: new Intl.NumberFormat(codeArg), options: merged, }; } // TODO: easier way to add new hooks? need to update a million things function createPlugin(input) { return { id: internalCommon.guid(), name: input.name, premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate) : undefined, deps: input.deps || [], 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 || [], themeClasses: input.themeClasses || {}, eventSourceDefs: input.eventSourceDefs || [], cmdFormatter: input.cmdFormatter, recurringTypes: input.recurringTypes || [], namedTimeZonedImpl: input.namedTimeZonedImpl, initialView: input.initialView || '', elementDraggingImpl: input.elementDraggingImpl, optionChangeHandlers: input.optionChangeHandlers || {}, scrollGridImpl: input.scrollGridImpl || null, listenerRefiners: input.listenerRefiners || {}, optionRefiners: input.optionRefiners || {}, propSetHandlers: input.propSetHandlers || {}, }; } function buildPluginHooks(pluginDefs, globalDefs) { let currentPluginIds = {}; 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: [], themeClasses: {}, eventSourceDefs: [], cmdFormatter: null, recurringTypes: [], namedTimeZonedImpl: null, initialView: '', elementDraggingImpl: null, optionChangeHandlers: {}, scrollGridImpl: null, listenerRefiners: {}, optionRefiners: {}, propSetHandlers: {}, }; function addDefs(defs) { for (let def of defs) { const pluginName = def.name; const currentId = currentPluginIds[pluginName]; if (currentId === undefined) { currentPluginIds[pluginName] = def.id; addDefs(def.deps); hooks = combineHooks(hooks, def); } else if (currentId !== def.id) { // different ID than the one already added console.warn(`Duplicate plugin '${pluginName}'`); } } } if (pluginDefs) { addDefs(pluginDefs); } addDefs(globalDefs); return hooks; } function buildBuildPluginHooks() { let currentOverrideDefs = []; let currentGlobalDefs = []; let currentHooks; return (overrideDefs, globalDefs) => { if (!currentHooks || !internalCommon.isArraysEqual(overrideDefs, currentOverrideDefs) || !internalCommon.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: Object.assign(Object.assign({}, hooks0.eventRefiners), hooks1.eventRefiners), eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders), eventSourceRefiners: Object.assign(Object.assign({}, 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: Object.assign(Object.assign({}, 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), themeClasses: Object.assign(Object.assign({}, hooks0.themeClasses), hooks1.themeClasses), eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs), cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter, recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes), namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl, initialView: hooks0.initialView || hooks1.initialView, elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, optionChangeHandlers: Object.assign(Object.assign({}, hooks0.optionChangeHandlers), hooks1.optionChangeHandlers), scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl, listenerRefiners: Object.assign(Object.assign({}, hooks0.listenerRefiners), hooks1.listenerRefiners), optionRefiners: Object.assign(Object.assign({}, hooks0.optionRefiners), hooks1.optionRefiners), propSetHandlers: Object.assign(Object.assign({}, 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())); } class StandardTheme extends internalCommon.Theme { } StandardTheme.prototype.classes = { root: 'fc-theme-standard', tableCellShaded: 'fc-cell-shaded', buttonGroup: 'fc-button-group', button: 'fc-button fc-button-primary', buttonActive: 'fc-button-active', }; StandardTheme.prototype.baseIconClass = 'fc-icon'; StandardTheme.prototype.iconClasses = { close: 'fc-icon-x', prev: 'fc-icon-chevron-left', next: 'fc-icon-chevron-right', prevYear: 'fc-icon-chevrons-left', nextYear: 'fc-icon-chevrons-right', }; StandardTheme.prototype.rtlIconClasses = { prev: 'fc-icon-chevron-right', next: 'fc-icon-chevron-left', prevYear: 'fc-icon-chevrons-right', nextYear: 'fc-icon-chevrons-left', }; StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; // TODO: make TS-friendly StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'; StandardTheme.prototype.iconOverridePrefix = 'fc-icon-'; 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: Object.assign(Object.assign({}, (superDef ? superDef.defaults : {})), (defaultConfig ? defaultConfig.rawOptions : {})), overrides: Object.assign(Object.assign({}, (superDef ? superDef.overrides : {})), (overrideConfig ? overrideConfig.rawOptions : {})), }; } function parseViewConfigs(inputs) { return internalCommon.mapHash(inputs, parseViewConfig); } function parseViewConfig(input) { let rawOptions = typeof input === 'function' ? { component: input } : input; let { component } = rawOptions; if (rawOptions.content) { // TODO: remove content/classNames/didMount/etc from options? component = createViewHookComponent(rawOptions); } else if (component && !(component.prototype instanceof internalCommon.BaseComponent)) { // WHY?: people were using `component` property for `content` // TODO: converge on one setting name component = createViewHookComponent(Object.assign(Object.assign({}, rawOptions), { content: component })); } return { superType: rawOptions.type, component: component, rawOptions, // includes type and component too :( }; } function createViewHookComponent(options) { return (viewProps) => (preact.createElement(internalCommon.ViewContextType.Consumer, null, (context) => (preact.createElement(internalCommon.ContentContainer, { elTag: "div", elClasses: internalCommon.buildViewClassNames(context.viewSpec), renderProps: Object.assign(Object.assign({}, viewProps), { nextDayThreshold: context.options.nextDayThreshold }), generatorName: undefined, customGenerator: options.content, classNameGenerator: options.classNames, didMount: options.didMount, willUnmount: options.willUnmount })))); } function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides, localeDefaults) { let defaultConfigs = parseViewConfigs(defaultInputs); let overrideConfigs = parseViewConfigs(optionOverrides.views); let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs); return internalCommon.mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults)); } function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults) { 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 = internalCommon.greatestDurationDenominator(duration); durationUnit = denom.unit; if (denom.value === 1) { singleUnit = durationUnit; singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {}; } } } let queryButtonText = (optionsSubset) => { let buttonTextMap = optionsSubset.buttonText || {}; let buttonTextKey = viewDef.defaults.buttonTextKey; if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) { return buttonTextMap[buttonTextKey]; } if (buttonTextMap[viewDef.type] != null) { return buttonTextMap[viewDef.type]; } if (buttonTextMap[singleUnit] != null) { return buttonTextMap[singleUnit]; } return null; }; let queryButtonTitle = (optionsSubset) => { let buttonHints = optionsSubset.buttonHints || {}; let buttonKey = viewDef.defaults.buttonTextKey; // use same key as text if (buttonKey != null && buttonHints[buttonKey] != null) { return buttonHints[buttonKey]; } if (buttonHints[viewDef.type] != null) { return buttonHints[viewDef.type]; } if (buttonHints[singleUnit] != null) { return buttonHints[singleUnit]; } return null; }; return { type: viewDef.type, component: viewDef.component, duration, durationUnit, singleUnit, optionDefaults: viewDef.defaults, optionOverrides: Object.assign(Object.assign({}, singleUnitOverrides), viewDef.overrides), buttonTextOverride: queryButtonText(dynamicOptionOverrides) || queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence viewDef.overrides.buttonText, buttonTextDefault: queryButtonText(localeDefaults) || viewDef.defaults.buttonText || queryButtonText(internalCommon.BASE_OPTION_DEFAULTS) || viewDef.type, // not DRY buttonTitleOverride: queryButtonTitle(dynamicOptionOverrides) || queryButtonTitle(optionOverrides) || viewDef.overrides.buttonHint, buttonTitleDefault: queryButtonTitle(localeDefaults) || viewDef.defaults.buttonHint || queryButtonTitle(internalCommon.BASE_OPTION_DEFAULTS), // will eventually fall back to buttonText }; } // hack to get memoization working let durationInputMap = {}; function createDurationCached(durationInput) { let json = JSON.stringify(durationInput); let res = durationInputMap[json]; if (res === undefined) { res = internalCommon.createDuration(durationInput); durationInputMap[json] = res; } return res; } function reduceViewType(viewType, action) { switch (action.type) { case 'CHANGE_VIEW_TYPE': viewType = action.viewType; } return viewType; } function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) { switch (action.type) { case 'SET_OPTION': return Object.assign(Object.assign({}, dynamicOptionOverrides), { [action.optionName]: action.rawOptionValue }); default: return dynamicOptionOverrides; } } function reduceDateProfile(currentDateProfile, action, currentDate, dateProfileGenerator) { let dp; switch (action.type) { case 'CHANGE_VIEW_TYPE': return dateProfileGenerator.build(action.dateMarker || currentDate); case 'CHANGE_DATE': return dateProfileGenerator.build(action.dateMarker); case 'PREV': dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate); if (dp.isValid) { return dp; } break; case 'NEXT': dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate); if (dp.isValid) { return dp; } break; } return currentDateProfile; } function initEventSources(calendarOptions, dateProfile, context) { let activeRange = dateProfile ? dateProfile.activeRange : null; return addSources({}, parseInitialSources(calendarOptions, context), activeRange, context); } function reduceEventSources(eventSources, action, dateProfile, context) { let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check? switch (action.type) { case 'ADD_EVENT_SOURCES': // already parsed return addSources(eventSources, action.sources, activeRange, context); case 'REMOVE_EVENT_SOURCE': return removeSource(eventSources, action.sourceId); case 'PREV': // TODO: how do we track all actions that affect dateProfile :( case 'NEXT': case 'CHANGE_DATE': case 'CHANGE_VIEW_TYPE': if (dateProfile) { return fetchDirtySources(eventSources, activeRange, context); } return eventSources; case 'FETCH_EVENT_SOURCES': return fetchSourcesByIds(eventSources, action.sourceIds ? // why no type? internalCommon.arrayToHash(action.sourceIds) : excludeStaticSources(eventSources, context), activeRange, action.isRefetch || false, context); case 'RECEIVE_EVENTS': case 'RECEIVE_EVENT_ERROR': return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange); case 'REMOVE_ALL_EVENT_SOURCES': return {}; default: return eventSources; } } function reduceEventSourcesNewTimeZone(eventSources, dateProfile, context) { let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check? return fetchSourcesByIds(eventSources, excludeStaticSources(eventSources, context), activeRange, true, context); } function computeEventSourcesLoading(eventSources) { for (let sourceId in eventSources) { if (eventSources[sourceId].isFetching) { return true; } } return false; } function addSources(eventSourceHash, sources, fetchRange, context) { let hash = {}; for (let source of sources) { hash[source.sourceId] = source; } if (fetchRange) { hash = fetchDirtySources(hash, fetchRange, context); } return Object.assign(Object.assign({}, eventSourceHash), hash); } function removeSource(eventSourceHash, sourceId) { return internalCommon.filterHash(eventSourceHash, (eventSource) => eventSource.sourceId !== sourceId); } function fetchDirtySources(sourceHash, fetchRange, context) { return fetchSourcesByIds(sourceHash, internalCommon.filterHash(sourceHash, (eventSource) => isSourceDirty(eventSource, fetchRange, context)), fetchRange, false, context); } function isSourceDirty(eventSource, fetchRange, context) { if (!doesSourceNeedRange(eventSource, context)) { return !eventSource.latestFetchId; } return !context.options.lazyFetching || !eventSource.fetchRange || eventSource.isFetching || // always cancel outdated in-progress fetches fetchRange.start < eventSource.fetchRange.start || fetchRange.end > eventSource.fetchRange.end; } function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, isRefetch, context) { let nextSources = {}; for (let sourceId in prevSources) { let source = prevSources[sourceId]; if (sourceIdHash[sourceId]) { nextSources[sourceId] = fetchSource(source, fetchRange, isRefetch, context); } else { nextSources[sourceId] = source; } } return nextSources; } function fetchSource(eventSource, fetchRange, isRefetch, context) { let { options, calendarApi } = context; let sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId]; let fetchId = internalCommon.guid(); sourceDef.fetch({ eventSource, range: fetchRange, isRefetch, context, }, (res) => { let { rawEvents } = res; if (options.eventSourceSuccess) { rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.response) || rawEvents; } if (eventSource.success) { rawEvents = eventSource.success.call(calendarApi, rawEvents, res.response) || rawEvents; } context.dispatch({ type: 'RECEIVE_EVENTS', sourceId: eventSource.sourceId, fetchId, fetchRange, rawEvents, }); }, (error) => { let errorHandled = false; if (options.eventSourceFailure) { options.eventSourceFailure.call(calendarApi, error); errorHandled = true; } if (eventSource.failure) { eventSource.failure(error); errorHandled = true; } if (!errorHandled) { console.warn(error.message, error); } context.dispatch({ type: 'RECEIVE_EVENT_ERROR', sourceId: eventSource.sourceId, fetchId, fetchRange, error, }); }); return Object.assign(Object.assign({}, eventSource), { isFetching: true, latestFetchId: fetchId }); } function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) { let eventSource = sourceHash[sourceId]; if (eventSource && // not already removed fetchId === eventSource.latestFetchId) { return Object.assign(Object.assign({}, sourceHash), { [sourceId]: Object.assign(Object.assign({}, eventSource), { isFetching: false, fetchRange }) }); } return sourceHash; } function excludeStaticSources(eventSources, context) { return internalCommon.filterHash(eventSources, (eventSource) => doesSourceNeedRange(eventSource, context)); } function parseInitialSources(rawOptions, context) { let refiners = internalCommon.buildEventSourceRefiners(context); let rawSources = [].concat(rawOptions.eventSources || []); let sources = []; // parsed if (rawOptions.initialEvents) { rawSources.unshift(rawOptions.initialEvents); } if (rawOptions.events) { rawSources.unshift(rawOptions.events); } for (let rawSource of rawSources) { let source = internalCommon.parseEventSource(rawSource, context, refiners); if (source) { sources.push(source); } } return sources; } function doesSourceNeedRange(eventSource, context) { let defs = context.pluginHooks.eventSourceDefs; return !defs[eventSource.sourceDefId].ignoreRange; } 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, calendarOptionOverrides, theme, viewSpecs, calendarApi) { let header = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null; let footer = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null; return { header, footer }; } function parseToolbar(sectionStrHash, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) { let sectionWidgets = {}; let viewsWithButtons = []; let hasTitle = false; for (let sectionName in sectionStrHash) { let sectionStr = sectionStrHash[sectionName]; let sectionRes = parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi); sectionWidgets[sectionName] = sectionRes.widgets; viewsWithButtons.push(...sectionRes.viewsWithButtons); hasTitle = hasTitle || sectionRes.hasTitle; } return { sectionWidgets, viewsWithButtons, hasTitle }; } /* BAD: querying icons and text here. should be done at render time */ function parseSection(sectionStr, calendarOptions, // defaults+overrides, then refined calendarOptionOverrides, // overrides only!, unrefined :( theme, viewSpecs, calendarApi) { let isRtl = calendarOptions.direction === 'rtl'; let calendarCustomButtons = calendarOptions.customButtons || {}; let calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {}; let calendarButtonText = calendarOptions.buttonText || {}; let calendarButtonHintOverrides = calendarOptionOverrides.buttonHints || {}; let calendarButtonHints = calendarOptions.buttonHints || {}; let sectionSubstrs = sectionStr ? sectionStr.split(' ') : []; let viewsWithButtons = []; let hasTitle = false; let widgets = sectionSubstrs.map((buttonGroupStr) => (buttonGroupStr.split(',').map((buttonName) => { if (buttonName === 'title') { hasTitle = true; return { buttonName }; } let customButtonProps; let viewSpec; let buttonClick; let buttonIcon; // only one of these will be set let buttonText; // " let buttonHint; // ^ for the title="" attribute, for accessibility if ((customButtonProps = calendarCustomButtons[buttonName])) { buttonClick = (ev) => { if (customButtonProps.click) { customButtonProps.click.call(ev.target, ev, ev.target); // TODO: use Calendar this context? } }; (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) || (buttonIcon = theme.getIconClass(buttonName, isRtl)) || (buttonText = customButtonProps.text); buttonHint = customButtonProps.hint || customButtonProps.text; } else if ((viewSpec = viewSpecs[buttonName])) { viewsWithButtons.push(buttonName); buttonClick = () => { calendarApi.changeView(buttonName); }; (buttonText = viewSpec.buttonTextOverride) || (buttonIcon = theme.getIconClass(buttonName, isRtl)) || (buttonText = viewSpec.buttonTextDefault); let textFallback = viewSpec.buttonTextOverride || viewSpec.buttonTextDefault; buttonHint = internalCommon.formatWithOrdinals(viewSpec.buttonTitleOverride || viewSpec.buttonTitleDefault || calendarOptions.viewHint, [textFallback, buttonName], // view-name = buttonName textFallback); } else if (calendarApi[buttonName]) { // a calendarApi method buttonClick = () => { calendarApi[buttonName](); }; (buttonText = calendarButtonTextOverrides[buttonName]) || (buttonIcon = theme.getIconClass(buttonName, isRtl)) || (buttonText = calendarButtonText[buttonName]); // everything else is considered default if (buttonName === 'prevYear' || buttonName === 'nextYear') { let prevOrNext = buttonName === 'prevYear' ? 'prev' : 'next'; buttonHint = internalCommon.formatWithOrdinals(calendarButtonHintOverrides[prevOrNext] || calendarButtonHints[prevOrNext], [ calendarButtonText.year || 'year', 'year', ], calendarButtonText[buttonName]); } else { buttonHint = (navUnit) => internalCommon.formatWithOrdinals(calendarButtonHintOverrides[buttonName] || calendarButtonHints[buttonName], [ calendarButtonText[navUnit] || navUnit, navUnit, ], calendarButtonText[buttonName]); } } return { buttonName, buttonClick, buttonIcon, buttonText, buttonHint }; }))); 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 } } let eventSourceDef$2 = { ignoreRange: true, parseMeta(refined) { if (Array.isArray(refined.events)) { return refined.events; } return null; }, fetch(arg, successCallback) { successCallback({ rawEvents: arg.eventSource.meta, }); }, }; const arrayEventSourcePlugin = createPlugin({ name: 'array-event-source', eventSourceDefs: [eventSourceDef$2], }); let eventSourceDef$1 = { parseMeta(refined) { if (typeof refined.events === 'function') { return refined.events; } return null; }, fetch(arg, successCallback, errorCallback) { const { dateEnv } = arg.context; const func = arg.eventSource.meta; internalCommon.unpromisify(func.bind(null, internalCommon.buildRangeApiWithTimeZone(arg.range, dateEnv)), (rawEvents) => successCallback({ rawEvents }), errorCallback); }, }; const funcEventSourcePlugin = createPlugin({ name: 'func-event-source', eventSourceDefs: [eventSourceDef$1], }); const JSON_FEED_EVENT_SOURCE_REFINERS = { method: String, extraParams: internalCommon.identity, startParam: String, endParam: String, timeZoneParam: String, }; let eventSourceDef = { parseMeta(refined) { if (refined.url && (refined.format === 'json' || !refined.format)) { return { url: refined.url, format: 'json', method: (refined.method || 'GET').toUpperCase(), extraParams: refined.extraParams, startParam: refined.startParam, endParam: refined.endParam, timeZoneParam: refined.timeZoneParam, }; } return null; }, fetch(arg, successCallback, errorCallback) { const { meta } = arg.eventSource; const requestParams = buildRequestParams(meta, arg.range, arg.context); internalCommon.requestJson(meta.method, meta.url, requestParams).then(([rawEvents, response]) => { successCallback({ rawEvents, response }); }, errorCallback); }, }; const jsonFeedEventSourcePlugin = createPlugin({ name: 'json-event-source', eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS, eventSourceDefs: [eventSourceDef], }); function buildRequestParams(meta, range, context) { let { dateEnv, options } = context; let startParam; let endParam; let timeZoneParam; let customRequestParams; let params = {}; startParam = meta.startParam; if (startParam == null) { startParam = options.startParam; } endParam = meta.endParam; if (endParam == null) { endParam = options.endParam; } timeZoneParam = meta.timeZoneParam; if (timeZoneParam == null) { timeZoneParam = options.timeZoneParam; } // retrieve any outbound GET/POST data from the options if (typeof meta.extraParams === 'function') { // supplied as a function that returns a key/value object customRequestParams = meta.extraParams(); } else { // probably supplied as a straight key/value object customRequestParams = meta.extraParams || {}; } Object.assign(params, customRequestParams); params[startParam] = dateEnv.formatIso(range.start); params[endParam] = dateEnv.formatIso(range.end); if (dateEnv.timeZone !== 'local') { params[timeZoneParam] = dateEnv.timeZone; } return params; } const SIMPLE_RECURRING_REFINERS = { daysOfWeek: internalCommon.identity, startTime: internalCommon.createDuration, endTime: internalCommon.createDuration, duration: internalCommon.createDuration, startRecur: internalCommon.identity, endRecur: internalCommon.identity, }; let recurring = { parse(refined, dateEnv) { if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) { let recurringData = { daysOfWeek: refined.daysOfWeek || null, startTime: refined.startTime || null, endTime: refined.endTime || null, startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null, endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null, }; let duration; if (refined.duration) { duration = refined.duration; } if (!duration && refined.startTime && refined.endTime) { duration = internalCommon.subtractDurations(refined.endTime, refined.startTime); } return { allDayGuess: Boolean(!refined.startTime && !refined.endTime), duration, typeData: recurringData, // doesn't need endTime anymore but oh well }; } return null; }, expand(typeData, framingRange, dateEnv) { let clippedFramingRange = internalCommon.intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur }); if (clippedFramingRange) { return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv); } return []; }, }; const simpleRecurringEventsPlugin = createPlugin({ name: 'simple-recurring-event', recurringTypes: [recurring], eventRefiners: SIMPLE_RECURRING_REFINERS, }); function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) { let dowHash = daysOfWeek ? internalCommon.arrayToHash(daysOfWeek) : null; let dayMarker = internalCommon.startOfDay(framingRange.start); let endMarker = framingRange.end; let instanceStarts = []; while (dayMarker < endMarker) { let instanceStart; // if everyday, or this particular day-of-week if (!dowHash || dowHash[dayMarker.getUTCDay()]) { if (startTime) { instanceStart = dateEnv.add(dayMarker, startTime); } else { instanceStart = dayMarker; } instanceStarts.push(instanceStart); } dayMarker = internalCommon.addDays(dayMarker, 1); } return instanceStarts; } const changeHandlerPlugin = createPlugin({ name: 'change-handler', optionChangeHandlers: { events(events, context) { handleEventSources([events], context); }, eventSources: handleEventSources, }, }); /* BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out */ function handleEventSources(inputs, context) { let unfoundSources = internalCommon.hashValuesToArray(context.getCurrentData().eventSources); if (unfoundSources.length === 1 && inputs.length === 1 && Array.isArray(unfoundSources[0]._raw) && Array.isArray(inputs[0])) { context.dispatch({ type: 'RESET_RAW_EVENTS', sourceId: unfoundSources[0].sourceId, rawEvents: inputs[0], }); return; } let newInputs = []; for (let input of inputs) { let inputFound = false; for (let i = 0; i < unfoundSources.length; i += 1) { if (unfoundSources[i]._raw === input) { unfoundSources.splice(i, 1); // delete inputFound = true; break; } } if (!inputFound) { newInputs.push(input); } } for (let unfoundSource of unfoundSources) { context.dispatch({ type: 'REMOVE_EVENT_SOURCE', sourceId: unfoundSource.sourceId, }); } for (let newInput of newInputs) { context.calendarApi.addEventSource(newInput); } } function handleDateProfile(dateProfile, context) { context.emitter.trigger('datesSet', Object.assign(Object.assign({}, internalCommon.buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv)), { view: context.viewApi })); } function handleEventStore(eventStore, context) { let { emitter } = context; if (emitter.hasHandlers('eventsSet')) { emitter.trigger('eventsSet', internalCommon.buildEventApis(eventStore, context)); } } /* this array is exposed on the root namespace so that UMD plugins can add to it. see the rollup-bundles script. */ const globalPlugins = [ arrayEventSourcePlugin, funcEventSourcePlugin, jsonFeedEventSourcePlugin, simpleRecurringEventsPlugin, changeHandlerPlugin, createPlugin({ name: 'misc', isLoadingFuncs: [ (state) => computeEventSourcesLoading(state.eventSources), ], propSetHandlers: { dateProfile: handleDateProfile, eventStore: handleEventStore, }, }), ]; class TaskRunner { constructor(runTaskOption, drainedOption) { this.runTaskOption = runTaskOption; this.drainedOption = drainedOption; this.queue = []; this.delayedRunner = new internalCommon.DelayedRunner(this.drain.bind(this)); } request(task, delay) { this.queue.push(task); this.delayedRunner.request(delay); } pause(scope) { this.delayedRunner.pause(scope); } resume(scope, force) { this.delayedRunner.resume(scope, force); } drain() { let { queue } = this; while (queue.length) { let completedTasks = []; let task; while ((task = queue.shift())) { this.runTask(task); completedTasks.push(task); } this.drained(completedTasks); } // keep going, in case new tasks were added in the drained handler } runTask(task) { if (this.runTaskOption) { this.runTaskOption(task); } } drained(completedTasks) { if (this.drainedOption) { this.drainedOption(completedTasks); } } } // 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; } return dateEnv.formatRange(range.start, range.end, internalCommon.createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), { isEndExclusive: dateProfile.isRangeAllDay, defaultSeparator: viewOptions.titleRangeSeparator, }); } // 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) { let { currentRangeUnit } = dateProfile; if (currentRangeUnit === 'year') { return { year: 'numeric' }; } if (currentRangeUnit === 'month') { return { year: 'numeric', month: 'long' }; // like "September 2014" } let days = internalCommon.diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end); if (days !== null && days > 1) { // multi-day range. shorter, like "Sep 9 - 10 2014" return { year: 'numeric', month: 'short', day: 'numeric' }; } // one day. longer, like "September 9 2014" return { year: 'numeric', month: 'long', day: 'numeric' }; } // in future refactor, do the redux-style function(state=initial) for initial-state // also, whatever is happening in constructor, have it happen in action queue too class CalendarDataManager { constructor(props) { this.computeCurrentViewData = internalCommon.memoize(this._computeCurrentViewData); this.organizeRawLocales = internalCommon.memoize(organizeRawLocales); this.buildLocale = internalCommon.memoize(buildLocale); this.buildPluginHooks = buildBuildPluginHooks(); this.buildDateEnv = internalCommon.memoize(buildDateEnv$1); this.buildTheme = internalCommon.memoize(buildTheme); this.parseToolbars = internalCommon.memoize(parseToolbars); this.buildViewSpecs = internalCommon.memoize(buildViewSpecs); this.buildDateProfileGenerator = internalCommon.memoizeObjArg(buildDateProfileGenerator); this.buildViewApi = internalCommon.memoize(buildViewApi); this.buildViewUiProps = internalCommon.memoizeObjArg(buildViewUiProps); this.buildEventUiBySource = internalCommon.memoize(buildEventUiBySource, internalCommon.isPropsEqual); this.buildEventUiBases = internalCommon.memoize(buildEventUiBases); this.parseContextBusinessHours = internalCommon.memoizeObjArg(parseContextBusinessHours); this.buildTitle = internalCommon.memoize(buildTitle); this.emitter = new internalCommon.Emitter(); this.actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this)); this.currentCalendarOptionsInput = {}; this.currentCalendarOptionsRefined = {}; this.currentViewOptionsInput = {}; this.currentViewOptionsRefined = {}; this.currentCalendarOptionsRefiners = {}; this.optionsForRefining = []; this.optionsForHandling = []; this.getCurrentData = () => this.data; this.dispatch = (action) => { this.actionRunner.request(action); // protects against recursive calls to _handleAction }; this.props = props; this.actionRunner.pause(); let dynamicOptionOverrides = {}; let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi); let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView; let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides); // wire things up // TODO: not DRY props.calendarApi.currentDataManager = this; this.emitter.setThisContext(props.calendarApi); this.emitter.setOptions(currentViewData.options); let currentDate = internalCommon.getInitialDate(optionsData.calendarOptions, optionsData.dateEnv); let dateProfile = currentViewData.dateProfileGenerator.build(currentDate); if (!internalCommon.rangeContainsMarker(dateProfile.activeRange, currentDate)) { currentDate = dateProfile.currentRange.start; } let calendarContext = { dateEnv: optionsData.dateEnv, options: optionsData.calendarOptions, pluginHooks: optionsData.pluginHooks, calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData, }; // needs to be after setThisContext for (let callback of optionsData.pluginHooks.contextInit) { callback(calendarContext); } // NOT DRY let eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext); let initialState = { dynamicOptionOverrides, currentViewType, currentDate, dateProfile, businessHours: this.parseContextBusinessHours(calendarContext), eventSources, eventUiBases: {}, eventStore: internalCommon.createEmptyEventStore(), renderableEventStore: internalCommon.createEmptyEventStore(), dateSelection: null, eventSelection: '', eventDrag: null, eventResize: null, selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig, }; let contextAndState = Object.assign(Object.assign({}, calendarContext), initialState); for (let reducer of optionsData.pluginHooks.reducers) { Object.assign(initialState, reducer(null, null, contextAndState)); } if (computeIsLoading(initialState, calendarContext)) { this.emitter.trigger('loading', true); // NOT DRY } this.state = initialState; this.updateData(); this.actionRunner.resume(); } resetOptions(optionOverrides, changedOptionNames) { let { props } = this; if (changedOptionNames === undefined) { props.optionOverrides = optionOverrides; } else { props.optionOverrides = Object.assign(Object.assign({}, (props.optionOverrides || {})), optionOverrides); this.optionsForRefining.push(...changedOptionNames); } if (changedOptionNames === undefined || changedOptionNames.length) { this.actionRunner.request({ type: 'NOTHING', }); } } _handleAction(action) { let { props, state, emitter } = this; let dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action); let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi); let currentViewType = reduceViewType(state.currentViewType, action); let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides); // wire things up // TODO: not DRY props.calendarApi.currentDataManager = this; emitter.setThisContext(props.calendarApi); emitter.setOptions(currentViewData.options); let calendarContext = { dateEnv: optionsData.dateEnv, options: optionsData.calendarOptions, pluginHooks: optionsData.pluginHooks, calendarApi: props.calendarApi, dispatch: this.dispatch, emitter, getCurrentData: this.getCurrentData, }; let { currentDate, dateProfile } = state; if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack dateProfile = currentViewData.dateProfileGenerator.build(currentDate); } currentDate = internalCommon.reduceCurrentDate(currentDate, action); dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator); if (action.type === 'PREV' || // TODO: move this logic into DateProfileGenerator action.type === 'NEXT' || // " !internalCommon.rangeContainsMarker(dateProfile.currentRange, currentDate)) { currentDate = dateProfile.currentRange.start; } let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext); let eventStore = internalCommon.reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext); let isEventsLoading = computeEventSourcesLoading(eventSources); // BAD. also called in this func in computeIsLoading let renderableEventStore = (isEventsLoading && !currentViewData.options.progressiveEventRendering) ? (state.renderableEventStore || eventStore) : // try from previous state eventStore; let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext); // will memoize obj let eventUiBySource = this.buildEventUiBySource(eventSources); let eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource); let newState = { dynamicOptionOverrides, currentViewType, currentDate, dateProfile, eventSources, eventStore, renderableEventStore, selectionConfig, eventUiBases, businessHours: this.parseContextBusinessHours(calendarContext), dateSelection: reduceDateSelection(state.dateSelection, action), eventSelection: reduceSelectedEvent(state.eventSelection, action), eventDrag: reduceEventDrag(state.eventDrag, action), eventResize: reduceEventResize(state.eventResize, action), }; let contextAndState = Object.assign(Object.assign({}, calendarContext), newState); for (let reducer of optionsData.pluginHooks.reducers) { Object.assign(newState, reducer(state, action, contextAndState)); // give the OLD state, for old value } let wasLoading = computeIsLoading(state, calendarContext); let isLoading = computeIsLoading(newState, calendarContext); // TODO: use propSetHandlers in plugin system if (!wasLoading && isLoading) { emitter.trigger('loading', true); } else if (wasLoading && !isLoading) { emitter.trigger('loading', false); } this.state = newState; if (props.onAction) { props.onAction(action); } } updateData() { let { props, state } = this; let oldData = this.data; let optionsData = this.computeOptionsData(props.optionOverrides, state.dynamicOptionOverrides, props.calendarApi); let currentViewData = this.computeCurrentViewData(state.currentViewType, optionsData, props.optionOverrides, state.dynamicOptionOverrides); let data = this.data = Object.assign(Object.assign(Object.assign({ viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData }, optionsData), currentViewData), state); let changeHandlers = optionsData.pluginHooks.optionChangeHandlers; let oldCalendarOptions = oldData && oldData.calendarOptions; let newCalendarOptions = optionsData.calendarOptions; if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) { if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) { // hack state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data); state.eventStore = data.eventStore = internalCommon.rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv); state.renderableEventStore = data.renderableEventStore = internalCommon.rezoneEventStoreDates(data.renderableEventStore, oldData.dateEnv, data.dateEnv); } for (let optionName in changeHandlers) { if (this.optionsForHandling.indexOf(optionName) !== -1 || oldCalendarOptions[optionName] !== newCalendarOptions[optionName]) { changeHandlers[optionName](newCalendarOptions[optionName], data); } } } this.optionsForHandling = []; if (props.onData) { props.onData(data); } } 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, extra, } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides); warnUnknownOptions(extra); let dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekText, pluginHooks, availableLocaleData, refinedOptions.defaultRangeSeparator); let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides, localeDefaults); let theme = this.buildTheme(refinedOptions, pluginHooks); let toolbarConfig = this.parseToolbars(refinedOptions, this.stableOptionOverrides, theme, viewSpecs, calendarApi); return this.stableCalendarOptionsData = { calendarOptions: refinedOptions, pluginHooks, dateEnv, viewSpecs, theme, toolbarConfig, localeDefaults, availableRawLocales: availableLocaleData.map, }; } // always called from behind a memoizer processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) { let { locales, locale } = internalCommon.mergeRawOptions([ internalCommon.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 = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, internalCommon.BASE_OPTION_REFINERS), internalCommon.CALENDAR_LISTENER_REFINERS), internalCommon.CALENDAR_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners); let extra = {}; let raw = internalCommon.mergeRawOptions([ internalCommon.BASE_OPTION_DEFAULTS, localeDefaults, optionOverrides, dynamicOptionOverrides, ]); 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] || (internalCommon.COMPLEX_OPTION_COMPARATORS[optionName] && (optionName in currentRaw) && internalCommon.COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName])))) { refined[optionName] = currentRefined[optionName]; } else if (refiners[optionName]) { refined[optionName] = refiners[optionName](raw[optionName]); anyChanges = true; } else { extra[optionName] = currentRaw[optionName]; } } 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, extra, }; } _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, extra } = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides); warnUnknownOptions(extra); let dateProfileGenerator = this.buildDateProfileGenerator({ dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass, duration: viewSpec.duration, durationUnit: viewSpec.durationUnit, usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime, dateEnv: optionsData.dateEnv, calendarApi: this.props.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, nowInput: refinedOptions.now, 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 raw = internalCommon.mergeRawOptions([ internalCommon.BASE_OPTION_DEFAULTS, viewSpec.optionDefaults, localeDefaults, optionOverrides, viewSpec.optionOverrides, dynamicOptionOverrides, ]); let refiners = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, internalCommon.BASE_OPTION_REFINERS), internalCommon.CALENDAR_LISTENER_REFINERS), internalCommon.CALENDAR_OPTION_REFINERS), internalCommon.VIEW_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners); let refined = {}; let currentRaw = this.currentViewOptionsInput; let currentRefined = this.currentViewOptionsRefined; let anyChanges = false; let extra = {}; for (let optionName in raw) { if (raw[optionName] === currentRaw[optionName] || (internalCommon.COMPLEX_OPTION_COMPARATORS[optionName] && internalCommon.COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName]))) { refined[optionName] = currentRefined[optionName]; } else { if (raw[optionName] === this.currentCalendarOptionsInput[optionName] || (internalCommon.COMPLEX_OPTION_COMPARATORS[optionName] && internalCommon.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]); } else { extra[optionName] = raw[optionName]; } anyChanges = true; } } if (anyChanges) { this.currentViewOptionsInput = raw; this.currentViewOptionsRefined = refined; } return { rawOptions: this.currentViewOptionsInput, refinedOptions: this.currentViewOptionsRefined, extra, }; } } function buildDateEnv$1(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekText, pluginHooks, availableLocaleData, defaultSeparator) { let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map); return new internalCommon.DateEnv({ calendarSystem: 'gregory', timeZone, namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl, locale, weekNumberCalculation, firstDay, weekText, cmdFormatter: pluginHooks.cmdFormatter, defaultSeparator, }); } function buildTheme(options, pluginHooks) { let ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme; return new ThemeClass(options); } function buildDateProfileGenerator(props) { let DateProfileGeneratorClass = props.dateProfileGeneratorClass || internalCommon.DateProfileGenerator; return new DateProfileGeneratorClass(props); } function buildViewApi(type, getCurrentData, dateEnv) { return new ViewImpl(type, getCurrentData, dateEnv); } function buildEventUiBySource(eventSources) { return internalCommon.mapHash(eventSources, (eventSource) => eventSource.ui); } function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) { let eventUiBases = { '': eventUiSingleBase }; 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) { let { options } = calendarContext; return { eventUiSingleBase: internalCommon.createEventUi({ display: options.eventDisplay, editable: options.editable, startEditable: options.eventStartEditable, durationEditable: options.eventDurationEditable, constraint: options.eventConstraint, overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined, allow: options.eventAllow, backgroundColor: options.eventBackgroundColor, borderColor: options.eventBorderColor, textColor: options.eventTextColor, color: options.eventColor, // classNames: options.eventClassNames // render hook will handle this }, calendarContext), selectionConfig: internalCommon.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 internalCommon.parseBusinessHours(calendarContext.options.businessHours, calendarContext); } function warnUnknownOptions(options, viewName) { for (let optionName in options) { console.warn(`Unknown option '${optionName}'` + (viewName ? ` for view '${viewName}'` : '')); } } class ToolbarSection extends internalCommon.BaseComponent { render() { let children = this.props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup)); return preact.createElement('div', { className: 'fc-toolbar-chunk' }, ...children); } renderWidgetGroup(widgetGroup) { let { props } = this; let { theme } = this.context; let children = []; let isOnlyButtons = true; for (let widget of widgetGroup) { let { buttonName, buttonClick, buttonText, buttonIcon, buttonHint } = widget; if (buttonName === 'title') { isOnlyButtons = false; children.push(preact.createElement("h2", { className: "fc-toolbar-title", id: props.titleId }, props.title)); } else { let isPressed = buttonName === props.activeButton; let isDisabled = (!props.isTodayEnabled && buttonName === 'today') || (!props.isPrevEnabled && buttonName === 'prev') || (!props.isNextEnabled && buttonName === 'next'); let buttonClasses = [`fc-${buttonName}-button`, theme.getClass('button')]; if (isPressed) { buttonClasses.push(theme.getClass('buttonActive')); } children.push(preact.createElement("button", { type: "button", title: typeof buttonHint === 'function' ? buttonHint(props.navUnit) : buttonHint, disabled: isDisabled, "aria-pressed": isPressed, className: buttonClasses.join(' '), onClick: buttonClick }, buttonText || (buttonIcon ? preact.createElement("span", { className: buttonIcon, role: "img" }) : ''))); } } if (children.length > 1) { let groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || ''; return preact.createElement('div', { className: groupClassName }, ...children); } return children[0]; } } class Toolbar extends internalCommon.BaseComponent { render() { let { model, extraClassName } = this.props; let forceLtr = false; let startContent; let endContent; let sectionWidgets = model.sectionWidgets; let centerContent = sectionWidgets.center; if (sectionWidgets.left) { forceLtr = true; startContent = sectionWidgets.left; } else { startContent = sectionWidgets.start; } if (sectionWidgets.right) { forceLtr = true; endContent = sectionWidgets.right; } else { endContent = sectionWidgets.end; } let classNames = [ extraClassName || '', 'fc-toolbar', forceLtr ? 'fc-toolbar-ltr' : '', ]; return (preact.createElement("div", { className: classNames.join(' ') }, this.renderSection('start', startContent || []), this.renderSection('center', centerContent || []), this.renderSection('end', endContent || []))); } renderSection(key, widgetGroups) { let { props } = this; return (preact.createElement(ToolbarSection, { key: key, widgetGroups: widgetGroups, title: props.title, navUnit: props.navUnit, activeButton: props.activeButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled, titleId: props.titleId })); } } class ViewHarness extends internalCommon.BaseComponent { constructor() { super(...arguments); this.state = { availableWidth: null, }; this.handleEl = (el) => { this.el = el; internalCommon.setRef(this.props.elRef, el); this.updateAvailableWidth(); }; this.handleResize = () => { this.updateAvailableWidth(); }; } render() { let { props, state } = this; let { aspectRatio } = props; let classNames = [ 'fc-view-harness', (aspectRatio || props.liquid || props.height) ? 'fc-view-harness-active' // harness controls the height : 'fc-view-harness-passive', // let the view do the height ]; let height = ''; let paddingBottom = ''; if (aspectRatio) { if (state.availableWidth !== null) { height = state.availableWidth / aspectRatio; } else { // while waiting to know availableWidth, we can't set height to *zero* // because will cause lots of unnecessary scrollbars within scrollgrid. // BETTER: don't start rendering ANYTHING yet until we know container width // NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606) paddingBottom = `${(1 / aspectRatio) * 100}%`; } } else { height = props.height || ''; } return (preact.createElement("div", { "aria-labelledby": props.labeledById, ref: this.handleEl, className: classNames.join(' '), style: { height, paddingBottom } }, props.children)); } componentDidMount() { this.context.addResizeHandler(this.handleResize); } componentWillUnmount() { this.context.removeResizeHandler(this.handleResize); } updateAvailableWidth() { if (this.el && // needed. but why? this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth ) { this.setState({ availableWidth: this.el.offsetWidth }); } } } /* Detects when the user clicks on an event within a DateComponent */ class EventClicking extends internalCommon.Interaction { constructor(settings) { super(settings); this.handleSegClick = (ev, segEl) => { let { component } = this; let { context } = component; let seg = internalCommon.getElSeg(segEl); if (seg && // might be the
surrounding the more link component.isValidSegDownEl(ev.target)) { // our way to simulate a link click for elements that can't be tags // grab before trigger fired in case trigger trashes DOM thru rerendering let hasUrlContainer = internalCommon.elementClosest(ev.target, '.fc-event-forced-url'); let url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : ''; context.emitter.trigger('eventClick', { el: segEl, event: new internalCommon.EventImpl(component.context, seg.eventRange.def, seg.eventRange.instance), jsEvent: ev, view: context.viewApi, }); if (url && !ev.defaultPrevented) { window.location.href = url; } } }; this.destroy = internalCommon.listenBySelector(settings.el, 'click', '.fc-event', // 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 internalCommon.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 (internalCommon.getElSeg(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 = internalCommon.listenToHoverBySelector(settings.el, '.fc-event', // on both fg and bg events this.handleSegEnter, this.handleSegLeave); } destroy() { this.removeHoverListeners(); } triggerEvent(publicEvName, ev, segEl) { let { component } = this; let { context } = component; let seg = internalCommon.getElSeg(segEl); if (!ev || component.isValidSegDownEl(ev.target)) { context.emitter.trigger(publicEvName, { el: segEl, event: new internalCommon.EventImpl(context, seg.eventRange.def, seg.eventRange.instance), jsEvent: ev, view: context.viewApi, }); } } } class CalendarContent extends internalCommon.PureComponent { constructor() { super(...arguments); this.buildViewContext = internalCommon.memoize(internalCommon.buildViewContext); this.buildViewPropTransformers = internalCommon.memoize(buildViewPropTransformers); this.buildToolbarProps = internalCommon.memoize(buildToolbarProps); this.headerRef = preact.createRef(); this.footerRef = preact.createRef(); this.interactionsStore = {}; // eslint-disable-next-line this.state = { viewLabelId: internalCommon.getUniqueDomId(), }; // Component Registration // ----------------------------------------------------------------------------------------------------------------- this.registerInteractiveComponent = (component, settingsInput) => { let settings = internalCommon.parseInteractionSettings(component, settingsInput); let DEFAULT_INTERACTIONS = [ EventClicking, EventHovering, ]; let interactionClasses = DEFAULT_INTERACTIONS.concat(this.props.pluginHooks.componentInteractions); let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings)); this.interactionsStore[component.uid] = interactions; internalCommon.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 internalCommon.interactionSettingsStore[component.uid]; }; // Resizing // ----------------------------------------------------------------------------------------------------------------- this.resizeRunner = new internalCommon.DelayedRunner(() => { this.props.emitter.trigger('_resize', true); // should window resizes be considered "forced" ? this.props.emitter.trigger('windowResize', { view: this.props.viewApi }); }); this.handleWindowResize = (ev) => { let { options } = this.props; if (options.handleWindowResize && ev.target === window // avoid jqui events ) { this.resizeRunner.request(options.windowResizeDelay); } }; } /* renders INSIDE of an outer div */ render() { let { props } = this; let { toolbarConfig, options } = props; let toolbarProps = this.buildToolbarProps(props.viewSpec, props.dateProfile, props.dateProfileGenerator, props.currentDate, internalCommon.getNow(props.options.now, props.dateEnv), // TODO: use NowTimer???? props.viewTitle); let viewVGrow = false; let viewHeight = ''; let viewAspectRatio; if (props.isHeightAuto || props.forPrint) { viewHeight = ''; } else if (options.height != null) { viewVGrow = 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.theme, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, this.registerInteractiveComponent, this.unregisterInteractiveComponent); let viewLabelId = (toolbarConfig.header && toolbarConfig.header.hasTitle) ? this.state.viewLabelId : undefined; return (preact.createElement(internalCommon.ViewContextType.Provider, { value: viewContext }, toolbarConfig.header && (preact.createElement(Toolbar, Object.assign({ ref: this.headerRef, extraClassName: "fc-header-toolbar", model: toolbarConfig.header, titleId: viewLabelId }, toolbarProps))), preact.createElement(ViewHarness, { liquid: viewVGrow, height: viewHeight, aspectRatio: viewAspectRatio, labeledById: viewLabelId }, this.renderView(props), this.buildAppendContent()), toolbarConfig.footer && (preact.createElement(Toolbar, Object.assign({ ref: this.footerRef, extraClassName: "fc-footer-toolbar", model: toolbarConfig.footer, titleId: "" }, toolbarProps))))); } componentDidMount() { let { props } = this; this.calendarInteractions = props.pluginHooks.calendarInteractions .map((CalendarInteractionClass) => new CalendarInteractionClass(props)); window.addEventListener('resize', this.handleWindowResize); let { propSetHandlers } = props.pluginHooks; for (let propName in propSetHandlers) { propSetHandlers[propName](props[propName], props); } } componentDidUpdate(prevProps) { let { props } = this; let { propSetHandlers } = props.pluginHooks; for (let propName in propSetHandlers) { if (props[propName] !== prevProps[propName]) { propSetHandlers[propName](props[propName], props); } } } componentWillUnmount() { window.removeEventListener('resize', this.handleWindowResize); this.resizeRunner.clear(); for (let interaction of this.calendarInteractions) { interaction.destroy(); } this.props.emitter.trigger('_unmount'); } buildAppendContent() { let { props } = this; let children = props.pluginHooks.viewContainerAppends.map((buildAppendContent) => buildAppendContent(props)); return preact.createElement(preact.Fragment, {}, ...children); } renderView(props) { let { pluginHooks } = props; let { viewSpec } = props; let viewProps = { dateProfile: props.dateProfile, businessHours: props.businessHours, eventStore: props.renderableEventStore, eventUiBases: props.eventUiBases, dateSelection: props.dateSelection, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, isHeightAuto: props.isHeightAuto, forPrint: props.forPrint, }; let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers); for (let transformer of transformers) { Object.assign(viewProps, transformer.transform(viewProps, props)); } let ViewComponent = viewSpec.component; return (preact.createElement(ViewComponent, Object.assign({}, viewProps))); } } function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, now, 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(now, undefined, false); // TODO: need `undefined` or else INFINITE LOOP for some reason let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false); let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false); return { title, activeButton: viewSpec.type, navUnit: viewSpec.singleUnit, isTodayEnabled: todayInfo.isValid && !internalCommon.rangeContainsMarker(dateProfile.currentRange, now), isPrevEnabled: prevInfo.isValid, isNextEnabled: nextInfo.isValid, }; } // Plugin // ----------------------------------------------------------------------------------------------------------------- function buildViewPropTransformers(theClasses) { return theClasses.map((TheClass) => new TheClass()); } class Calendar extends internalCommon.CalendarImpl { constructor(el, optionOverrides = {}) { super(); this.isRendering = false; this.isRendered = false; this.currentClassNames = []; this.customContentRenderId = 0; this.handleAction = (action) => { // actions we know we want to render immediately switch (action.type) { case 'SET_EVENT_DRAG': case 'SET_EVENT_RESIZE': this.renderRunner.tryDrain(); } }; this.handleData = (data) => { this.currentData = data; this.renderRunner.request(data.calendarOptions.rerenderDelay); }; this.handleRenderRequest = () => { if (this.isRendering) { this.isRendered = true; let { currentData } = this; internalCommon.flushSync(() => { preact.render(preact.createElement(internalCommon.CalendarRoot, { options: currentData.calendarOptions, theme: currentData.theme, emitter: currentData.emitter }, (classNames, height, isHeightAuto, forPrint) => { this.setClassNames(classNames); this.setHeight(height); return (preact.createElement(internalCommon.RenderId.Provider, { value: this.customContentRenderId }, preact.createElement(CalendarContent, Object.assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData)))); }), this.el); }); } else if (this.isRendered) { this.isRendered = false; preact.render(null, this.el); this.setClassNames([]); this.setHeight(''); } }; internalCommon.ensureElHasStyles(el); this.el = el; this.renderRunner = new internalCommon.DelayedRunner(this.handleRenderRequest); new CalendarDataManager({ optionOverrides, calendarApi: this, onAction: this.handleAction, onData: this.handleData, }); } render() { let wasRendering = this.isRendering; if (!wasRendering) { this.isRendering = true; } else { this.customContentRenderId += 1; } this.renderRunner.request(); if (wasRendering) { this.updateSize(); } } destroy() { if (this.isRendering) { this.isRendering = false; this.renderRunner.request(); } } updateSize() { internalCommon.flushSync(() => { super.updateSize(); }); } batchRendering(func) { this.renderRunner.pause('batchRendering'); func(); this.renderRunner.resume('batchRendering'); } pauseRendering() { this.renderRunner.pause('pauseRendering'); } resumeRendering() { this.renderRunner.resume('pauseRendering', true); } resetOptions(optionOverrides, changedOptionNames) { this.currentDataManager.resetOptions(optionOverrides, changedOptionNames); } setClassNames(classNames) { if (!internalCommon.isArraysEqual(classNames, this.currentClassNames)) { let { classList } = this.el; for (let className of this.currentClassNames) { classList.remove(className); } for (let className of classNames) { classList.add(className); } this.currentClassNames = classNames; } } setHeight(height) { internalCommon.applyStyleProp(this.el, 'height', height); } } function formatDate(dateInput, options = {}) { let dateEnv = buildDateEnv(options); let formatter = internalCommon.createFormatter(options); let dateMeta = dateEnv.createMarkerMeta(dateInput); if (!dateMeta) { // TODO: warning? return ''; } return dateEnv.format(dateMeta.marker, formatter, { forcedTzo: dateMeta.forcedTzo, }); } function formatRange(startInput, endInput, options) { let dateEnv = buildDateEnv(typeof options === 'object' && options ? options : {}); // pass in if non-null object let formatter = internalCommon.createFormatter(options); let startMeta = dateEnv.createMarkerMeta(startInput); let endMeta = dateEnv.createMarkerMeta(endInput); if (!startMeta || !endMeta) { // TODO: warning? return ''; } return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, { forcedStartTzo: startMeta.forcedTzo, forcedEndTzo: endMeta.forcedTzo, isEndExclusive: options.isEndExclusive, defaultSeparator: internalCommon.BASE_OPTION_DEFAULTS.defaultRangeSeparator, }); } // TODO: more DRY and optimized function buildDateEnv(settings) { let locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map); // TODO: don't hardcode 'en' everywhere return new internalCommon.DateEnv(Object.assign(Object.assign({ timeZone: internalCommon.BASE_OPTION_DEFAULTS.timeZone, calendarSystem: 'gregory' }, settings), { locale })); } // HELPERS /* if nextDayThreshold is specified, slicing is done in an all-day fashion. you can get nextDayThreshold from context.nextDayThreshold */ function sliceEvents(props, allDay) { return internalCommon.sliceEventStore(props.eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? props.nextDayThreshold : null).fg; } const version = '6.1.11'; exports.JsonRequestError = internalCommon.JsonRequestError; exports.Calendar = Calendar; exports.createPlugin = createPlugin; exports.formatDate = formatDate; exports.formatRange = formatRange; exports.globalLocales = globalLocales; exports.globalPlugins = globalPlugins; exports.sliceEvents = sliceEvents; exports.version = version;