UNPKG

@fullcalendar/web-component

Version:
1,407 lines (1,391 loc) 82.7 kB
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