import { createDuration } from './datelib/duration.js' import { mergeProps, isPropsEqual } from './util/object.js' import { isArraysEqual } from './util/array.js' import { createFormatter } from './datelib/formatting.js' import { parseFieldSpecs } from './util/misc.js' import { DateProfileGeneratorClass } from './DateProfileGenerator.js' import { CalendarApi } from './api/CalendarApi.js' import { ViewApi } from './api/ViewApi.js' import { EventApi } from './api/EventApi.js' import { CssDimValue, DateInput, DateRangeInput, BusinessHoursInput, EventSourceInput, LocaleSingularArg, LocaleInput, EventInput, EventInputTransformer, OverlapFunc, ConstraintInput, AllowFunc, PluginDef, ViewComponentType, SpecificViewContentArg, SpecificViewMountArg, ClassNamesGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler, NowIndicatorContentArg, NowIndicatorMountArg, WeekNumberContentArg, WeekNumberMountArg, SlotLaneContentArg, SlotLaneMountArg, SlotLabelContentArg, SlotLabelMountArg, AllDayContentArg, AllDayMountArg, DayHeaderContentArg, DayHeaderMountArg, DayCellContentArg, DayCellMountArg, ViewContentArg, ViewMountArg, EventClickArg, EventHoveringArg, DateSelectArg, DateUnselectArg, WeekNumberCalculation, FormatterInput, ToolbarInput, CustomButtonInput, ButtonIconsInput, ButtonTextCompoundInput, EventContentArg, EventMountArg, DatesSetArg, EventAddArg, EventChangeArg, EventRemoveArg, MoreLinkContentArg, MoreLinkMountArg, MoreLinkAction, ButtonHintCompoundInput, CustomRenderingHandler, } from './api/structs.js' // base options // ------------ export const BASE_OPTION_REFINERS = { navLinkDayClick: identity as Identity void)>, navLinkWeekClick: identity as Identity void)>, duration: createDuration, bootstrapFontAwesome: identity as Identity, // TODO: move to bootstrap plugin buttonIcons: identity as Identity, customButtons: identity as Identity<{ [name: string]: CustomButtonInput }>, defaultAllDayEventDuration: createDuration, defaultTimedEventDuration: createDuration, nextDayThreshold: createDuration, scrollTime: createDuration, scrollTimeReset: Boolean, slotMinTime: createDuration, slotMaxTime: createDuration, dayPopoverFormat: createFormatter, slotDuration: createDuration, snapDuration: createDuration, headerToolbar: identity as Identity, footerToolbar: identity as Identity, defaultRangeSeparator: String, titleRangeSeparator: String, forceEventDuration: Boolean, dayHeaders: Boolean, dayHeaderFormat: createFormatter, dayHeaderClassNames: identity as Identity>, dayHeaderContent: identity as Identity>, dayHeaderDidMount: identity as Identity>, dayHeaderWillUnmount: identity as Identity>, dayCellClassNames: identity as Identity>, dayCellContent: identity as Identity>, dayCellDidMount: identity as Identity>, dayCellWillUnmount: identity as Identity>, initialView: String, aspectRatio: Number, weekends: Boolean, weekNumberCalculation: identity as Identity, weekNumbers: Boolean, weekNumberClassNames: identity as Identity>, weekNumberContent: identity as Identity>, weekNumberDidMount: identity as Identity>, weekNumberWillUnmount: identity as Identity>, editable: Boolean, viewClassNames: identity as Identity>, viewDidMount: identity as Identity>, viewWillUnmount: identity as Identity>, nowIndicator: Boolean, nowIndicatorClassNames: identity as Identity>, nowIndicatorContent: identity as Identity>, nowIndicatorDidMount: identity as Identity>, nowIndicatorWillUnmount: identity as Identity>, showNonCurrentDates: Boolean, lazyFetching: Boolean, startParam: String, endParam: String, timeZoneParam: String, timeZone: String, locales: identity as Identity, locale: identity as Identity, themeSystem: String as Identity<'standard' | string>, dragRevertDuration: Number, dragScroll: Boolean, allDayMaintainDuration: Boolean, unselectAuto: Boolean, dropAccept: identity as Identity boolean)>, // TODO: type draggable eventOrder: parseFieldSpecs, eventOrderStrict: Boolean, handleWindowResize: Boolean, windowResizeDelay: Number, longPressDelay: Number, eventDragMinDistance: Number, expandRows: Boolean, height: identity as Identity, contentHeight: identity as Identity, direction: String as Identity<'ltr' | 'rtl'>, weekNumberFormat: createFormatter, eventResizableFromStart: Boolean, displayEventTime: Boolean, displayEventEnd: Boolean, weekText: String, // the short version weekTextLong: String, // falls back to weekText progressiveEventRendering: Boolean, businessHours: identity as Identity, initialDate: identity as Identity, now: identity as Identity DateInput)>, eventDataTransform: identity as Identity, stickyHeaderDates: identity as Identity, stickyFooterScrollbar: identity as Identity, viewHeight: identity as Identity, defaultAllDay: Boolean, eventSourceFailure: identity as Identity<(this: CalendarApi, error: any) => void>, eventSourceSuccess: identity as Identity<(this: CalendarApi, eventsInput: EventInput[], response?: Response) => EventInput[] | void>, eventDisplay: String, // TODO: give more specific eventStartEditable: Boolean, eventDurationEditable: Boolean, eventOverlap: identity as Identity, eventConstraint: identity as Identity, eventAllow: identity as Identity, eventBackgroundColor: String, eventBorderColor: String, eventTextColor: String, eventColor: String, eventClassNames: identity as Identity>, eventContent: identity as Identity>, eventDidMount: identity as Identity>, eventWillUnmount: identity as Identity>, selectConstraint: identity as Identity, selectOverlap: identity as Identity, selectAllow: identity as Identity, droppable: Boolean, unselectCancel: String, slotLabelFormat: identity as Identity, slotLaneClassNames: identity as Identity>, slotLaneContent: identity as Identity>, slotLaneDidMount: identity as Identity>, slotLaneWillUnmount: identity as Identity>, slotLabelClassNames: identity as Identity>, slotLabelContent: identity as Identity>, slotLabelDidMount: identity as Identity>, slotLabelWillUnmount: identity as Identity>, dayMaxEvents: identity as Identity, dayMaxEventRows: identity as Identity, dayMinWidth: Number, slotLabelInterval: createDuration, allDayText: String, allDayClassNames: identity as Identity>, allDayContent: identity as Identity>, allDayDidMount: identity as Identity>, allDayWillUnmount: identity as Identity>, slotMinWidth: Number, // move to timeline? navLinks: Boolean, eventTimeFormat: createFormatter, rerenderDelay: Number, // TODO: move to @fullcalendar/core right? nah keep here moreLinkText: identity as Identity string)>, // this not enforced :( check others too moreLinkHint: identity as Identity string)>, selectMinDistance: Number, selectable: Boolean, selectLongPressDelay: Number, eventLongPressDelay: Number, selectMirror: Boolean, eventMaxStack: Number, eventMinHeight: Number, eventMinWidth: Number, eventShortHeight: Number, slotEventOverlap: Boolean, plugins: identity as Identity, firstDay: Number, dayCount: Number, dateAlignment: String, dateIncrement: createDuration, hiddenDays: identity as Identity, fixedWeekCount: Boolean, validRange: identity as Identity DateRangeInput)>, // `this` works? visibleRange: identity as Identity DateRangeInput)>, // `this` works? titleFormat: identity as Identity, // DONT parse just yet. we need to inject titleSeparator eventInteractive: Boolean, // only used by list-view, but languages define the value, so we need it in base options noEventsText: String, viewHint: identity as Identity string)>, navLinkHint: identity as Identity string)>, closeHint: String, timeHint: String, eventHint: String, moreLinkClick: identity as Identity, moreLinkClassNames: identity as Identity>, moreLinkContent: identity as Identity>, moreLinkDidMount: identity as Identity>, moreLinkWillUnmount: identity as Identity>, monthStartFormat: createFormatter, // for connectors // (can't be part of plugin system b/c must be provided at runtime) handleCustomRendering: identity as Identity>, customRenderingMetaMap: identity as Identity<{ [optionName: string]: any }>, customRenderingReplaces: Boolean, } type BuiltInBaseOptionRefiners = typeof BASE_OPTION_REFINERS export interface BaseOptionRefiners extends BuiltInBaseOptionRefiners { // for ambient-extending } export type BaseOptions = RawOptionsFromRefiners< // as RawOptions Required // Required is a hack for "Index signature is missing" > // do NOT give a type here. need `typeof BASE_OPTION_DEFAULTS` to give real results. // raw values. export const BASE_OPTION_DEFAULTS = { eventDisplay: 'auto', defaultRangeSeparator: ' - ', titleRangeSeparator: ' \u2013 ', // en dash defaultTimedEventDuration: '01:00:00', defaultAllDayEventDuration: { day: 1 }, forceEventDuration: false, nextDayThreshold: '00:00:00', dayHeaders: true, initialView: '', aspectRatio: 1.35, headerToolbar: { start: 'title', center: '', end: 'today prev,next', }, weekends: true, weekNumbers: false, weekNumberCalculation: 'local' as WeekNumberCalculation, 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[] themeSystem: 'standard', dragRevertDuration: 500, dragScroll: true, allDayMaintainDuration: false, unselectAuto: true, dropAccept: '*', eventOrder: 'start,-duration,allDay,title', dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' }, handleWindowResize: true, windowResizeDelay: 100, // milliseconds before an updateSize happens 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' }, } export type BaseOptionsRefined = DefaultedRefinedOptions< RefinedOptionsFromRefiners>, // Required is a hack for "Index signature is missing" keyof typeof BASE_OPTION_DEFAULTS > // calendar listeners // ------------------ export const CALENDAR_LISTENER_REFINERS = { datesSet: identity as Identity<(arg: DatesSetArg) => void>, eventsSet: identity as Identity<(events: EventApi[]) => void>, eventAdd: identity as Identity<(arg: EventAddArg) => void>, eventChange: identity as Identity<(arg: EventChangeArg) => void>, eventRemove: identity as Identity<(arg: EventRemoveArg) => void>, windowResize: identity as Identity<(arg: { view: ViewApi }) => void>, eventClick: identity as Identity<(arg: EventClickArg) => void>, // TODO: resource for scheduler???? eventMouseEnter: identity as Identity<(arg: EventHoveringArg) => void>, eventMouseLeave: identity as Identity<(arg: EventHoveringArg) => void>, select: identity as Identity<(arg: DateSelectArg) => void>, // resource for scheduler???? unselect: identity as Identity<(arg: DateUnselectArg) => void>, loading: identity as Identity<(isLoading: boolean) => void>, // internal _unmount: identity as Identity<() => void>, _beforeprint: identity as Identity<() => void>, _afterprint: identity as Identity<() => void>, _noEventDrop: identity as Identity<() => void>, _noEventResize: identity as Identity<() => void>, _resize: identity as Identity<(forced: boolean) => void>, _scrollRequest: identity as Identity<(arg: any) => void>, } type BuiltInCalendarListenerRefiners = typeof CALENDAR_LISTENER_REFINERS export interface CalendarListenerRefiners extends BuiltInCalendarListenerRefiners { // for ambient extending } type CalendarListenersLoose = RefinedOptionsFromRefiners> // Required hack export type CalendarListeners = Required // much more convenient for Emitters and whatnot // calendar-specific options // ------------------------- export const CALENDAR_OPTION_REFINERS = { // does not include base nor calendar listeners buttonText: identity as Identity, buttonHints: identity as Identity, views: identity as Identity<{ [viewId: string]: ViewOptions }>, plugins: identity as Identity, initialEvents: identity as Identity, events: identity as Identity, eventSources: identity as Identity, } type BuiltInCalendarOptionRefiners = typeof CALENDAR_OPTION_REFINERS export interface CalendarOptionRefiners extends BuiltInCalendarOptionRefiners { // for ambient-extending } export type CalendarOptions = BaseOptions & CalendarListenersLoose & RawOptionsFromRefiners> // Required hack https://github.com/microsoft/TypeScript/issues/15300 export type CalendarOptionsRefined = BaseOptionsRefined & CalendarListenersLoose & RefinedOptionsFromRefiners> // Required hack export const COMPLEX_OPTION_COMPARATORS: { [optionName in keyof CalendarOptions]: (a: CalendarOptions[optionName], b: CalendarOptions[optionName]) => boolean } = { headerToolbar: isMaybeObjectsEqual, footerToolbar: isMaybeObjectsEqual, buttonText: isMaybeObjectsEqual, buttonHints: isMaybeObjectsEqual, buttonIcons: isMaybeObjectsEqual, dateIncrement: isMaybeObjectsEqual, plugins: isMaybeArraysEqual, events: isMaybeArraysEqual, eventSources: isMaybeArraysEqual, ['resources' as any]: isMaybeArraysEqual, } export function isMaybeObjectsEqual(a, b) { if (typeof a === 'object' && typeof b === 'object' && a && b) { // both non-null objects return isPropsEqual(a, b) } return a === b } function isMaybeArraysEqual(a, b) { if (Array.isArray(a) && Array.isArray(b)) { return isArraysEqual(a, b) } return a === b } // view-specific options // --------------------- export const VIEW_OPTION_REFINERS: { [name: string]: any } = { type: String, component: identity as Identity, buttonText: String, buttonTextKey: String, // internal only dateProfileGeneratorClass: identity as Identity, usesMinMaxTime: Boolean, // internal only classNames: identity as Identity>, content: identity as Identity>, didMount: identity as Identity>, willUnmount: identity as Identity>, } type BuiltInViewOptionRefiners = typeof VIEW_OPTION_REFINERS export interface ViewOptionRefiners extends BuiltInViewOptionRefiners { // for ambient-extending } export type ViewOptions = BaseOptions & CalendarListenersLoose & RawOptionsFromRefiners> // Required hack export type ViewOptionsRefined = BaseOptionsRefined & CalendarListenersLoose & RefinedOptionsFromRefiners> // Required hack // util funcs // ---------------------------------------------------------------------------------------------------- export function mergeRawOptions(optionSets: Dictionary[]) { return mergeProps(optionSets, COMPLEX_OPTION_COMPARATORS) } export function refineProps>( input: Raw, refiners: Refiners, ): { refined: RefinedOptionsFromRefiners, extra: Dictionary, } { let refined = {} as any let extra = {} as any for (let propName in refiners) { if (propName in input) { refined[propName] = refiners[propName](input[propName]) } } for (let propName in input) { if (!(propName in refiners)) { extra[propName] = input[propName] } } return { refined, extra } } // definition utils // ---------------------------------------------------------------------------------------------------- export type GenericRefiners = { [propName: string]: (input: any) => any } export type GenericListenerRefiners = { [listenerName: string]: Identity<(this: CalendarApi, ...args: any[]) => void> } export type RawOptionsFromRefiners = { [Prop in keyof Refiners]?: // all optional Refiners[Prop] extends ((input: infer RawType) => infer RefinedType) ? (any extends RawType ? RefinedType : RawType) // if input type `any`, use output (for Boolean/Number/String) : never } export type RefinedOptionsFromRefiners = { [Prop in keyof Refiners]?: // all optional Refiners[Prop] extends ((input: any) => infer RefinedType) ? RefinedType : never } export type DefaultedRefinedOptions = Required> & Partial> export type Dictionary = Record export type Identity = (raw: T) => T export function identity(raw: T): T { return raw }