fullcalendar
Version:
FullCalendar Vanilla JS package for rendering a calendar
685 lines (667 loc) • 23.5 kB
JavaScript
import { m as mergeCalendarOptions } from './f0fa24ec.js';
import { b as buildRangeApiWithTimeZone, a as buildEventApis, h as hashValuesToArray, r as refineProps, g as guid, c as createEventUi, E as EVENT_UI_REFINERS, d as arrayToHash, f as filterHash, w as warn, i as identity } from './2add5508.js';
import { createDuration, subtractDurations, intersectRanges, startOfDay, addDays } from '@full-ui/headless-calendar';
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,
},
},
];
export { JsonRequestError as J, globalPlugins as a, buildLocale as b, computeEventSourcesLoading as c, reduceEventSourcesNewTimeZone as d, requestJson as e, globalLocales as g, initEventSources as i, organizeRawLocales as o, parseEventSource as p, reduceEventSources as r, unpromisify as u };