@fullcalendar/web-component
Version:
Custom Element for FullCalendar
1,407 lines (1,391 loc) • 82.7 kB
JavaScript
import { NativeDateFormatter, CmdDateFormatter, FuncDateFormatter, createDuration, diffDayAndTime, startOfDay, durationsEqual, joinDateTimeFormatParts, intersectRanges, invertRanges, rangeContainsMarker, diffDays, addDays, asRoughMs, rangesEqual, addMs } from '@full-ui/headless-calendar';
import { j as joinClassNames } from './423b7bc6.js';
var classNames = {"popoverZ":"fc-dp","isolate":"fc-6T","borderBoxRoot":"fc-BO","notAllowed":"fc-la","noScrollbars":"fc-mM","noShrink":"fc-yf","calendarScreenRoot":"fc-wS","safeTiles":"fc-mP","calendarPrintRoot":"fc-OB","cursorPointer":"fc-hH","cursorResizeT":"fc-wi","cursorResizeB":"fc-My","cursorResizeS":"fc-N8","cursorResizeE":"fc-Yv","cursorColResizer":"fc-DR","hit":"fc-Lp","hitX":"fc-YZ","hitY":"fc-RU","hitXSkinny":"fc-4P","selectNone":"fc-6M","invisible":"fc-Sy","borderNone":"fc-QX","borderOnlyT":"fc-3t","borderOnlyB":"fc-fo","borderOnlyS":"fc-wk","borderOnlyE":"fc-fx","borderlessX":"fc-8R","borderlessY":"fc-5a","fakeBorderS":"fc-hM","flexRow":"fc-ei","flexCol":"fc-Xt","grow":"fc-QU","liquid":"fc-J6","minHeight0":"fc-w9","liquidX":"fc-q3","printRoot":"fc-32","printHeader":"fc-IH","noPadding":"fc-Kk","noMargin":"fc-5M","noMarginY":"fc-G8","noMarginX":"fc-tR","whiteSpaceNoWrap":"fc-8y","whiteSpacePre":"fc-gR","overflowAnchorNone":"fc-eu","crop":"fc-75","cropNowrap":"fc-CJ","rel":"fc-xp","abs":"fc-So","start0":"fc-Q8","fill":"fc-TJ","fillTop":"fc-X3","fillX":"fc-O5","fillY":"fc-WS","fillStart":"fc-Ld","sticky":"fc-l5","stickyT":"fc-dl","stickyS":"fc-oh","tableHeaderSticky":"fc-3f","contentBox":"fc-wx","offscreen":"fc-pJ","alignCenter":"fc-xM","alignStart":"fc-RJ","alignEnd":"fc-x9","footerScrollbarSticky":"fc-10","footerScrollbar":"fc-rg","breakInsideAvoid":"fc-sv","printSiblingRow":"fc-Ph","z0":"fc-6d","z1":"fc-y0","focusZ2":"fc-5y","internalTimelineSlot":"fc-RK","internalEvent":"fc-vB","internalEventMirror":"fc-nH","internalEventDraggable":"fc-Be","internalEventSelected":"fc-w3","internalEventResizable":"fc-Kf","internalEventResizer":"fc-ve","internalEventResizerStart":"fc-Er","internalEventResizerEnd":"fc-ll","internalBgEvent":"fc-BR","internalMoreLink":"fc-GZ","internalNavLink":"fc-Z9","internalPopover":"fc-VO","internalView":"fc-rF","internalScroller":"fc-zT"};
function createFormatter(input) {
if (typeof input === 'object' && input) { // non-null object
return new NativeDateFormatter(input);
}
if (typeof input === 'string') {
return new CmdDateFormatter(input);
}
if (typeof input === 'function') {
return new FuncDateFormatter(input);
}
return null;
}
function warn(...args) {
console.warn('FullCalendar:', ...args);
}
/* eslint max-classes-per-file: off */
const warnedClassNameOptions = {};
function refineClassName(input, optionName) {
if (!input || typeof input === 'string') {
return input;
}
warnInvalidClassName(optionName);
return '';
}
function refineClassNameGenerator(input, optionName) {
if (typeof input === 'function') {
return (renderProps) => refineClassName(input(renderProps), optionName);
}
return refineClassName(input, optionName);
}
function warnInvalidClassName(optionName) {
if (!warnedClassNameOptions[optionName]) {
warn(`Invalid option \`${optionName}\`: expected a className string or a falsy value.`);
warnedClassNameOptions[optionName] = true;
}
}
// Stops a mouse/touch event from doing it's native browser action
function preventDefault(ev) {
ev.preventDefault();
}
// Event Delegation
// ----------------------------------------------------------------------------------------------------------------
function buildDelegationHandler(selector, handler) {
return (ev) => {
let matchedChild = ev.target.closest(selector);
if (matchedChild) {
handler.call(matchedChild, ev, matchedChild);
}
};
}
function listenBySelector(container, eventType, selector, handler) {
let attachedHandler = buildDelegationHandler(selector, handler);
container.addEventListener(eventType, attachedHandler);
return () => {
container.removeEventListener(eventType, attachedHandler);
};
}
function listenToHoverBySelector(container, selector, onMouseEnter, onMouseLeave) {
let currentMatchedChild;
return listenBySelector(container, 'mouseover', selector, (mouseOverEv, matchedChild) => {
if (matchedChild !== currentMatchedChild) {
currentMatchedChild = matchedChild;
onMouseEnter(mouseOverEv, matchedChild);
let realOnMouseLeave = (mouseLeaveEv) => {
currentMatchedChild = null;
onMouseLeave(mouseLeaveEv, matchedChild);
matchedChild.removeEventListener('mouseleave', realOnMouseLeave);
};
// listen to the next mouseleave, and then unattach
matchedChild.addEventListener('mouseleave', realOnMouseLeave);
}
});
}
// Animation
// ----------------------------------------------------------------------------------------------------------------
const transitionEventNames = [
'webkitTransitionEnd',
'otransitionend',
'oTransitionEnd',
'msTransitionEnd',
'transitionend',
];
// triggered only when the next single subsequent transition finishes
function whenTransitionDone(el, callback) {
let realCallback = (ev) => {
callback(ev);
transitionEventNames.forEach((eventName) => {
el.removeEventListener(eventName, realCallback);
});
};
transitionEventNames.forEach((eventName) => {
el.addEventListener(eventName, realCallback); // cross-browser way to determine when the transition finishes
});
}
// ARIA workarounds
// ----------------------------------------------------------------------------------------------------------------
function createAriaClickAttrs(handler) {
return {
onClick: handler,
...createAriaKeyboardAttrs(handler),
};
}
function createAriaKeyboardAttrs(handler) {
return {
tabIndex: 0,
onKeyDown(ev) {
if (ev.key === 'Enter' || ev.key === ' ') {
handler(ev);
ev.preventDefault(); // if space, don't scroll down page
}
},
};
}
let guidNumber = 0;
function guid() {
guidNumber += 1;
return String(guidNumber);
}
/* FullCalendar-specific DOM Utilities
----------------------------------------------------------------------------------------------------------------------*/
// Make the mouse cursor express that an event is not allowed in the current area
function disableCursor() {
document.body.classList.add(classNames.notAllowed);
}
// Returns the mouse cursor to its original look
function enableCursor() {
document.body.classList.remove(classNames.notAllowed);
}
/* Selection
----------------------------------------------------------------------------------------------------------------------*/
function preventSelection(el) {
el.style.userSelect = 'none';
el.style.webkitUserSelect = 'none';
el.addEventListener('selectstart', preventDefault);
}
function allowSelection(el) {
el.style.userSelect = '';
el.style.webkitUserSelect = '';
el.removeEventListener('selectstart', preventDefault);
}
/* Context Menu
----------------------------------------------------------------------------------------------------------------------*/
function preventContextMenu(el) {
el.addEventListener('contextmenu', preventDefault);
}
function allowContextMenu(el) {
el.removeEventListener('contextmenu', preventDefault);
}
function parseFieldSpecs(input) {
let specs = [];
let tokens = [];
let i;
let token;
if (typeof input === 'string') {
tokens = input.split(/\s*,\s*/);
}
else if (typeof input === 'function') {
tokens = [input];
}
else if (Array.isArray(input)) {
tokens = input;
}
for (i = 0; i < tokens.length; i += 1) {
token = tokens[i];
if (typeof token === 'string') {
specs.push(token.charAt(0) === '-' ?
{ field: token.substring(1), order: -1 } :
{ field: token, order: 1 });
}
else if (typeof token === 'function') {
specs.push({ func: token });
}
}
return specs;
}
function compareByFieldSpecs(obj0, obj1, fieldSpecs) {
let i;
let cmp;
for (i = 0; i < fieldSpecs.length; i += 1) {
cmp = compareByFieldSpec(obj0, obj1, fieldSpecs[i]);
if (cmp) {
return cmp;
}
}
return 0;
}
function compareByFieldSpec(obj0, obj1, fieldSpec) {
if (fieldSpec.func) {
return fieldSpec.func(obj0, obj1);
}
return flexibleCompare(obj0[fieldSpec.field], obj1[fieldSpec.field])
* (fieldSpec.order || 1);
}
function flexibleCompare(a, b) {
if (!a && !b) {
return 0;
}
if (b == null) {
return -1;
}
if (a == null) {
return 1;
}
if (typeof a === 'string' || typeof b === 'string') {
return String(a).localeCompare(String(b));
}
return a - b;
}
/* String Utilities
----------------------------------------------------------------------------------------------------------------------*/
function formatWithOrdinals(formatter, args, fallbackText) {
if (typeof formatter === 'function') {
return formatter(...args);
}
if (typeof formatter === 'string') { // non-blank string
return args.reduce((str, arg, index) => (str.replace('$' + index, arg || '')), formatter);
}
return fallbackText;
}
/* Number Utilities
----------------------------------------------------------------------------------------------------------------------*/
function compareNumbers(a, b) {
return a - b;
}
function valuesIdentical(a, b) {
return a === b;
}
function computeViewBorderless(options) {
const borderless = options.borderless;
return {
borderlessX: Boolean(options.borderlessX ?? borderless),
borderlessTop: Boolean(options.borderlessTop ?? borderless),
borderlessBottom: Boolean(options.borderlessBottom ?? borderless),
};
}
const { hasOwnProperty } = Object.prototype;
// Filter / Map
// -------------------------------------------------------------------------------------------------
function filterHash(hash, func) {
let filtered = {};
for (let key in hash) {
if (func(hash[key], key)) {
filtered[key] = hash[key];
}
}
return filtered;
}
function mapHash(hash, func) {
let newHash = {};
for (let key in hash) {
newHash[key] = func(hash[key], key);
}
return newHash;
}
// Conversion
// -------------------------------------------------------------------------------------------------
// Can't use Object.values yet because no es2015 support
// TODO: reassess browser support
// https://caniuse.com/?search=object.values
function hashValuesToArray(obj) {
let a = [];
for (let key in obj) {
a.push(obj[key]);
}
return a;
}
// TODO: rename to stringArrayToHash or something
function arrayToHash(a) {
let hash = {};
for (let item of a) {
hash[item] = true;
}
return hash;
}
// Equality
// -------------------------------------------------------------------------------------------------
function isMaybePropsEqualDepth1(props0, props1) {
if (typeof props0 === 'object' && props0 && // non-null object
typeof props1 === 'object' && props1 // non-null object
) {
return isPropsEqualWithFunc(props0, props1, isPropsEqualShallow);
}
return props0 === props1;
}
function isPropsEqualWithFunc(props0, props1, valuesEqual) {
if (props0 === props1) {
return true;
}
for (let key in props0) {
if (hasOwnProperty.call(props0, key)) {
if (!(key in props1)) {
return false;
}
}
}
for (let key in props1) {
if (hasOwnProperty.call(props1, key)) {
if (!(key in props0) || !valuesEqual(props0[key], props1[key], key)) {
return false;
}
}
}
return true;
}
function isMaybePropsEqualShallow(props0, props1) {
if (typeof props0 === 'object' &&
typeof props1 === 'object' &&
props0 && props1 // both non-null objects
) {
return isPropsEqualShallow(props0, props1);
}
return props0 === props1;
}
function isPropsEqualShallow(props0, props1) {
return isPropsEqualWithFunc(props0, props1, valuesIdentical);
}
function isPropsEqualWithMap(props0, props1, equalityFuncMap) {
return isPropsEqualWithFunc(props0, props1, (val0, val1, key) => {
const equalityFunc = equalityFuncMap[key];
const isEqual = equalityFunc
? equalityFunc(val0, val1)
: val0 === val1;
// if (debugMessage && !isEqual) {
// console.log(
// debugMessage, key, 'NOT EQUAL', 'rerunning...',
// equalityFunc
// ? equalityFunc(val0, val1)
// : val0 === val1
// )
// }
return isEqual;
});
}
/*
Returns array of keys
*/
function getUnequalProps(props0, props1) {
let keys = [];
for (let key in props0) {
if (hasOwnProperty.call(props0, key)) {
if (!(key in props1)) {
keys.push(key);
}
}
}
for (let key in props1) {
if (hasOwnProperty.call(props1, key)) {
if (props0[key] !== props1[key]) {
keys.push(key);
}
}
}
return keys;
}
// Merge
// -------------------------------------------------------------------------------------------------
function mergeMaybePropsDepth1(props0, props1) {
if (!props0) {
return props1;
}
return mergePropsWithFunc(props0, props1, mergePropsShallow);
}
function mergePropsWithFunc(props0, props1, mergeValues) {
const dest = {};
for (let key in props0) {
if (hasOwnProperty.call(props0, key)) {
if (!(key in props1)) {
dest[key] = props0[key];
}
}
}
for (let key in props1) {
if (hasOwnProperty.call(props1, key)) {
if (!(key in props0)) {
dest[key] = props1[key];
}
else {
dest[key] = mergeValues(props0[key], props1[key]);
}
}
}
return dest;
}
function mergePropsShallow(props0, props1) {
return Object.assign({}, props0, props1);
}
function isMaybeArraysEqual(array0, array1) {
if (Array.isArray(array0) && Array.isArray(array1)) {
return isArraysEqual(array0, array1);
}
return array0 === array1;
}
function isArraysEqual(array0, array1, itemsEqual = valuesIdentical) {
if (array0 === array1) {
return true;
}
let len = array0.length;
let i;
if (len !== array1.length) { // not array? or not same length?
return false;
}
for (i = 0; i < len; i += 1) {
if (!itemsEqual(array0[i], array1[i])) {
return false;
}
}
return true;
}
// base options
// ------------
const BASE_OPTION_REFINERS = {
navLinkDayClick: identity,
navLinkWeekClick: identity,
duration: createDuration,
buttons: identity,
toolbarElements: identity,
prevText: String,
nextText: String,
prevYearText: String,
nextYearText: String,
todayText: String,
yearText: String,
monthText: String,
weekTextLong: String,
weekTextShort: String,
dayText: String,
listText: identity,
todayHint: identity,
prevHint: identity,
nextHint: identity,
// TODO: make type for hint input
buttonDisplay: identity,
buttonGroupClass: refineClassNameGenerator,
buttonClass: refineClassNameGenerator,
defaultAllDayEventDuration: createDuration,
defaultTimedEventDuration: createDuration,
nextDayThreshold: createDuration,
scrollTime: createDuration,
scrollTimeReset: Boolean,
slotMinTime: createDuration,
slotMaxTime: createDuration,
popoverFormat: createFormatter,
slotDuration: createDuration,
snapDuration: createDuration,
headerToolbar: identity,
footerToolbar: identity,
forceEventDuration: Boolean,
// TODO: move to timegrid
dayLaneClass: refineClassNameGenerator,
dayLaneInnerClass: refineClassNameGenerator,
dayLaneDidMount: identity,
dayLaneWillUnmount: identity,
initialView: String,
aspectRatio: Number,
weekends: Boolean,
weekNumberCalculation: identity,
weekNumbers: Boolean,
weekNumberHeaderClass: refineClassNameGenerator,
weekNumberHeaderInnerClass: refineClassNameGenerator,
weekNumberHeaderContent: identity,
weekNumberHeaderDidMount: identity,
weekNumberHeaderWillUnmount: identity,
inlineWeekNumberClass: refineClassNameGenerator,
inlineWeekNumberContent: identity,
inlineWeekNumberDidMount: identity,
inlineWeekNumberWillUnmount: identity,
editable: Boolean,
controller: identity,
nowIndicator: Boolean,
nowIndicatorSnap: identity,
nowIndicatorHeaderClass: refineClassNameGenerator,
nowIndicatorHeaderContent: identity,
nowIndicatorHeaderDidMount: identity,
nowIndicatorHeaderWillUnmount: identity,
nowIndicatorDotClass: refineClassName,
nowIndicatorLineClass: refineClassNameGenerator,
nowIndicatorLineContent: identity,
nowIndicatorLineDidMount: identity,
nowIndicatorLineWillUnmount: identity,
showNonCurrentDates: Boolean,
lazyFetching: Boolean,
startParam: String,
endParam: String,
timeZoneParam: String,
timeZone: String,
locales: identity,
locale: identity,
dragRevertDuration: Number,
dragScroll: Boolean,
allDayMaintainDuration: Boolean,
unselectAuto: Boolean,
dropAccept: identity, // TODO: type draggable
eventOrder: parseFieldSpecs,
eventOrderStrict: Boolean,
eventSlicing: Boolean, // default: true
eventPrintLayout: String,
longPressDelay: Number,
eventDragMinDistance: Number,
expandRows: Boolean,
height: identity,
contentHeight: identity,
direction: String,
colorScheme: String,
weekNumberFormat: createFormatter,
eventResizableFromStart: Boolean,
displayEventTime: Boolean,
displayEventEnd: Boolean,
progressiveEventRendering: Boolean,
businessHours: identity,
initialDate: identity,
now: identity,
eventDataTransform: identity,
tableHeaderSticky: identity,
footerScrollbarSticky: identity,
defaultAllDay: Boolean,
eventSourceFailure: identity,
eventSourceSuccess: identity,
eventDisplay: String, // TODO: give more specific
eventStartEditable: Boolean,
eventDurationEditable: Boolean,
eventOverlap: identity,
eventConstraint: identity,
eventAllow: identity,
eventColor: String,
eventContrastColor: String,
eventDidMount: identity,
eventWillUnmount: identity,
eventContent: identity,
eventClass: refineClassNameGenerator,
eventInnerClass: refineClassNameGenerator,
eventTimeClass: refineClassNameGenerator,
eventTitleClass: refineClassNameGenerator,
eventBeforeClass: refineClassNameGenerator,
eventAfterClass: refineClassNameGenerator,
//
listItemEventClass: refineClassNameGenerator,
listItemEventInnerClass: refineClassNameGenerator,
listItemEventTimeClass: refineClassNameGenerator,
listItemEventTitleClass: refineClassNameGenerator,
listItemEventBeforeClass: refineClassNameGenerator,
listItemEventAfterClass: refineClassNameGenerator,
//
blockEventClass: refineClassNameGenerator,
blockEventInnerClass: refineClassNameGenerator,
blockEventTimeClass: refineClassNameGenerator,
blockEventTitleClass: refineClassNameGenerator,
blockEventBeforeClass: refineClassNameGenerator,
blockEventAfterClass: refineClassNameGenerator,
//
rowEventClass: refineClassNameGenerator,
rowEventInnerClass: refineClassNameGenerator,
rowEventTimeClass: refineClassNameGenerator,
rowEventTitleClass: refineClassNameGenerator,
rowEventTitleSticky: Boolean,
rowEventBeforeClass: refineClassNameGenerator,
rowEventBeforeContent: identity,
rowEventAfterClass: refineClassNameGenerator,
rowEventAfterContent: identity,
//
columnEventClass: refineClassNameGenerator,
columnEventInnerClass: refineClassNameGenerator,
columnEventTimeClass: refineClassNameGenerator,
columnEventTitleClass: refineClassNameGenerator,
columnEventTitleSticky: Boolean,
columnEventBeforeClass: refineClassNameGenerator,
columnEventAfterClass: refineClassNameGenerator,
//
backgroundEventClass: refineClassNameGenerator,
backgroundEventDidMount: identity,
backgroundEventWillUnmount: identity,
backgroundEventContent: identity,
backgroundEventInnerClass: refineClassNameGenerator,
backgroundEventTitleClass: refineClassNameGenerator,
backgroundEventColor: String,
selectConstraint: identity,
selectOverlap: identity,
selectAllow: identity,
droppable: Boolean,
unselectCancel: String,
slotHeaderFormat: identity,
slotLaneClass: refineClassNameGenerator,
slotLaneDidMount: identity,
slotLaneWillUnmount: identity,
slotHeaderClass: refineClassNameGenerator,
slotHeaderInnerClass: refineClassNameGenerator,
slotHeaderContent: identity,
slotHeaderDidMount: identity,
slotHeaderWillUnmount: identity,
slotHeaderAlign: identity,
slotHeaderSticky: identity,
slotHeaderRowClass: refineClassName,
slotHeaderDividerClass: refineClassNameGenerator,
dayMaxEvents: identity,
dayMaxEventRows: identity,
dayMinWidth: Number,
slotHeaderInterval: createDuration,
// in core because more-popover needs it
dayHeaderClass: refineClassNameGenerator,
dayHeaderInnerClass: refineClassNameGenerator,
dayHeaderContent: identity,
dayHeaderDidMount: identity,
dayHeaderWillUnmount: identity,
dayHeaderAlign: identity,
// stickiness for cell-inner-contents laterally. experimental settings
_dayHeaderSticky: identity,
dayHeaderRowClass: refineClassName,
dayHeaderDividerClass: refineClassNameGenerator,
dayRowClass: refineClassName,
dayCellDidMount: identity,
dayCellWillUnmount: identity,
dayCellClass: refineClassNameGenerator,
dayCellInnerClass: refineClassNameGenerator,
dayCellTopContent: identity,
dayCellTopClass: refineClassNameGenerator,
dayCellTopInnerClass: refineClassNameGenerator,
dayCellBottomClass: refineClassNameGenerator,
allDaySlot: Boolean,
allDayText: String,
allDayHeaderClass: refineClassNameGenerator,
allDayHeaderInnerClass: refineClassNameGenerator,
allDayHeaderContent: identity,
allDayHeaderDidMount: identity,
allDayHeaderWillUnmount: identity,
timedText: String,
slotMinWidth: Number,
slotMinHeight: Number,
navLinks: Boolean,
eventTimeFormat: createFormatter,
rerenderDelay: Number, // TODO: move to vanilla right? nah keep here
moreLinkText: identity, // this not enforced :( check others too
moreLinkHint: identity,
selectMinDistance: Number,
selectable: Boolean,
selectLongPressDelay: Number,
eventLongPressDelay: Number,
selectMirror: Boolean,
eventMaxStack: Number,
eventMinHeight: Number,
eventMinWidth: Number,
eventShortHeight: Number,
slotEventOverlap: Boolean,
firstDay: Number,
dayCount: Number,
dateAlignment: String,
dateIncrement: createDuration,
hiddenDays: identity,
fixedWeekCount: Boolean,
validRange: identity, // `this` works?
visibleRange: identity, // `this` works?
titleFormat: identity,
eventInteractive: Boolean,
// only used by list-view, but languages define the value, so we need it in base options
noEventsText: String,
viewHint: identity,
viewChangeHint: String, // for the tab container
navLinkHint: identity,
closeHint: String,
eventsHint: String,
headingLevel: Number,
moreLinkClick: identity,
moreLinkContent: identity,
moreLinkDidMount: identity,
moreLinkWillUnmount: identity,
moreLinkClass: refineClassNameGenerator,
moreLinkInnerClass: refineClassNameGenerator,
//
rowMoreLinkClass: refineClassNameGenerator,
rowMoreLinkInnerClass: refineClassNameGenerator,
//
columnMoreLinkClass: refineClassNameGenerator,
columnMoreLinkInnerClass: refineClassNameGenerator,
navLinkClass: refineClassName,
monthStartFormat: createFormatter,
dayCellFormat: createFormatter,
// for connectors
// (can't be part of plugin system b/c must be provided at runtime)
handleCustomRendering: identity,
customRenderingMetaMap: identity,
popoverClass: refineClassName,
popoverCloseClass: refineClassName,
popoverCloseContent: identity,
dayNarrowWidth: Number,
borderless: Boolean,
borderlessX: Boolean,
borderlessTop: Boolean,
borderlessBottom: Boolean,
fillerClass: refineClassNameGenerator,
headerToolbarClass: refineClassNameGenerator,
footerToolbarClass: refineClassNameGenerator,
toolbarClass: refineClassNameGenerator,
toolbarSectionClass: refineClassNameGenerator,
toolbarTitleClass: refineClassName,
tableClass: refineClassNameGenerator,
tableHeaderClass: refineClassNameGenerator,
tableBodyClass: refineClassNameGenerator,
nonBusinessHoursClass: refineClassName,
highlightClass: refineClassName,
// daygrid-only
dayHeaders: Boolean,
dayHeaderFormat: createFormatter,
// timegrid-only
allDayDividerClass: refineClassName,
// list-only
listDaysClass: refineClassName, // rename this?
listDayClass: refineClassNameGenerator,
//
listDayFormat: createFalsableFormatter, // defaults specified in list plugins
listDayAltFormat: createFalsableFormatter, // "
//
listDayHeaderDidMount: identity,
listDayHeaderWillUnmount: identity,
listDayHeaderClass: refineClassNameGenerator,
listDayHeaderInnerClass: refineClassNameGenerator,
listDayHeaderContent: identity,
//
listDayBodyClass: refineClassNameGenerator,
//
noEventsClass: refineClassNameGenerator,
noEventsInnerClass: refineClassNameGenerator,
noEventsContent: identity,
noEventsDidMount: identity,
noEventsWillUnmount: identity,
// noEventsText is defined in base options
// multimonth-only
multiMonthMaxColumns: Number,
//
singleMonthMinWidth: Number,
singleMonthTitleFormat: createFormatter,
singleMonthDidMount: identity,
singleMonthWillUnmount: identity,
singleMonthClass: refineClassNameGenerator,
singleMonthHeaderClass: refineClassNameGenerator,
singleMonthHeaderInnerClass: refineClassNameGenerator,
};
// do NOT give a type here. need `typeof BASE_OPTION_DEFAULTS` to give real results.
// raw values.
const BASE_OPTION_DEFAULTS = {
buttonDisplay: 'auto',
eventDisplay: 'auto',
defaultTimedEventDuration: '01:00:00',
defaultAllDayEventDuration: { day: 1 },
forceEventDuration: false,
nextDayThreshold: '00:00:00',
initialView: '',
aspectRatio: 1.35,
weekends: true,
weekNumbers: false,
weekNumberCalculation: 'local',
editable: false,
nowIndicator: false,
scrollTime: '06:00:00',
scrollTimeReset: true,
slotMinTime: '00:00:00',
slotMaxTime: '24:00:00',
showNonCurrentDates: true,
lazyFetching: true,
startParam: 'start',
endParam: 'end',
timeZoneParam: 'timeZone',
timeZone: 'local', // TODO: throw error if given falsy value?
locales: [],
locale: '', // blank values means it will compute based off locales[]
dragRevertDuration: 500,
dragScroll: true,
allDayMaintainDuration: false,
unselectAuto: true,
dropAccept: '*',
eventOrder: 'start,-duration,allDay,title',
eventPrintLayout: 'auto',
popoverFormat: { month: 'long', day: 'numeric', year: 'numeric' },
longPressDelay: 1000,
eventDragMinDistance: 5, // only applies to mouse
expandRows: false,
navLinks: false,
selectable: false,
eventMinHeight: 15,
eventMinWidth: 30,
eventShortHeight: 30,
monthStartFormat: { month: 'long', day: 'numeric' },
dayCellFormat: { day: 'numeric', omitTrailing: true },
headingLevel: 2, // like H2
outerBorder: true,
dayNarrowWidth: 80,
eventOverlap: true,
slotHeaderAlign: 'start',
slotHeaderSticky: true,
dayHeaderAlign: 'start',
_dayHeaderSticky: true,
rowEventTitleSticky: true,
columnEventTitleSticky: true,
nowIndicatorSnap: 'auto',
// daygrid-only
dayHeaders: true,
};
// calendar listeners
// ------------------
const CALENDAR_LISTENER_REFINERS = {
datesSet: identity,
eventsSet: identity,
eventAdd: identity,
eventChange: identity,
eventRemove: identity,
eventClick: identity, // TODO: resource for scheduler????
eventMouseEnter: identity,
eventMouseLeave: identity,
select: identity, // resource for scheduler????
unselect: identity,
loading: identity,
// internal
_unmount: identity,
_beforeprint: identity,
_afterprint: identity,
_noDateSelect: identity,
_noEventDrop: identity,
_noEventResize: identity,
_timeScrollRequest: identity,
// interaction-plugin-only
dateClick: identity,
eventDragStart: identity,
eventDragStop: identity,
eventDrop: identity,
eventResizeStart: identity,
eventResizeStop: identity,
eventResize: identity,
drop: identity,
eventReceive: identity,
eventLeave: identity,
};
// calendar-only options (not for view-specific)
// ---------------------------------------------
const CALENDAR_ONLY_OPTION_REFINERS = {
class: refineClassNameGenerator,
className: refineClassNameGenerator,
viewClass: refineClassNameGenerator,
viewDidMount: identity,
viewWillUnmount: identity,
views: identity,
plugins: identity,
initialEvents: identity,
events: identity,
eventSources: identity,
};
// view-specific options
// ---------------------
const VIEW_ONLY_OPTION_REFINERS = {
type: String,
component: identity,
class: refineClassNameGenerator,
className: refineClassNameGenerator,
content: identity,
didMount: identity,
willUnmount: identity,
// internal only
buttonTextKey: String,
dateProfileGeneratorClass: identity,
usesMinMaxTime: Boolean,
disallowAmbigTitle: Boolean,
};
const COMPLEX_OPTION_COMPARATORS = {
// Unfortunately always need 'maybe' to handle undefined inital value, because of CalendarDataManager
dateIncrement: isMaybePropsEqualShallow,
headerToolbar: isMaybePropsEqualShallow,
footerToolbar: isMaybePropsEqualShallow,
buttons: isMaybePropsEqualDepth1,
plugins: isMaybeArraysEqual,
events: isMaybeArraysEqual,
eventSources: isMaybeArraysEqual,
['resources']: isMaybeArraysEqual,
};
// util funcs
// ----------------------------------------------------------------------------------------------------
function refineProps(input, refiners) {
let refined = {};
let extra = {};
for (let propName in refiners) {
if (propName in input) {
refined[propName] = refiners[propName](input[propName], propName);
}
}
for (let propName in input) {
if (!(propName in refiners)) {
extra[propName] = input[propName];
}
}
return { refined, extra };
}
function identity(raw) {
return raw;
}
function createFalsableFormatter(input) {
return input === false ? null : createFormatter(input);
}
/* Date stuff that doesn't belong in datelib core
----------------------------------------------------------------------------------------------------------------------*/
// given a timed range, computes an all-day range that has the same exact duration,
// but whose start time is aligned with the start of the day.
function computeAlignedDayRange(timedRange) {
let dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1;
let start = startOfDay(timedRange.start);
let end = addDays(start, dayCnt);
return { start, end };
}
// given a timed range, computes an all-day range based on how for the end date bleeds into the next day
// TODO: give nextDayThreshold a default arg
function computeVisibleDayRange(timedRange, nextDayThreshold = createDuration(0)) {
let startDay = null;
let endDay = null;
if (timedRange.end) {
endDay = startOfDay(timedRange.end);
let endTimeMS = timedRange.end.valueOf() - endDay.valueOf(); // # of milliseconds into `endDay`
// If the end time is actually inclusively part of the next day and is equal to or
// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) {
endDay = addDays(endDay, 1);
}
}
if (timedRange.start) {
startDay = startOfDay(timedRange.start); // the beginning of the day the range starts
// If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
if (endDay && endDay <= startDay) {
endDay = addDays(startDay, 1);
}
}
return { start: startDay, end: endDay };
}
function diffDates(date0, date1, dateEnv, largeUnit) {
if (largeUnit === 'year') {
return createDuration(dateEnv.diffWholeYears(date0, date1), 'year');
}
if (largeUnit === 'month') {
return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month');
}
return diffDayAndTime(date0, date1); // returns a duration
}
function createEventInstance(defId, range) {
return {
instanceId: guid(),
defId,
range,
};
}
function parseRecurring(refined, defaultAllDay, dateEnv, recurringTypes) {
for (let i = 0; i < recurringTypes.length; i += 1) {
let parsed = recurringTypes[i].parse(refined, dateEnv);
if (parsed) {
let { allDay } = refined;
if (allDay == null) {
allDay = defaultAllDay;
if (allDay == null) {
allDay = parsed.allDayGuess;
if (allDay == null) {
allDay = false;
}
}
}
return {
allDay,
duration: parsed.duration,
typeData: parsed.typeData,
typeId: i,
};
}
}
return null;
}
function expandRecurring(eventStore, framingRange, context) {
let { dateEnv, pluginHooks, options } = context;
let { defs, instances } = eventStore;
// remove existing recurring instances
// TODO: bad. always expand events as a second step
instances = filterHash(instances, (instance) => !defs[instance.defId].recurringDef);
for (let defId in defs) {
let def = defs[defId];
if (def.recurringDef) {
let { duration } = def.recurringDef;
if (!duration) {
duration = def.allDay ?
options.defaultAllDayEventDuration :
options.defaultTimedEventDuration;
}
let starts = expandRecurringRanges(def, duration, framingRange, dateEnv, pluginHooks.recurringTypes);
for (let start of starts) {
let instance = createEventInstance(defId, {
start,
end: dateEnv.add(start, duration),
});
instances[instance.instanceId] = instance;
}
}
}
return { defs, instances };
}
/*
Event MUST have a recurringDef
*/
function expandRecurringRanges(eventDef, duration, framingRange, dateEnv, recurringTypes) {
let typeDef = recurringTypes[eventDef.recurringDef.typeId];
let markers = typeDef.expand(eventDef.recurringDef.typeData, {
start: dateEnv.subtract(framingRange.start, duration), // for when event starts before framing range and goes into
end: framingRange.end,
}, dateEnv);
// the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to
if (eventDef.allDay) {
markers = markers.map(startOfDay);
}
return markers;
}
function parseEvents(rawEvents, eventSource, context, allowOpenRange, defIdMap, instanceIdMap) {
let eventStore = createEmptyEventStore();
let eventRefiners = buildEventRefiners(context);
for (let rawEvent of rawEvents) {
let tuple = parseEvent(rawEvent, eventSource, context, allowOpenRange, eventRefiners, defIdMap, instanceIdMap);
if (tuple) {
eventTupleToStore(tuple, eventStore);
}
}
return eventStore;
}
function eventTupleToStore(tuple, eventStore = createEmptyEventStore()) {
eventStore.defs[tuple.def.defId] = tuple.def;
if (tuple.instance) {
eventStore.instances[tuple.instance.instanceId] = tuple.instance;
}
return eventStore;
}
// retrieves events that have the same groupId as the instance specified by `instanceId`
// or they are the same as the instance.
// why might instanceId not be in the store? an event from another calendar?
function getRelevantEvents(eventStore, instanceId) {
let instance = eventStore.instances[instanceId];
if (instance) {
let def = eventStore.defs[instance.defId];
// get events/instances with same group
let newStore = filterEventStoreDefs(eventStore, (lookDef) => isEventDefsGrouped(def, lookDef));
// add the original
// TODO: wish we could use eventTupleToStore or something like it
newStore.defs[def.defId] = def;
newStore.instances[instance.instanceId] = instance;
return newStore;
}
return createEmptyEventStore();
}
function isEventDefsGrouped(def0, def1) {
return Boolean(def0.groupId && def0.groupId === def1.groupId);
}
function createEmptyEventStore() {
return { defs: {}, instances: {} };
}
function mergeEventStores(store0, store1) {
return {
defs: { ...store0.defs, ...store1.defs },
instances: { ...store0.instances, ...store1.instances },
};
}
function filterEventStoreDefs(eventStore, filterFunc) {
let defs = filterHash(eventStore.defs, filterFunc);
let instances = filterHash(eventStore.instances, (instance) => (defs[instance.defId] // still exists?
));
return { defs, instances };
}
function excludeSubEventStore(master, sub) {
let { defs, instances } = master;
let filteredDefs = {};
let filteredInstances = {};
for (let defId in defs) {
if (!sub.defs[defId]) { // not explicitly excluded
filteredDefs[defId] = defs[defId];
}
}
for (let instanceId in instances) {
if (!sub.instances[instanceId] && // not explicitly excluded
filteredDefs[instances[instanceId].defId] // def wasn't filtered away
) {
filteredInstances[instanceId] = instances[instanceId];
}
}
return {
defs: filteredDefs,
instances: filteredInstances,
};
}
function normalizeConstraint(input, context) {
if (Array.isArray(input)) {
return parseEvents(input, null, context, true); // allowOpenRange=true
}
if (typeof input === 'object' && input) { // non-null object
return parseEvents([input], null, context, true); // allowOpenRange=true
}
if (input != null) {
return String(input);
}
return null;
}
// TODO: better called "EventSettings" or "EventConfig"
// TODO: move this file into structs
// TODO: separate constraint/overlap/allow, because selection uses only that, not other props
const EVENT_UI_REFINERS = {
display: String,
editable: Boolean,
startEditable: Boolean,
durationEditable: Boolean,
constraint: identity, // Identity<ConstraintInput>, // circular reference. ts dies. event->constraint->event
overlap: identity,
allow: identity,
class: refineClassName,
className: refineClassName,
color: String,
contrastColor: String,
};
const EMPTY_EVENT_UI = {
display: null,
startEditable: null,
durationEditable: null,
constraints: [],
overlap: null,
allows: [],
color: '',
contrastColor: '',
className: '',
};
function createEventUi(refined, context) {
let constraint = normalizeConstraint(refined.constraint, context);
return {
display: refined.display || null,
startEditable: refined.startEditable != null ? refined.startEditable : refined.editable,
durationEditable: refined.durationEditable != null ? refined.durationEditable : refined.editable,
constraints: constraint != null ? [constraint] : [],
overlap: refined.overlap != null ? refined.overlap : null,
allows: refined.allow != null ? [refined.allow] : [],
color: refined.color || '',
contrastColor: refined.contrastColor || '',
className: (refined.class ?? refined.className) || '',
};
}
// TODO: prevent against problems with <2 args!
function combineEventUis(uis) {
return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI);
}
function combineTwoEventUis(item0, item1) {
return {
display: item1.display != null ? item1.display : item0.display,
startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable,
durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable,
constraints: item0.constraints.concat(item1.constraints),
overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap,
allows: item0.allows.concat(item1.allows),
color: item1.color || item0.color,
contrastColor: item1.contrastColor || item0.contrastColor,
className: joinClassNames(item0.className, item1.className),
};
}
const EVENT_NON_DATE_REFINERS = {
id: String,
groupId: String,
title: String,
url: String,
interactive: Boolean,
};
const EVENT_DATE_REFINERS = {
start: identity,
end: identity,
date: identity,
allDay: Boolean,
};
const EVENT_REFINERS = {
...EVENT_NON_DATE_REFINERS,
...EVENT_DATE_REFINERS,
extendedProps: identity,
};
function parseEvent(raw, eventSource, context, allowOpenRange, refiners = buildEventRefiners(context), defIdMap, instanceIdMap) {
let { refined, extra } = refineEventDef(raw, context, refiners);
let defaultAllDay = computeIsDefaultAllDay(eventSource, context);
let recurringRes = parseRecurring(refined, defaultAllDay, context.dateEnv, context.pluginHooks.recurringTypes);
if (recurringRes) {
let def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', recurringRes.allDay, Boolean(recurringRes.duration), context, defIdMap);
def.recurringDef = {
typeId: recurringRes.typeId,
typeData: recurringRes.typeData,
duration: recurringRes.duration,
};
return { def, instance: null };
}
let singleRes = parseSingle(refined, defaultAllDay, context, allowOpenRange);
if (singleRes) {
let def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', singleRes.allDay, singleRes.hasEnd, context, defIdMap);
let instance = createEventInstance(def.defId, singleRes.range);
if (instanceIdMap && def.publicId && instanceIdMap[def.publicId]) {
instance.instanceId = instanceIdMap[def.publicId];
}
return { def, instance };
}
return null;
}
function refineEventDef(raw, context, refiners = buildEventRefiners(context)) {
return refineProps(raw, refiners);
}
function buildEventRefiners(context) {
return { ...EVENT_UI_REFINERS, ...EVENT_REFINERS, ...context.pluginHooks.eventRefiners };
}
/*
Will NOT populate extendedProps with the leftover properties.
Will NOT populate date-related props.
*/
function parseEventDef(refined, extra, sourceId, allDay, hasEnd, context, defIdMap) {
let def = {
title: refined.title || '',
groupId: refined.groupId || '',
publicId: refined.id || '',
url: refined.url || '',
recurringDef: null,
defId: ((defIdMap && refined.id) ? defIdMap[refined.id] : '') || guid(),
sourceId,
allDay,
hasEnd,
interactive: refined.interactive,
ui: createEventUi(refined, context),
extendedProps: {
...(refined.extendedProps || {}),
...extra,
},
};
for (let memberAdder of context.pluginHooks.eventDefMemberAdders) {
Object.assign(def, memberAdder(refined));
}
// help out EventImpl from having user modify props
Object.freeze(def.ui.className); // might be simple string, but freeze still works
Object.freeze(def.extendedProps);
return def;
}
function parseSingle(refined, defaultAllDay, context, allowOpenRange) {
let { allDay } = refined;
let startMeta;
let startMarker = null;
let hasEnd = false;
let endMeta;
let endMarker = null;
let startInput = refined.start != null ? refined.start : refined.date;
startMeta = context.dateEnv.createMarkerMeta(startInput);
if (startMeta) {
startMarker = startMeta.marker;
}
else if (!allowOpenRange) {
return null;
}
if (refined.end != null) {
endMeta = context.dateEnv.createMarkerMeta(refined.end);
}
if (allDay == null) {
if (defaultAllDay != null) {
allDay = defaultAllDay;
}
else {
// fall back to the date props LAST
allDay = (!startMeta || startMeta.isTimeUnspecified) &&
(!endMeta || endMeta.isTimeUnspecified);
}
}
if (allDay && startMarker) {
startMarker = startOfDay(startMarker);
}
if (endMeta) {
endMarker = endMeta.marker;
if (allDay) {
endMarker = startOfDay(endMarker);
}
if (startMarker && endMarker <= startMarker) {
endMarker = null;
}
}
if (endMarker) {
hasEnd = true;
}
else if (!allowOpenRange) {
hasEnd = context.options.forceEventDuration || false;
endMarker = context.dateEnv.add(startMarker, allDay ?
context.options.defaultAllDayEventDuration :
context.options.defaultTimedEventDuration);
}
return {
allDay,
hasEnd,
range: { start: startMarker, end: endMarker },
};
}
function computeIsDefaultAllDay(eventSource, context) {
let res = null;
if (eventSource) {
res = eventSource.defaultAllDay;
}
if (res == null) {
res = context.options.defaultAllDay;
}
return res;
}
const STANDARD_PROPS = {
start: identity,
end: identity,
allDay: Boolean,
};
function parseDateSpan(raw, dateEnv, defaultDuration) {
let span = parseOpenDateSpan(raw, dateEnv);
let { range } = span;
if (!range.start) {
return null;
}
if (!range.end) {
if (defaultDuration == null) {
return null;
}
range.end = dateEnv.add(range.start, defaultDuration);
}
return span;
}
/*
TODO: somehow combine with parseRange?
Will return null if the start/end props were present but parsed invalidly.
*/
function parseOpenDateSpan(raw, dateEnv) {
let { refined: standardProps, extra } = refineProps(raw, STANDARD_PROPS);
let startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null;
let endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null;
let { allDay } = standardProps;
if (allDay == null) {
allDay = (startMeta && startMeta.isTimeUnspecified) &&
(!endMeta || endMeta.isTimeUnspecified);
}
return {
range: {
start: startMeta ? startMeta.marker : null,
end: endMeta ? endMeta.marker : null,
},
allDay,
...extra,
};
}
function isDateSpansEqual(span0, span1) {
return rangesEqual(span0.range, span1.range) &&
span0.allDay === span1.allDay &&
isSpanPropsEqual(span0, span1);
}
// the NON-DATE-RELATED props
function isSpanPropsEqual(span0, span1) {
for (let propName in span1) {
if (propName !== 'range' && propName !== 'allDay') {
if (span0[propName] !== span1[propName]) {
return false;
}
}
}
// are there any props that span0 has that span1 DOESN'T have?
// both have range/allDay, so no need to special-case.
for (let propName in span0) {
if (!(propName in span1)) {
return false;
}
}
return true;
}
function buildDateSpanApi(span, dateEnv) {
return {
...buildRangeApi(span.range, dateEnv, span.allDay),
allDay: span.allDay,
};
}
function buildRangeApiWithTimeZone(range, dateEnv, omitTime) {
return {
...buildRangeApi(range, dateEnv, omitTime),
timeZone: dateEnv.timeZone,
};
}
function buildRangeApi(range, dateEnv, omitTime) {
return {
start: dateEnv.toDate(range.start),
end: dateEnv.toDate(range.end),
startStr: dateEnv.formatIso(range.start, { omitTime }),
endStr: dateEnv.formatIso(range.end, { omitTime }),
};
}
function fabricateEventRange(dateSpan, eventUiBases, context) {
let res = refineEventDef({ editable: false }, context);
let def = parseEventDef(res.refined, res.extra, '', // sourceId
dateSpan.allDay, true, // hasEnd
context);
return {
def,
ui: compileEventUi(def, eventUiBases),
instance: createEventInstance(def.defId, d