UNPKG

@fullcalendar/web-component

Version:
1,311 lines (1,279 loc) 118 kB
import { jsxs, jsx, Fragment } from 'preact/jsx-runtime'; import { Component, flushSync, Fragment as Fragment$1, createElement } from 'preact/compat'; import { createRoot } from 'preact/compat/client'; import { m as mergeCalendarOptions, a as memoize, b as memoizeObjArg, N as NowTimerRunner, i as isMergedPropsEqual, g as generateClassName, P as PureComponent, c as buildViewContext, d as getIsHeightAuto, V as ViewContextType, B as BaseComponent, C as ContentContainer, e as mergeViewOptionsMap, R as RenderId, D as DelayedRunner } from './fa7e597f.js'; import { b as buildRangeApiWithTimeZone, a as buildEventApis, h as hashValuesToArray, i as identity, d as arrayToHash, r as refineProps, g as guid, e as createEventUi, E as EVENT_UI_REFINERS, f as filterHash, w as warn, j as isPropsEqualShallow, k as Emitter, l as createEmptyEventStore, B as BASE_OPTION_DEFAULTS, m as BASE_OPTION_REFINERS, C as CALENDAR_LISTENER_REFINERS, n as CALENDAR_ONLY_OPTION_REFINERS, o as COMPLEX_OPTION_COMPARATORS, V as VIEW_ONLY_OPTION_REFINERS, p as mapHash, c as createFormatter, q as parseDateSpan, t as triggerDateSelect, u as triggerDateUnselect, v as EventImpl, x as eventTupleToStore, y as EventSourceImpl, z as parseEvent, A as eventApiToStore, D as formatWithOrdinals, F as classNames, G as isArraysEqual, H as parseEvents, I as computeViewBorderless, J as getElEventRange, K as listenBySelector, L as listenToHoverBySelector, M as applyStyleProp } from './69b11357.js'; import { createDuration, subtractDurations, intersectRanges, startOfDay, addDays, rangeContainsMarker, DateEnv, joinDateTimeFormatParts, greatestDurationDenominator, diffWholeDays } from '@full-ui/headless-calendar'; import { j as joinClassNames } from './423b7bc6.js'; import { D as DateProfileGenerator } from './3d9e7be7.js'; import { r as reduceEventStore, a as rezoneEventStoreDates, p as parseInteractionSettings, i as interactionSettingsStore, I as Interaction } from './2b5ac8cd.js'; import { Fragment as Fragment$2 } from 'preact'; const globalLocales = []; const MINIMAL_RAW_EN_LOCALE = { code: 'en', week: { dow: 0, // Sunday is the first day of the week doy: 4, // 4 days need to be within the year to be considered the first week }, direction: 'ltr', // TODO: make a real type for this todayText: 'Today', prevText: 'Prev', nextText: 'Next', prevYearText: 'Prev year', nextYearText: 'Next year', yearText: 'Year', monthText: 'Month', weekTextLong: 'Week', dayText: 'Day', listText: 'List', closeHint: 'Close', eventsHint: 'Events', allDayText: 'All-day', timedText: 'Timed', moreLinkText: 'more', noEventsText: 'No events to display', }; /* Includes things we don't want other locales to inherit, things that derive from other translatable strings. */ const RAW_EN_LOCALE = { ...MINIMAL_RAW_EN_LOCALE, // if a locale doesn't define this, fall back to weekTextLong, don't use EN weekTextShort: 'W', todayHint: (unitText, unit) => { return (unit === 'day') ? 'Today' : `This ${unitText}`; }, prevHint: 'Previous $0', nextHint: 'Next $0', viewHint: '$0 view', viewChangeHint: 'Change 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 = mergeCalendarOptions(MINIMAL_RAW_EN_LOCALE, raw); 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, }; } class JsonRequestError extends Error { constructor(message, response) { super(message); this.response = response; } } function requestJson(method, url, params) { method = method.toUpperCase(); const fetchOptions = { method, }; if (method === 'GET') { url += (url.indexOf('?') === -1 ? '?' : '&') + new URLSearchParams(params); } else { fetchOptions.body = new URLSearchParams(params); fetchOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; } return fetch(url, fetchOptions).then((fetchRes) => { if (fetchRes.ok) { return fetchRes.json().then((parsedResponse) => { return [parsedResponse, fetchRes]; }, () => { throw new JsonRequestError('Failure parsing JSON', fetchRes); }); } else { throw new JsonRequestError('Request failed', fetchRes); } }); } function handleDateProfile(dateProfile, context) { context.emitter.trigger('datesSet', { ...buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv), view: context.viewApi, }); } function handleEventStore(eventStore, context) { let { emitter } = context; if (emitter.hasHandlers('eventsSet')) { emitter.trigger('eventsSet', buildEventApis(eventStore, context)); } } 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 = { name: 'array-event-source', eventSourceDefs: [eventSourceDef$2], }; /* given a function that resolves a result asynchronously. the function can either call passed-in success and failure callbacks, or it can return a promise. if you need to pass additional params to func, bind them first. */ function unpromisify(func, normalizedSuccessCallback, normalizedFailureCallback) { // guard against success/failure callbacks being called more than once // and guard against a promise AND callback being used together. let isResolved = false; let wrappedSuccess = function (res) { if (!isResolved) { isResolved = true; normalizedSuccessCallback(res); } }; let wrappedFailure = function (error) { if (!isResolved) { isResolved = true; normalizedFailureCallback(error); } }; let res = func(wrappedSuccess, wrappedFailure); if (res && typeof res.then === 'function') { res.then(wrappedSuccess, wrappedFailure); } } 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; unpromisify(func.bind(null, buildRangeApiWithTimeZone(arg.range, dateEnv)), (rawEvents) => successCallback({ rawEvents }), errorCallback); }, }; const funcEventSourcePlugin = { name: 'func-event-source', eventSourceDefs: [eventSourceDef$1], }; const JSON_FEED_EVENT_SOURCE_REFINERS = { method: String, extraParams: 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); requestJson(meta.method, meta.url, requestParams).then(([rawEvents, response]) => { successCallback({ rawEvents, response }); }, errorCallback); }, }; const jsonFeedEventSourcePlugin = { 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 changeHandlerPlugin = { name: 'change-handler', optionChangeHandlers: { controller(controller, context) { // TODO: the initial setting is in CalendarDataManager controller._setApi(context.calendarApi); }, 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 = 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); } } const EVENT_SOURCE_REFINERS = { id: String, defaultAllDay: Boolean, url: String, format: String, events: identity, // array or function eventDataTransform: identity, // for any network-related sources success: identity, failure: identity, }; function parseEventSource(raw, context, refiners = buildEventSourceRefiners(context)) { let rawObj; if (typeof raw === 'string') { rawObj = { url: raw }; } else if (typeof raw === 'function' || Array.isArray(raw)) { rawObj = { events: raw }; } else if (typeof raw === 'object' && raw) { // not null rawObj = raw; } if (rawObj) { let { refined, extra } = refineProps(rawObj, refiners); let metaRes = buildEventSourceMeta(refined, context); if (metaRes) { return { _raw: raw, isFetching: false, latestFetchId: '', fetchRange: null, defaultAllDay: refined.defaultAllDay, eventDataTransform: refined.eventDataTransform, success: refined.success, failure: refined.failure, publicId: refined.id || '', sourceId: guid(), sourceDefId: metaRes.sourceDefId, meta: metaRes.meta, ui: createEventUi(refined, context), extendedProps: extra, }; } } return null; } function buildEventSourceRefiners(context) { return { ...EVENT_UI_REFINERS, ...EVENT_SOURCE_REFINERS, ...context.pluginHooks.eventSourceRefiners }; } function buildEventSourceMeta(raw, context) { let defs = context.pluginHooks.eventSourceDefs; for (let i = defs.length - 1; i >= 0; i -= 1) { // later-added plugins take precedence let def = defs[i]; let meta = def.parseMeta(raw); if (meta) { return { sourceDefId: i, meta }; } } return null; } 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? 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 { ...eventSourceHash, ...hash }; } function removeSource(eventSourceHash, sourceId) { return filterHash(eventSourceHash, (eventSource) => eventSource.sourceId !== sourceId); } function fetchDirtySources(sourceHash, fetchRange, context) { return fetchSourcesByIds(sourceHash, 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 = 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) { warn(`Unhandled event source error: ${error.message}`, error); } context.dispatch({ type: 'RECEIVE_EVENT_ERROR', sourceId: eventSource.sourceId, fetchId, fetchRange, error, }); }); return { ...eventSource, isFetching: true, latestFetchId: fetchId, }; } function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) { let eventSource = sourceHash[sourceId]; if (eventSource && // not already removed fetchId === eventSource.latestFetchId) { return { ...sourceHash, [sourceId]: { ...eventSource, isFetching: false, fetchRange, // also serves as a marker that at least one fetch has completed }, }; } return sourceHash; } function excludeStaticSources(eventSources, context) { return filterHash(eventSources, (eventSource) => doesSourceNeedRange(eventSource, context)); } function parseInitialSources(rawOptions, context) { let refiners = 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 = 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; } const SIMPLE_RECURRING_REFINERS = { daysOfWeek: identity, startTime: createDuration, endTime: createDuration, duration: createDuration, startRecur: identity, endRecur: 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, dateEnv, }; let duration; if (refined.duration) { duration = refined.duration; } if (!duration && refined.startTime && refined.endTime) { duration = 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 = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur }); if (clippedFramingRange) { return expandRanges(typeData.daysOfWeek, typeData.startTime, typeData.dateEnv, dateEnv, clippedFramingRange); } return []; }, }; const simpleRecurringEventsPlugin = { name: 'simple-recurring-event', recurringTypes: [recurring], eventRefiners: SIMPLE_RECURRING_REFINERS, }; function expandRanges(daysOfWeek, startTime, eventDateEnv, calendarDateEnv, framingRange) { let dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null; let dayMarker = startOfDay(framingRange.start); let endMarker = framingRange.end; let instanceStarts = []; // https://github.com/fullcalendar/fullcalendar/issues/7934 if (startTime) { if (startTime.milliseconds < 0) { // possible for next-day to have negative business hours that go into current day endMarker = addDays(endMarker, 1); } else if (startTime.milliseconds >= 1000 * 60 * 60 * 24) { // possible for prev-day to have >24hr business hours that go into current day dayMarker = addDays(dayMarker, -1); } } while (dayMarker < endMarker) { let instanceStart; // if everyday, or this particular day-of-week if (!dowHash || dowHash[dayMarker.getUTCDay()]) { if (startTime) { instanceStart = calendarDateEnv.add(dayMarker, startTime); } else { instanceStart = dayMarker; } instanceStarts.push(calendarDateEnv.createMarker(eventDateEnv.toDate(instanceStart))); } dayMarker = addDays(dayMarker, 1); } return instanceStarts; } /* 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, { name: 'misc', isLoadingFuncs: [ (state) => computeEventSourcesLoading(state.eventSources), ], propSetHandlers: { dateProfile: handleDateProfile, eventStore: handleEventStore, }, }, ]; // 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 fo