@fullcalendar/web-component
Version:
Custom Element for FullCalendar
1,311 lines (1,279 loc) • 118 kB
JavaScript
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