import { __assign } from 'tslib' ; import { buildLocale , RawLocaleInfo , organizeRawLocales , LocaleSingularArg } from '../datelib/locale' ; import { memoize , memoizeObjArg } from '../util/memoize' ; import { Action } from './Action' ; import { buildBuildPluginHooks } from '../plugin-system' ; import { PluginHooks } from '../plugin-system-struct' ; import { DateEnv } from '../datelib/env' ; import { CalendarApi } from '../CalendarApi' ; import { StandardTheme } from '../theme/StandardTheme' ; import { EventSourceHash } from '../structs/event-source' ; import { buildViewSpecs , ViewSpec } from '../structs/view-spec' ; import { mapHash , isPropsEqual } from '../util/object' ; import { DateProfileGenerator , DateProfileGeneratorProps } from '../DateProfileGenerator' ; import { reduceViewType } from './view-type' ; import { getInitialDate , reduceCurrentDate } from './current-date' ; import { reduceDynamicOptionOverrides } from './options' ; import { reduceDateProfile } from './date-profile' ; import { reduceEventSources , initEventSources , reduceEventSourcesNewTimeZone , computeEventSourcesLoading } from './eventSources' ; import { reduceEventStore , rezoneEventStoreDates } from './eventStore' ; import { reduceDateSelection } from './date-selection' ; import { reduceSelectedEvent } from './selected-event' ; import { reduceEventDrag } from './event-drag' ; import { reduceEventResize } from './event-resize' ; import { Emitter } from '../common/Emitter' ; import { EventUiHash , EventUi , createEventUi } from '../component/event-ui' ; import { EventDefHash } from '../structs/event-def' ; import { parseToolbars } from '../toolbar-parse' ; import { CalendarOptionsRefined , CalendarOptions , CALENDAR_OPTION_REFINERS , COMPLEX_OPTION_COMPARATORS , ViewOptions , ViewOptionsRefined , BASE_OPTION_DEFAULTS , mergeRawOptions , BASE_OPTION_REFINERS , VIEW_OPTION_REFINERS , CalendarListeners , CALENDAR_LISTENER_REFINERS , Dictionary , } from '../options' ; import { rangeContainsMarker } from '../datelib/date-range' ; import { ViewApi } from '../ViewApi' ; import { parseBusinessHours } from '../structs/business-hours' ; import { globalPlugins } from '../global-plugins' ; import { createEmptyEventStore } from '../structs/event-store' ; import { CalendarContext } from '../CalendarContext' ; import { CalendarDataManagerState , CalendarOptionsData , CalendarCurrentViewData , CalendarData } from './data-types' ; import { TaskRunner } from '../util/TaskRunner' ; import { buildTitle } from './title-formatting' ; export interface CalendarDataManagerProps { optionOverrides : CalendarOptions ; calendarApi : CalendarApi ; onAction ? : ( action : Action ) => void ; onData ? : ( data : CalendarData ) => void ; } export type ReducerFunc = ( currentState : Dictionary | null , action : Action | null , context : CalendarContext & CalendarDataManagerState ) => Dictionary ; export class CalendarDataManager { private computeOptionsData = memoize ( this . _computeOptionsData ) ; private computeCurrentViewData = memoize ( this . _computeCurrentViewData ) ; private organizeRawLocales = memoize ( organizeRawLocales ) ; private buildLocale = memoize ( buildLocale ) ; private buildPluginHooks = buildBuildPluginHooks ( ) ; private buildDateEnv = memoize ( buildDateEnv ) ; private buildTheme = memoize ( buildTheme ) ; private parseToolbars = memoize ( parseToolbars ) ; private buildViewSpecs = memoize ( buildViewSpecs ) ; private buildDateProfileGenerator = memoizeObjArg ( buildDateProfileGenerator ) ; private buildViewApi = memoize ( buildViewApi ) ; private buildViewUiProps = memoizeObjArg ( buildViewUiProps ) ; private buildEventUiBySource = memoize ( buildEventUiBySource , isPropsEqual ) ; private buildEventUiBases = memoize ( buildEventUiBases ) ; private parseContextBusinessHours = memoizeObjArg ( parseContextBusinessHours ) ; private buildTitle = memoize ( buildTitle ) ; public emitter = new Emitter < CalendarListeners > ( ) ; private actionRunner = new TaskRunner ( this . _handleAction . bind ( this ) , this . updateData . bind ( this ) ) ; private props : CalendarDataManagerProps ; private state : CalendarDataManagerState ; private data : CalendarData ; public currentCalendarOptionsInput : CalendarOptions = { } ; private currentCalendarOptionsRefined : CalendarOptionsRefined = ( { } as any ) ; private currentViewOptionsInput : ViewOptions = { } ; private currentViewOptionsRefined : ViewOptionsRefined = ( { } as any ) ; public currentCalendarOptionsRefiners : any = { } ; constructor ( props ) { this . props = props ; this . actionRunner . pause ( ) ; let dynamicOptionOverrides = { } ; let optionsData = this . computeOptionsData ( props . optionOverrides , dynamicOptionOverrides , props . calendarApi ) ; let currentViewType = optionsData . calendarOptions . initialView || optionsData . pluginHooks . initialView ; let currentViewData = this . computeCurrentViewData ( currentViewType , optionsData , props . optionOverrides , dynamicOptionOverrides ) ; props . calendarApi . currentDataManager = this ; this . emitter . setThisContext ( props . calendarApi ) ; this . emitter . setOptions ( currentViewData . options ) ; let currentDate = getInitialDate ( optionsData . calendarOptions , optionsData . dateEnv ) ; let dateProfile = currentViewData . dateProfileGenerator . build ( currentDate ) ; if ( ! rangeContainsMarker ( dateProfile . activeRange , currentDate ) ) { currentDate = dateProfile . currentRange . start ; } let calendarContext = { dateEnv : optionsData . dateEnv , options : optionsData . calendarOptions , pluginHooks : optionsData . pluginHooks , calendarApi : props . calendarApi , dispatch : this . dispatch , emitter : this . emitter , getCurrentData : this . getCurrentData , } ; for ( let callback of optionsData . pluginHooks . contextInit ) { callback ( calendarContext ) ; } let eventSources = initEventSources ( optionsData . calendarOptions , dateProfile , calendarContext ) ; let initialState = { dynamicOptionOverrides , currentViewType , currentDate , dateProfile , businessHours : this . parseContextBusinessHours ( calendarContext ) , eventSources , eventUiBases : { } , eventStore : createEmptyEventStore ( ) , renderableEventStore : createEmptyEventStore ( ) , dateSelection : null , eventSelection : '' , eventDrag : null , eventResize : null , selectionConfig : this . buildViewUiProps ( calendarContext ) . selectionConfig , } ; let contextAndState = { ... calendarContext , ... initialState } ; for ( let reducer of optionsData . pluginHooks . reducers ) { __assign ( initialState , reducer ( null , null , contextAndState ) ) ; } if ( computeIsLoading ( initialState , calendarContext ) ) { this . emitter . trigger ( 'loading' , true ) ; } this . state = initialState ; this . updateData ( ) ; this . actionRunner . resume ( ) ; } getCurrentData = ( ) => this . data ; dispatch = ( action ) => { this . actionRunner . request ( action ) ; } ; resetOptions ( optionOverrides , append ? ) { let { props } = this ; props . optionOverrides = append ? { ... props . optionOverrides , ... optionOverrides } : optionOverrides ; this . actionRunner . request ( { type : 'NOTHING' , } ) ; } _handleAction ( action ) { let { props , state , emitter } = this ; let dynamicOptionOverrides = reduceDynamicOptionOverrides ( state . dynamicOptionOverrides , action ) ; let optionsData = this . computeOptionsData ( props . optionOverrides , dynamicOptionOverrides , props . calendarApi ) ; let currentViewType = reduceViewType ( state . currentViewType , action ) ; let currentViewData = this . computeCurrentViewData ( currentViewType , optionsData , props . optionOverrides , dynamicOptionOverrides ) ; props . calendarApi . currentDataManager = this ; emitter . setThisContext ( props . calendarApi ) ; emitter . setOptions ( currentViewData . options ) ; let calendarContext = { dateEnv : optionsData . dateEnv , options : optionsData . calendarOptions , pluginHooks : optionsData . pluginHooks , calendarApi : props . calendarApi , dispatch : this . dispatch , emitter , getCurrentData : this . getCurrentData , } ; let { currentDate , dateProfile } = state ; if ( this . data && this . data . dateProfileGenerator !== currentViewData . dateProfileGenerator ) { dateProfile = currentViewData . dateProfileGenerator . build ( currentDate ) ; } currentDate = reduceCurrentDate ( currentDate , action ) ; dateProfile = reduceDateProfile ( dateProfile , action , currentDate , currentViewData . dateProfileGenerator ) ; if ( action . type === 'PREV' || action . type === 'NEXT' || ! rangeContainsMarker ( dateProfile . currentRange , currentDate ) ) { currentDate = dateProfile . currentRange . start ; } let eventSources = reduceEventSources ( state . eventSources , action , dateProfile , calendarContext ) ; let eventStore = reduceEventStore ( state . eventStore , action , eventSources , dateProfile , calendarContext ) ; let isEventsLoading = computeEventSourcesLoading ( eventSources ) ; let renderableEventStore = ( isEventsLoading && ! currentViewData . options . progressiveEventRendering ) ? ( state . renderableEventStore || eventStore ) : eventStore ; let { eventUiSingleBase , selectionConfig } = this . buildViewUiProps ( calendarContext ) ; let eventUiBySource = this . buildEventUiBySource ( eventSources ) ; let eventUiBases = this . buildEventUiBases ( renderableEventStore . defs , eventUiSingleBase , eventUiBySource ) ; let newState = { dynamicOptionOverrides , currentViewType , currentDate , dateProfile , eventSources , eventStore , renderableEventStore , selectionConfig , eventUiBases , businessHours : this . parseContextBusinessHours ( calendarContext ) , dateSelection : reduceDateSelection ( state . dateSelection , action ) , eventSelection : reduceSelectedEvent ( state . eventSelection , action ) , eventDrag : reduceEventDrag ( state . eventDrag , action ) , eventResize : reduceEventResize ( state . eventResize , action ) , } ; let contextAndState = { ... calendarContext , ... newState } ; for ( let reducer of optionsData . pluginHooks . reducers ) { __assign ( newState , reducer ( state , action , contextAndState ) ) ; } let wasLoading = computeIsLoading ( state , calendarContext ) ; let isLoading = computeIsLoading ( newState , calendarContext ) ; if ( ! wasLoading && isLoading ) { emitter . trigger ( 'loading' , true ) ; } else if ( wasLoading && ! isLoading ) { emitter . trigger ( 'loading' , false ) ; } this . state = newState ; if ( props . onAction ) { props . onAction ( action ) ; } } updateData ( ) { let { props , state } = this ; let oldData = this . data ; let optionsData = this . computeOptionsData ( props . optionOverrides , state . dynamicOptionOverrides , props . calendarApi ) ; let currentViewData = this . computeCurrentViewData ( state . currentViewType , optionsData , props . optionOverrides , state . dynamicOptionOverrides ) ; let data = this . data = { viewTitle : this . buildTitle ( state . dateProfile , currentViewData . options , optionsData . dateEnv ) , calendarApi : props . calendarApi , dispatch : this . dispatch , emitter : this . emitter , getCurrentData : this . getCurrentData , ... optionsData , ... currentViewData , ... state , } ; let changeHandlers = optionsData . pluginHooks . optionChangeHandlers ; let oldCalendarOptions = oldData && oldData . calendarOptions ; let newCalendarOptions = optionsData . calendarOptions ; if ( oldCalendarOptions && oldCalendarOptions !== newCalendarOptions ) { if ( oldCalendarOptions . timeZone !== newCalendarOptions . timeZone ) { state . eventSources = data . eventSources = reduceEventSourcesNewTimeZone ( data . eventSources , state . dateProfile , data ) ; state . eventStore = data . eventStore = rezoneEventStoreDates ( data . eventStore , oldData . dateEnv , data . dateEnv ) ; } for ( let optionName in changeHandlers ) { if ( oldCalendarOptions [ optionName ] !== newCalendarOptions [ optionName ] ) { changeHandlers [ optionName ] ( newCalendarOptions [ optionName ] , data ) ; } } } if ( props . onData ) { props . onData ( data ) ; } } _computeOptionsData ( optionOverrides , dynamicOptionOverrides , calendarApi ) { let { refinedOptions , pluginHooks , localeDefaults , availableLocaleData , extra , } = this . processRawCalendarOptions ( optionOverrides , dynamicOptionOverrides ) ; warnUnknownOptions ( extra ) ; let dateEnv = this . buildDateEnv ( refinedOptions . timeZone , refinedOptions . locale , refinedOptions . weekNumberCalculation , refinedOptions . firstDay , refinedOptions . weekText , pluginHooks , availableLocaleData , refinedOptions . defaultRangeSeparator ) ; let viewSpecs = this . buildViewSpecs ( pluginHooks . views , optionOverrides , dynamicOptionOverrides , localeDefaults ) ; let theme = this . buildTheme ( refinedOptions , pluginHooks ) ; let toolbarConfig = this . parseToolbars ( refinedOptions , optionOverrides , theme , viewSpecs , calendarApi ) ; return { calendarOptions : refinedOptions , pluginHooks , dateEnv , viewSpecs , theme , toolbarConfig , localeDefaults , availableRawLocales : availableLocaleData . map , } ; } processRawCalendarOptions ( optionOverrides , dynamicOptionOverrides ) { let { locales , locale } = mergeRawOptions ( [ BASE_OPTION_DEFAULTS , optionOverrides , dynamicOptionOverrides , ] ) ; let availableLocaleData = this . organizeRawLocales ( locales ) ; let availableRawLocales = availableLocaleData . map ; let localeDefaults = this . buildLocale ( locale || availableLocaleData . defaultCode , availableRawLocales ) . options ; let pluginHooks = this . buildPluginHooks ( optionOverrides . plugins || [ ] , globalPlugins ) ; let refiners = this . currentCalendarOptionsRefiners = { ... BASE_OPTION_REFINERS , ... CALENDAR_LISTENER_REFINERS , ... CALENDAR_OPTION_REFINERS , ... pluginHooks . listenerRefiners , ... pluginHooks . optionRefiners , } ; let extra = { } ; let raw = mergeRawOptions ( [ BASE_OPTION_DEFAULTS , localeDefaults , optionOverrides , dynamicOptionOverrides , ] ) ; let refined = { } ; let currentRaw = this . currentCalendarOptionsInput ; let currentRefined = this . currentCalendarOptionsRefined ; let anyChanges = false ; for ( let optionName in raw ) { if ( optionName !== 'plugins' ) { if ( raw [ optionName ] === currentRaw [ optionName ] || ( COMPLEX_OPTION_COMPARATORS [ optionName ] && ( optionName in currentRaw ) && COMPLEX_OPTION_COMPARATORS [ optionName ] ( currentRaw [ optionName ] , raw [ optionName ] ) ) ) { refined [ optionName ] = currentRefined [ optionName ] ; } else if ( refiners [ optionName ] ) { refined [ optionName ] = refiners [ optionName ] ( raw [ optionName ] ) ; anyChanges = true ; } else { extra [ optionName ] = currentRaw [ optionName ] ; } } } if ( anyChanges ) { this . currentCalendarOptionsInput = raw ; this . currentCalendarOptionsRefined = refined as CalendarOptionsRefined ; } return { rawOptions : this . currentCalendarOptionsInput , refinedOptions : this . currentCalendarOptionsRefined , pluginHooks , availableLocaleData , localeDefaults , extra , } ; } _computeCurrentViewData ( viewType , optionsData , optionOverrides , dynamicOptionOverrides ) { let viewSpec = optionsData . viewSpecs [ viewType ] ; if ( ! viewSpec ) { throw new Error ( `viewType "${ viewType }" is not available. Please make sure you've loaded all neccessary plugins` ) ; } let { refinedOptions , extra } = this . processRawViewOptions ( viewSpec , optionsData . pluginHooks , optionsData . localeDefaults , optionOverrides , dynamicOptionOverrides ) ; warnUnknownOptions ( extra ) ; let dateProfileGenerator = this . buildDateProfileGenerator ( { dateProfileGeneratorClass : viewSpec . optionDefaults . dateProfileGeneratorClass as any , duration : viewSpec . duration , durationUnit : viewSpec . durationUnit , usesMinMaxTime : viewSpec . optionDefaults . usesMinMaxTime as any , dateEnv : optionsData . dateEnv , calendarApi : this . props . calendarApi , slotMinTime : refinedOptions . slotMinTime , slotMaxTime : refinedOptions . slotMaxTime , showNonCurrentDates : refinedOptions . showNonCurrentDates , dayCount : refinedOptions . dayCount , dateAlignment : refinedOptions . dateAlignment , dateIncrement : refinedOptions . dateIncrement , hiddenDays : refinedOptions . hiddenDays , weekends : refinedOptions . weekends , nowInput : refinedOptions . now , validRangeInput : refinedOptions . validRange , visibleRangeInput : refinedOptions . visibleRange , monthMode : refinedOptions . monthMode , fixedWeekCount : refinedOptions . fixedWeekCount , } ) ; let viewApi = this . buildViewApi ( viewType , this . getCurrentData , optionsData . dateEnv ) ; return { viewSpec , options : refinedOptions , dateProfileGenerator , viewApi } ; } processRawViewOptions ( viewSpec , pluginHooks , localeDefaults , optionOverrides , dynamicOptionOverrides ) { let raw = mergeRawOptions ( [ BASE_OPTION_DEFAULTS , viewSpec . optionDefaults , localeDefaults , optionOverrides , viewSpec . optionOverrides , dynamicOptionOverrides , ] ) ; let refiners = { ... BASE_OPTION_REFINERS , ... CALENDAR_LISTENER_REFINERS , ... CALENDAR_OPTION_REFINERS , ... VIEW_OPTION_REFINERS , ... pluginHooks . listenerRefiners , ... pluginHooks . optionRefiners , } ; let refined = { } ; let currentRaw = this . currentViewOptionsInput ; let currentRefined = this . currentViewOptionsRefined ; let anyChanges = false ; let extra = { } ; for ( let optionName in raw ) { if ( raw [ optionName ] === currentRaw [ optionName ] ) { refined [ optionName ] = currentRefined [ optionName ] ; } else { if ( raw [ optionName ] === this . currentCalendarOptionsInput [ optionName ] ) { if ( optionName in this . currentCalendarOptionsRefined ) { refined [ optionName ] = this . currentCalendarOptionsRefined [ optionName ] ; } } else if ( refiners [ optionName ] ) { refined [ optionName ] = refiners [ optionName ] ( raw [ optionName ] ) ; } else { extra [ optionName ] = raw [ optionName ] ; } anyChanges = true ; } } if ( anyChanges ) { this . currentViewOptionsInput = raw ; this . currentViewOptionsRefined = refined as ViewOptionsRefined ; } return { rawOptions : this . currentViewOptionsInput , refinedOptions : this . currentViewOptionsRefined , extra , } ; } } function buildDateEnv ( timeZone , explicitLocale , weekNumberCalculation , firstDay , weekText , pluginHooks , availableLocaleData , defaultSeparator ) { let locale = buildLocale ( explicitLocale || availableLocaleData . defaultCode , availableLocaleData . map ) ; return new DateEnv ( { calendarSystem : 'gregory' , timeZone , namedTimeZoneImpl : pluginHooks . namedTimeZonedImpl , locale , weekNumberCalculation , firstDay , weekText , cmdFormatter : pluginHooks . cmdFormatter , defaultSeparator , } ) ; } function buildTheme ( options , pluginHooks ) { let ThemeClass = pluginHooks . themeClasses [ options . themeSystem ] || StandardTheme ; return new ThemeClass ( options ) ; } function buildDateProfileGenerator ( props ) { let DateProfileGeneratorClass = props . dateProfileGeneratorClass || DateProfileGenerator ; return new DateProfileGeneratorClass ( props ) ; } function buildViewApi ( type , getCurrentData , dateEnv ) { return new ViewApi ( type , getCurrentData , dateEnv ) ; } function buildEventUiBySource ( eventSources ) { return mapHash ( eventSources , ( eventSource ) => eventSource . ui ) ; } function buildEventUiBases ( eventDefs , eventUiSingleBase , eventUiBySource ) { let eventUiBases = { '' : eventUiSingleBase } ; for ( let defId in eventDefs ) { let def = eventDefs [ defId ] ; if ( def . sourceId && eventUiBySource [ def . sourceId ] ) { eventUiBases [ defId ] = eventUiBySource [ def . sourceId ] ; } } return eventUiBases ; } function buildViewUiProps ( calendarContext ) { let { options } = calendarContext ; return { eventUiSingleBase : createEventUi ( { display : options . eventDisplay , editable : options . editable , startEditable : options . eventStartEditable , durationEditable : options . eventDurationEditable , constraint : options . eventConstraint , overlap : typeof options . eventOverlap === 'boolean' ? options . eventOverlap : undefined , allow : options . eventAllow , backgroundColor : options . eventBackgroundColor , borderColor : options . eventBorderColor , textColor : options . eventTextColor , color : options . eventColor , } , calendarContext ) , selectionConfig : createEventUi ( { constraint : options . selectConstraint , overlap : typeof options . selectOverlap === 'boolean' ? options . selectOverlap : undefined , allow : options . selectAllow , } , calendarContext ) , } ; } function computeIsLoading ( state , context ) { for ( let isLoadingFunc of context . pluginHooks . isLoadingFuncs ) { if ( isLoadingFunc ( state ) ) { return true ; } } return false ; } function parseContextBusinessHours ( calendarContext ) { return parseBusinessHours ( calendarContext . options . businessHours , calendarContext ) ; } function warnUnknownOptions ( options , viewName ? ) { for ( let optionName in options ) { console . warn ( `Unknown option '${ optionName }'` + ( viewName ? ` for view '${ viewName }'` : '' ) ) ; } }