Ver Fonte

new options/context system

Adam Shaw há 5 anos atrás
pai
commit
ea5a295cc1
66 ficheiros alterados com 972 adições e 964 exclusões
  1. 1 1
      packages-premium
  2. 1 1
      packages/__tests__/src/datelib/main.js
  3. 132 375
      packages/core/src/Calendar.tsx
  4. 101 45
      packages/core/src/CalendarComponent.tsx
  5. 9 10
      packages/core/src/DateProfileGenerator.ts
  6. 24 52
      packages/core/src/OptionsManager.ts
  7. 3 2
      packages/core/src/Toolbar.tsx
  8. 2 6
      packages/core/src/View.ts
  9. 6 6
      packages/core/src/api/EventApi.ts
  10. 2 8
      packages/core/src/common/DayCellRoot.tsx
  11. 0 3
      packages/core/src/common/DayHeader.tsx
  12. 1 1
      packages/core/src/common/EventRoot.tsx
  13. 1 1
      packages/core/src/common/NowIndicatorRoot.tsx
  14. 3 5
      packages/core/src/common/TableDateCell.tsx
  15. 1 1
      packages/core/src/common/ViewRoot.tsx
  16. 46 25
      packages/core/src/component/ComponentContext.ts
  17. 1 1
      packages/core/src/event-sources/func-event-source.ts
  18. 1 1
      packages/core/src/event-sources/json-feed-event-source.ts
  19. 2 2
      packages/core/src/interactions/EventClicking.ts
  20. 2 2
      packages/core/src/interactions/EventHovering.ts
  21. 2 8
      packages/core/src/option-change-handlers.ts
  22. 46 44
      packages/core/src/plugin-system.ts
  23. 174 0
      packages/core/src/reducers/CalendarStateReducer.ts
  24. 58 0
      packages/core/src/reducers/current-date.ts
  25. 58 0
      packages/core/src/reducers/date-profile.ts
  26. 17 0
      packages/core/src/reducers/date-selection.ts
  27. 23 0
      packages/core/src/reducers/event-drag.ts
  28. 23 0
      packages/core/src/reducers/event-resize.ts
  29. 67 20
      packages/core/src/reducers/eventSources.ts
  30. 9 3
      packages/core/src/reducers/eventStore.ts
  31. 0 204
      packages/core/src/reducers/main.ts
  32. 16 0
      packages/core/src/reducers/selected-event.ts
  33. 19 4
      packages/core/src/reducers/types.ts
  34. 17 0
      packages/core/src/reducers/view-type.ts
  35. 2 2
      packages/core/src/structs/event-mutation.ts
  36. 7 4
      packages/core/src/structs/event-source.ts
  37. 2 2
      packages/core/src/structs/event-store.ts
  38. 6 6
      packages/core/src/structs/event.ts
  39. 1 1
      packages/core/src/structs/view-config.tsx
  40. 20 20
      packages/core/src/structs/view-spec.ts
  41. 5 5
      packages/core/src/toolbar-parse.ts
  42. 1 1
      packages/core/src/validation.ts
  43. 2 5
      packages/daygrid/src/DayTable.tsx
  44. 2 5
      packages/daygrid/src/DayTableView.tsx
  45. 3 5
      packages/daygrid/src/Table.tsx
  46. 1 7
      packages/daygrid/src/TableCell.tsx
  47. 0 1
      packages/daygrid/src/TableRow.tsx
  48. 2 2
      packages/daygrid/src/TableView.tsx
  49. 1 1
      packages/google-calendar/src/main.ts
  50. 4 4
      packages/interaction/src/interactions-external/ExternalElementDragging.ts
  51. 2 2
      packages/interaction/src/interactions/DateClicking.ts
  52. 6 6
      packages/interaction/src/interactions/EventDragging.ts
  53. 5 5
      packages/interaction/src/interactions/EventResizing.ts
  54. 2 2
      packages/interaction/src/interactions/HitDragging.ts
  55. 4 4
      packages/list/src/ListView.tsx
  56. 1 1
      packages/list/src/ListViewEventRow.tsx
  57. 1 1
      packages/list/src/ListViewHeaderRow.tsx
  58. 3 3
      packages/luxon/src/main.ts
  59. 2 2
      packages/moment/src/main.ts
  60. 2 4
      packages/timegrid/src/DayTimeCols.tsx
  61. 1 5
      packages/timegrid/src/DayTimeColsView.tsx
  62. 2 3
      packages/timegrid/src/TimeCol.tsx
  63. 5 10
      packages/timegrid/src/TimeCols.tsx
  64. 0 3
      packages/timegrid/src/TimeColsContent.tsx
  65. 4 6
      packages/timegrid/src/TimeColsSlats.tsx
  66. 5 5
      packages/timegrid/src/TimeColsView.tsx

+ 1 - 1
packages-premium

@@ -1 +1 @@
-Subproject commit 9d97dbece366decbe3e715025586fd162a248df5
+Subproject commit ea4c0aad49411a6779d9941b9fb9284130c5d32e

+ 1 - 1
packages/__tests__/src/datelib/main.js

@@ -9,7 +9,7 @@ describe('datelib', function() {
   beforeEach(function() {
   beforeEach(function() {
     enLocale = new Calendar(document.createElement('div'), { // HACK
     enLocale = new Calendar(document.createElement('div'), { // HACK
       plugins: [ dayGridPlugin ]
       plugins: [ dayGridPlugin ]
-    }).dateEnv.locale
+    }).state.dateEnv.locale
   })
   })
 
 
   describe('computeWeekNumber', function() {
   describe('computeWeekNumber', function() {

+ 132 - 375
packages/core/src/Calendar.tsx

@@ -1,17 +1,13 @@
 import { EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 import { EmitterMixin, EmitterInterface } from './common/EmitterMixin'
-import { OptionsManager } from './OptionsManager'
 import { OptionsInput } from './types/input-types'
 import { OptionsInput } from './types/input-types'
-import { buildLocale, organizeRawLocales, RawLocaleMap } from './datelib/locale'
-import { DateEnv, DateInput } from './datelib/env'
-import { DateMarker, startOfDay, diffWholeDays } from './datelib/marker'
+import { DateInput } from './datelib/env'
+import { DateMarker, startOfDay } from './datelib/marker'
 import { createFormatter } from './datelib/formatting'
 import { createFormatter } from './datelib/formatting'
 import { createDuration, DurationInput, Duration } from './datelib/duration'
 import { createDuration, DurationInput, Duration } from './datelib/duration'
-import { reduce } from './reducers/main'
 import { parseDateSpan, DateSpanInput, DateSpan, buildDateSpanApi, DateSpanApi, buildDatePointApi, DatePointApi } from './structs/date-span'
 import { parseDateSpan, DateSpanInput, DateSpan, buildDateSpanApi, DateSpanApi, buildDatePointApi, DatePointApi } from './structs/date-span'
 import { memoize } from './util/memoize'
 import { memoize } from './util/memoize'
 import { mapHash, isPropsEqual } from './util/object'
 import { mapHash, isPropsEqual } from './util/object'
-import { DateRangeInput, DateRange } from './datelib/date-range'
-import { DateProfileGenerator, DateProfile } from './DateProfileGenerator'
+import { DateRangeInput } from './datelib/date-range'
 import { EventSourceInput, parseEventSource, EventSourceHash } from './structs/event-source'
 import { EventSourceInput, parseEventSource, EventSourceHash } from './structs/event-source'
 import { EventInput, parseEvent, EventDefHash } from './structs/event'
 import { EventInput, parseEvent, EventDefHash } from './structs/event'
 import { CalendarState, Action } from './reducers/types'
 import { CalendarState, Action } from './reducers/types'
@@ -19,8 +15,7 @@ import { EventSourceApi } from './api/EventSourceApi'
 import { EventApi } from './api/EventApi'
 import { EventApi } from './api/EventApi'
 import { createEmptyEventStore, eventTupleToStore, EventStore } from './structs/event-store'
 import { createEmptyEventStore, eventTupleToStore, EventStore } from './structs/event-store'
 import { processScopedUiProps, EventUiHash, EventUi } from './component/event-ui'
 import { processScopedUiProps, EventUiHash, EventUi } from './component/event-ui'
-import { buildViewSpecs, ViewSpecHash, ViewSpec } from './structs/view-spec'
-import { PluginSystem, PluginHooks, PluginDef } from './plugin-system'
+import { ViewSpec } from './structs/view-spec'
 import { CalendarComponent } from './CalendarComponent'
 import { CalendarComponent } from './CalendarComponent'
 import { __assign } from 'tslib'
 import { __assign } from 'tslib'
 import { DateComponent } from './component/DateComponent'
 import { DateComponent } from './component/DateComponent'
@@ -28,16 +23,16 @@ import { PointerDragEvent } from './interactions/pointer'
 import { InteractionSettingsInput, parseInteractionSettings, Interaction, interactionSettingsStore, InteractionClass } from './interactions/interaction'
 import { InteractionSettingsInput, parseInteractionSettings, Interaction, interactionSettingsStore, InteractionClass } from './interactions/interaction'
 import { EventClicking } from './interactions/EventClicking'
 import { EventClicking } from './interactions/EventClicking'
 import { EventHovering } from './interactions/EventHovering'
 import { EventHovering } from './interactions/EventHovering'
-import { StandardTheme } from './theme/StandardTheme'
-import { ComponentContext, ComponentContextType, buildContext } from './component/ComponentContext'
 import { render, h, createRef, flushToDom } from './vdom'
 import { render, h, createRef, flushToDom } from './vdom'
 import { TaskRunner, DelayedRunner } from './util/runner'
 import { TaskRunner, DelayedRunner } from './util/runner'
 import { ViewApi } from './ViewApi'
 import { ViewApi } from './ViewApi'
-import { globalPlugins } from './global-plugins'
 import { removeExact } from './util/array'
 import { removeExact } from './util/array'
 import { guid } from './util/misc'
 import { guid } from './util/misc'
 import { CssDimValue } from './scrollgrid/util'
 import { CssDimValue } from './scrollgrid/util'
 import { applyStyleProp } from './util/dom-manip'
 import { applyStyleProp } from './util/dom-manip'
+import { CalendarStateReducer } from './reducers/CalendarStateReducer'
+import { parseToolbars } from './toolbar-parse'
+import { getNow } from './reducers/current-date'
 
 
 
 
 export interface DateClickApi extends DatePointApi {
 export interface DateClickApi extends DatePointApi {
@@ -77,96 +72,68 @@ export class Calendar {
   triggerWith: EmitterInterface['triggerWith']
   triggerWith: EmitterInterface['triggerWith']
   hasHandlers: EmitterInterface['hasHandlers']
   hasHandlers: EmitterInterface['hasHandlers']
 
 
-  // option-processing internals
-  // TODO: make these all private
-  public pluginSystem: PluginSystem
-  public optionsManager: OptionsManager
-  public viewSpecs: ViewSpecHash
-  public dateProfileGenerators: { [viewName: string]: DateProfileGenerator }
-
   // derived state
   // derived state
   // TODO: make these all private
   // TODO: make these all private
-  private organizeRawLocales = memoize(organizeRawLocales)
-  private buildDateEnv = memoize(buildDateEnv)
-  private computeTitle = memoize(computeTitle)
-  private buildTheme = memoize(buildTheme)
-  private buildContext = memoize(buildContext)
   private buildEventUiSingleBase = memoize(buildEventUiSingleBase)
   private buildEventUiSingleBase = memoize(buildEventUiSingleBase)
-  private buildSelectionConfig = memoize(buildSelectionConfig)
   private buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual)
   private buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual)
   private buildEventUiBases = memoize(buildEventUiBases)
   private buildEventUiBases = memoize(buildEventUiBases)
   private renderableEventStore: EventStore
   private renderableEventStore: EventStore
   public eventUiBases: EventUiHash // needed for validation system
   public eventUiBases: EventUiHash // needed for validation system
   public selectionConfig: EventUi // needed for validation system. doesn't need all the info EventUi provides. only validation-related
   public selectionConfig: EventUi // needed for validation system. doesn't need all the info EventUi provides. only validation-related
-  private availableRawLocales: RawLocaleMap
-  public context: ComponentContext
-  public dateEnv: DateEnv
   public defaultAllDayEventDuration: Duration
   public defaultAllDayEventDuration: Duration
   public defaultTimedEventDuration: Duration
   public defaultTimedEventDuration: Duration
   public slotMinTime: Duration
   public slotMinTime: Duration
   public slotMaxTime: Duration
   public slotMaxTime: Duration
   private resizeHandlers: ResizeHandler[] = []
   private resizeHandlers: ResizeHandler[] = []
+  private toolbarConfig
 
 
   // interaction
   // interaction
   calendarInteractions: CalendarInteraction[]
   calendarInteractions: CalendarInteraction[]
   interactionsStore: { [componentUid: string]: Interaction[] } = {}
   interactionsStore: { [componentUid: string]: Interaction[] } = {}
 
 
-  state: CalendarState
+  state: CalendarState = {} as any
   isRendering = false
   isRendering = false
   isRendered = false
   isRendered = false
+  reducer: CalendarStateReducer
   renderRunner: DelayedRunner
   renderRunner: DelayedRunner
-  actionRunner: TaskRunner<Action> // for reducer. bad name
+  actionRunner: TaskRunner<Action> // guards against nested action calls
   el: HTMLElement
   el: HTMLElement
   currentClassNames: string[] = []
   currentClassNames: string[] = []
   componentRef = createRef<CalendarComponent>()
   componentRef = createRef<CalendarComponent>()
   view: ViewApi // public API
   view: ViewApi // public API
 
 
-  get component() { return this.componentRef.current }
+  get component() { return this.componentRef.current } // used to get view-specific business hours :(
 
 
 
 
-  constructor(el: HTMLElement, overrides?: OptionsInput) {
+  constructor(el: HTMLElement, optionOverrides: OptionsInput = {}) {
     this.el = el
     this.el = el
 
 
-    let optionsManager = this.optionsManager = new OptionsManager(overrides || {})
-    this.pluginSystem = new PluginSystem()
+    this.reducer = new CalendarStateReducer()
 
 
     let renderRunner = this.renderRunner = new DelayedRunner(
     let renderRunner = this.renderRunner = new DelayedRunner(
       this.updateComponent.bind(this)
       this.updateComponent.bind(this)
     )
     )
 
 
-    let actionRunner = this.actionRunner = new TaskRunner(
+    this.actionRunner = new TaskRunner( // do we really need this in a runner?
       this.runAction.bind(this),
       this.runAction.bind(this),
       () => {
       () => {
-        this.updateDerivedState()
-        renderRunner.request(optionsManager.computed.rerenderDelay)
+        renderRunner.request(this.state.options.rerenderDelay)
       }
       }
     )
     )
-    actionRunner.pause()
-
-    // only do once. don't do in onOptionsChange. because can't remove plugins
-    this.addPluginDefs(
-      globalPlugins.concat(optionsManager.computed.plugins || [])
-    )
-
-    this.onOptionsChange()
 
 
-    this.publiclyTrigger('_init') // for tests
-    this.hydrate()
-    actionRunner.resume()
+    this.dispatch({
+      type: 'INIT',
+      optionOverrides
+    })
 
 
-    this.calendarInteractions = this.pluginSystem.hooks.calendarInteractions
+    // must go after INIT
+    this.calendarInteractions = this.state.pluginHooks.calendarInteractions
       .map((calendarInteractionClass) => {
       .map((calendarInteractionClass) => {
         return new calendarInteractionClass(this)
         return new calendarInteractionClass(this)
       })
       })
   }
   }
 
 
 
 
-  addPluginDefs(pluginDefs: PluginDef[]) {
-    for (let pluginDef of pluginDefs) {
-      this.pluginSystem.add(pluginDef)
-    }
-  }
-
 
 
   // Public API for rendering
   // Public API for rendering
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
@@ -199,50 +166,6 @@ export class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
 
 
 
 
-  hydrate() {
-    this.state = this.buildInitialState()
-
-    let rawSources = this.opt('eventSources') || []
-    let singleRawSource = this.opt('events')
-    let sources = [] // parsed
-
-    if (singleRawSource) {
-      rawSources.unshift(singleRawSource)
-    }
-
-    for (let rawSource of rawSources) {
-      let source = parseEventSource(rawSource, this)
-      if (source) {
-        sources.push(source)
-      }
-    }
-
-    this.dispatch({ type: 'INIT' }) // pass in sources here?
-    this.dispatch({ type: 'ADD_EVENT_SOURCES', sources })
-    this.dispatch({
-      type: 'SET_VIEW_TYPE',
-      viewType: this.opt('initialView') || this.pluginSystem.hooks.initialView
-    })
-  }
-
-
-  buildInitialState(): CalendarState {
-    return {
-      viewType: null,
-      loadingLevel: 0,
-      eventSourceLoadingLevel: 0,
-      currentDate: this.getInitialDate(),
-      dateProfile: null,
-      eventSources: {},
-      eventStore: createEmptyEventStore(),
-      dateSelection: null,
-      eventSelection: '',
-      eventDrag: null,
-      eventResize: null
-    }
-  }
-
-
   dispatch(action: Action) {
   dispatch(action: Action) {
     this.actionRunner.request(action)
     this.actionRunner.request(action)
 
 
@@ -257,11 +180,16 @@ export class Calendar {
 
 
   runAction(action: Action) {
   runAction(action: Action) {
     let oldState = this.state
     let oldState = this.state
-    let newState = this.state = reduce(this.state, action, this)
+    let newState = this.state = this.reducer.reduce(this.state, action, this)
 
 
-    if (!oldState.loadingLevel && newState.loadingLevel) {
+    if (oldState && oldState.options !== newState.options) {
+      this.updateDerivedOptions(newState.options)
+    }
+
+    if ((!oldState || !oldState.loadingLevel) && newState.loadingLevel) {
       this.publiclyTrigger('loading', [ true ])
       this.publiclyTrigger('loading', [ true ])
-    } else if (oldState.loadingLevel && !newState.loadingLevel) {
+
+    } else if ((oldState && oldState.loadingLevel) && !newState.loadingLevel) {
       this.publiclyTrigger('loading', [ false ])
       this.publiclyTrigger('loading', [ false ])
     }
     }
   }
   }
@@ -302,10 +230,9 @@ export class Calendar {
 
 
 
 
   renderComponent() {
   renderComponent() {
-    let { context, state } = this
+    let { state } = this
     let { viewType } = state
     let { viewType } = state
-    let viewSpec = this.viewSpecs[viewType]
-    let viewApi = context.view
+    let viewSpec = state.viewSpecs[viewType]
 
 
     // if event sources are still loading and progressive rendering hasn't been enabled,
     // if event sources are still loading and progressive rendering hasn't been enabled,
     // keep rendering the last fully loaded set of events
     // keep rendering the last fully loaded set of events
@@ -319,24 +246,23 @@ export class Calendar {
     let eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource)
     let eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource)
 
 
     render(
     render(
-      <ComponentContextType.Provider value={context}>
-        <CalendarComponent
-          ref={this.componentRef}
-          { ...state }
-          viewSpec={viewSpec}
-          dateProfileGenerator={this.dateProfileGenerators[viewType]}
-          dateProfile={state.dateProfile}
-          eventStore={renderableEventStore}
-          eventUiBases={eventUiBases}
-          dateSelection={state.dateSelection}
-          eventSelection={state.eventSelection}
-          eventDrag={state.eventDrag}
-          eventResize={state.eventResize}
-          title={viewApi.title}
-          onClassNameChange={this.handleClassNames}
-          onHeightChange={this.handleHeightChange}
-          />
-      </ComponentContextType.Provider>,
+      <CalendarComponent
+        ref={this.componentRef}
+        { ...state }
+        viewSpec={viewSpec}
+        dateProfileGenerator={state.dateProfileGenerator}
+        dateProfile={state.dateProfile}
+        eventStore={renderableEventStore}
+        eventUiBases={eventUiBases}
+        dateSelection={state.dateSelection}
+        eventSelection={state.eventSelection}
+        eventDrag={state.eventDrag}
+        eventResize={state.eventResize}
+        onClassNameChange={this.handleClassNames}
+        onHeightChange={this.handleHeightChange}
+        toolbarConfig={this.toolbarConfig}
+        calendar={this}
+      />,
       this.el
       this.el
     )
     )
     flushToDom()
     flushToDom()
@@ -379,34 +305,36 @@ export class Calendar {
 
 
 
 
   setOption(name: string, val) {
   setOption(name: string, val) {
-    this.mutateOptions({ [name]: val }, [], true)
+    this.dispatch({
+      type: 'SET_OPTION',
+      optionName: name,
+      optionValue: val
+    })
   }
   }
 
 
 
 
   getOption(name: string) { // getter, used externally
   getOption(name: string) { // getter, used externally
-    return this.optionsManager.computed[name]
+    return this.state.options[name]
   }
   }
 
 
 
 
   opt(name: string) { // getter, used internally
   opt(name: string) { // getter, used internally
-    return this.optionsManager.computed[name]
+    return this.state.options[name]
   }
   }
 
 
 
 
   viewOpt(name: string) { // getter, used internally
   viewOpt(name: string) { // getter, used internally
-    return this.viewSpecs[this.state.viewType].options[name]
+    return this.state.viewSpecs[this.state.viewType].options[name]
   }
   }
 
 
+
   /*
   /*
   handles option changes (like a diff)
   handles option changes (like a diff)
   */
   */
-  mutateOptions(updates, removals: string[] = [], isDynamic?: boolean) {
-    let changeHandlers = this.pluginSystem.hooks.optionChangeHandlers
+  mutateOptions(updates, removals: string[] = [], isDynamic = false) {
+    let changeHandlers = this.state.pluginHooks.optionChangeHandlers
     let normalUpdates = {}
     let normalUpdates = {}
     let specialUpdates = {}
     let specialUpdates = {}
-    let oldDateEnv = this.dateEnv // do this before onOptionsChange
-    let isTimeZoneDirty = false
-    let anyDifficultOptions = Boolean(removals.length) // pretty much all options are "difficult" :(
 
 
     for (let name in updates) {
     for (let name in updates) {
       if (changeHandlers[name]) {
       if (changeHandlers[name]) {
@@ -416,135 +344,42 @@ export class Calendar {
       }
       }
     }
     }
 
 
-    for (let name in normalUpdates) {
-      if (/^(initialDate|initialView)$/.test(name)) {
-        // can't change date this way. use gotoDate instead
-      } else {
-        anyDifficultOptions = true // I guess all options are "difficult" ?
-
-        if (name === 'timeZone') {
-          isTimeZoneDirty = true
-        }
-      }
-    }
-
-    this.optionsManager.mutate(normalUpdates, removals, isDynamic)
-
-    if (anyDifficultOptions) {
-      this.onOptionsChange()
-    }
-
     this.batchRendering(() => {
     this.batchRendering(() => {
 
 
-      if (anyDifficultOptions) {
-
-        if (isTimeZoneDirty) {
-          this.dispatch({
-            type: 'CHANGE_TIMEZONE',
-            oldDateEnv
-          })
-        }
-
-        /* HACK
-        has the same effect as calling this.updateComponent()
-        but recomputes the state's dateProfile
-        */
-        this.dispatch({
-          type: 'SET_VIEW_TYPE',
-          viewType: this.state.viewType
-        })
-
-      }
+      this.dispatch({
+        type: 'MUTATE_OPTIONS',
+        updates: normalUpdates,
+        removals,
+        isDynamic
+      })
 
 
       // special updates
       // special updates
       for (let name in specialUpdates) {
       for (let name in specialUpdates) {
         changeHandlers[name](specialUpdates[name], this)
         changeHandlers[name](specialUpdates[name], this)
       }
       }
-
     })
     })
   }
   }
 
 
 
 
   /*
   /*
-  rebuilds things based off of a complete set of refined options
-  TODO: move all this to updateDerivedState, but hard because reducer depends on some values
+  only called when we know options changed
   */
   */
-  onOptionsChange() {
-    let pluginHooks = this.pluginSystem.hooks
-    let rawOptions = this.optionsManager.computed
-
-    let availableLocaleData = this.organizeRawLocales(rawOptions.locales)
-    let dateEnv = this.buildDateEnv(rawOptions, pluginHooks, availableLocaleData)
-
-    this.availableRawLocales = availableLocaleData.map
-    this.dateEnv = dateEnv
-
-    // TODO: don't do every time
-    this.viewSpecs = buildViewSpecs(pluginHooks.views, this.optionsManager)
-
-    // needs to happen before dateProfileGenerators
+  updateDerivedOptions(rawOptions) {
     this.slotMinTime = createDuration(rawOptions.slotMinTime)
     this.slotMinTime = createDuration(rawOptions.slotMinTime)
     this.slotMaxTime = createDuration(rawOptions.slotMaxTime)
     this.slotMaxTime = createDuration(rawOptions.slotMaxTime)
 
 
-    // needs to happen after dateEnv assigned :( because DateProfileGenerator grabs onto reference
-    // TODO: don't do every time
-    this.dateProfileGenerators = mapHash(this.viewSpecs, (viewSpec) => {
-      let dateProfileGeneratorClass = viewSpec.options.dateProfileGeneratorClass || DateProfileGenerator
-      return new dateProfileGeneratorClass(viewSpec, this)
-    })
-
     // TODO: don't do every time
     // TODO: don't do every time
     this.defaultAllDayEventDuration = createDuration(rawOptions.defaultAllDayEventDuration)
     this.defaultAllDayEventDuration = createDuration(rawOptions.defaultAllDayEventDuration)
     this.defaultTimedEventDuration = createDuration(rawOptions.defaultTimedEventDuration)
     this.defaultTimedEventDuration = createDuration(rawOptions.defaultTimedEventDuration)
-  }
-
-
-  /*
-  always executes after onOptionsChange
-  */
-  updateDerivedState() {
-    let pluginHooks = this.pluginSystem.hooks
-    let rawOptions = this.optionsManager.computed
-    let { dateEnv } = this
-    let { viewType, dateProfile } = this.state
-    let viewSpec = this.viewSpecs[viewType]
-
-    if (!viewSpec) {
-      throw new Error(`View type "${viewType}" is not valid`)
-    }
-
-    let theme = this.buildTheme(rawOptions, pluginHooks)
-    let title = this.computeTitle(dateProfile, dateEnv, viewSpec.options)
-    let viewApi = this.buildViewApi(viewType, title, dateProfile, dateEnv)
-    let context = this.buildContext(this, pluginHooks, dateEnv, theme, viewApi, rawOptions)
-
-    this.context = context
-    this.selectionConfig = this.buildSelectionConfig(rawOptions) // MUST happen after dateEnv assigned :(
-  }
 
 
+    this.selectionConfig = buildSelectionConfig(rawOptions, this) // relies on dateEnv
 
 
-  /*
-  will only create a new instance when viewType is changed
-  */
-  buildViewApi(viewType: string, title: string, dateProfile: DateProfile, dateEnv: DateEnv) {
-    let { view } = this
-
-    if (!view || view.type !== viewType) {
-      view = this.view = { type: viewType } as ViewApi
-    }
-
-    view.title = title
-    view.activeStart = dateEnv.toDate(dateProfile.activeRange.start)
-    view.activeEnd = dateEnv.toDate(dateProfile.activeRange.end)
-    view.currentStart = dateEnv.toDate(dateProfile.currentRange.start)
-    view.currentEnd = dateEnv.toDate(dateProfile.currentRange.end)
-
-    return view
+    this.toolbarConfig = parseToolbars(rawOptions, this.state.theme, rawOptions.direction === 'rtl', this)
   }
   }
 
 
 
 
   getAvailableLocaleCodes() {
   getAvailableLocaleCodes() {
-    return Object.keys(this.availableRawLocales)
+    return Object.keys(this.state.availableRawLocales)
   }
   }
 
 
 
 
@@ -575,27 +410,41 @@ export class Calendar {
 
 
   // Returns a boolean about whether the view is okay to instantiate at some point
   // Returns a boolean about whether the view is okay to instantiate at some point
   isValidViewType(viewType: string): boolean {
   isValidViewType(viewType: string): boolean {
-    return Boolean(this.viewSpecs[viewType])
+    return Boolean(this.state.viewSpecs[viewType])
   }
   }
 
 
 
 
   changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput) {
   changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput) {
-    let dateMarker = null
-
-    if (dateOrRange) {
-      if ((dateOrRange as DateRangeInput).start && (dateOrRange as DateRangeInput).end) { // a range
-        this.optionsManager.mutate({ visibleRange: dateOrRange }, []) // will not rerender
-        this.onOptionsChange() // ...but yuck
-      } else { // a date
-        dateMarker = this.dateEnv.createMarker(dateOrRange as DateInput) // just like gotoDate
-      }
-    }
+    this.batchRendering(() => {
+      this.unselect()
 
 
-    this.unselect()
-    this.dispatch({
-      type: 'SET_VIEW_TYPE',
-      viewType,
-      dateMarker
+      if (dateOrRange) {
+
+        if ((dateOrRange as DateRangeInput).start && (dateOrRange as DateRangeInput).end) { // a range
+          this.dispatch({
+            type: 'SET_VIEW_TYPE',
+            viewType,
+          })
+          this.dispatch({
+            type: 'SET_OPTION',
+            optionName: 'visibleRange',
+            optionValue: dateOrRange
+          })
+
+        } else {
+          this.dispatch({
+            type: 'SET_VIEW_TYPE',
+            viewType,
+            dateMarker: this.state.dateEnv.createMarker(dateOrRange as DateInput)
+          })
+        }
+
+      } else {
+        this.dispatch({
+          type: 'SET_VIEW_TYPE',
+          viewType
+        })
+      }
     })
     })
   }
   }
 
 
@@ -607,7 +456,7 @@ export class Calendar {
     let spec
     let spec
 
 
     viewType = viewType || 'day' // day is default zoom
     viewType = viewType || 'day' // day is default zoom
-    spec = this.viewSpecs[viewType] || this.getUnitViewSpec(viewType)
+    spec = this.state.viewSpecs[viewType] || this.getUnitViewSpec(viewType)
 
 
     this.unselect()
     this.unselect()
 
 
@@ -617,6 +466,7 @@ export class Calendar {
         viewType: spec.type,
         viewType: spec.type,
         dateMarker
         dateMarker
       })
       })
+
     } else {
     } else {
       this.dispatch({
       this.dispatch({
         type: 'SET_DATE',
         type: 'SET_DATE',
@@ -629,16 +479,17 @@ export class Calendar {
   // Given a duration singular unit, like "week" or "day", finds a matching view spec.
   // Given a duration singular unit, like "week" or "day", finds a matching view spec.
   // Preference is given to views that have corresponding buttons.
   // Preference is given to views that have corresponding buttons.
   getUnitViewSpec(unit: string): ViewSpec | null {
   getUnitViewSpec(unit: string): ViewSpec | null {
-    let viewTypes = [].concat(this.context.viewsWithButtons)
+    let { viewSpecs } = this.state
+    let viewTypes = [].concat(this.toolbarConfig.viewsWithButtons)
     let i
     let i
     let spec
     let spec
 
 
-    for (let viewType in this.viewSpecs) {
+    for (let viewType in viewSpecs) {
       viewTypes.push(viewType)
       viewTypes.push(viewType)
     }
     }
 
 
     for (i = 0; i < viewTypes.length; i++) {
     for (i = 0; i < viewTypes.length; i++) {
-      spec = this.viewSpecs[viewTypes[i]]
+      spec = viewSpecs[viewTypes[i]]
       if (spec) {
       if (spec) {
         if (spec.singleUnit === unit) {
         if (spec.singleUnit === unit) {
           return spec
           return spec
@@ -652,18 +503,6 @@ export class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
 
 
 
 
-  getInitialDate() {
-    let initialDateInput = this.opt('initialDate')
-
-    // compute the initial ambig-timezone date
-    if (initialDateInput != null) {
-      return this.dateEnv.createMarker(initialDateInput)
-    } else {
-      return this.getNow() // getNow already returns unzoned
-    }
-  }
-
-
   prev() {
   prev() {
     this.unselect()
     this.unselect()
     this.dispatch({ type: 'PREV' })
     this.dispatch({ type: 'PREV' })
@@ -680,7 +519,7 @@ export class Calendar {
     this.unselect()
     this.unselect()
     this.dispatch({
     this.dispatch({
       type: 'SET_DATE',
       type: 'SET_DATE',
-      dateMarker: this.dateEnv.addYears(this.state.currentDate, -1)
+      dateMarker: this.state.dateEnv.addYears(this.state.currentDate, -1)
     })
     })
   }
   }
 
 
@@ -689,7 +528,7 @@ export class Calendar {
     this.unselect()
     this.unselect()
     this.dispatch({
     this.dispatch({
       type: 'SET_DATE',
       type: 'SET_DATE',
-      dateMarker: this.dateEnv.addYears(this.state.currentDate, 1)
+      dateMarker: this.state.dateEnv.addYears(this.state.currentDate, 1)
     })
     })
   }
   }
 
 
@@ -707,7 +546,7 @@ export class Calendar {
     this.unselect()
     this.unselect()
     this.dispatch({
     this.dispatch({
       type: 'SET_DATE',
       type: 'SET_DATE',
-      dateMarker: this.dateEnv.createMarker(zonedDateInput)
+      dateMarker: this.state.dateEnv.createMarker(zonedDateInput)
     })
     })
   }
   }
 
 
@@ -719,7 +558,7 @@ export class Calendar {
       this.unselect()
       this.unselect()
       this.dispatch({
       this.dispatch({
         type: 'SET_DATE',
         type: 'SET_DATE',
-        dateMarker: this.dateEnv.add(this.state.currentDate, delta)
+        dateMarker: this.state.dateEnv.add(this.state.currentDate, delta)
       })
       })
     }
     }
   }
   }
@@ -727,7 +566,7 @@ export class Calendar {
 
 
   // for external API
   // for external API
   getDate(): Date {
   getDate(): Date {
-    return this.dateEnv.toDate(this.state.currentDate)
+    return this.state.dateEnv.toDate(this.state.currentDate)
   }
   }
 
 
 
 
@@ -736,7 +575,8 @@ export class Calendar {
 
 
 
 
   formatDate(d: DateInput, formatter): string {
   formatDate(d: DateInput, formatter): string {
-    let { dateEnv } = this
+    let { dateEnv } = this.state
+
     return dateEnv.format(
     return dateEnv.format(
       dateEnv.createMarker(d),
       dateEnv.createMarker(d),
       createFormatter(formatter)
       createFormatter(formatter)
@@ -746,7 +586,8 @@ export class Calendar {
 
 
   // `settings` is for formatter AND isEndExclusive
   // `settings` is for formatter AND isEndExclusive
   formatRange(d0: DateInput, d1: DateInput, settings) {
   formatRange(d0: DateInput, d1: DateInput, settings) {
-    let { dateEnv } = this
+    let { dateEnv } = this.state
+
     return dateEnv.formatRange(
     return dateEnv.formatRange(
       dateEnv.createMarker(d0),
       dateEnv.createMarker(d0),
       dateEnv.createMarker(d1),
       dateEnv.createMarker(d1),
@@ -757,7 +598,8 @@ export class Calendar {
 
 
 
 
   formatIso(d: DateInput, omitTime?: boolean) {
   formatIso(d: DateInput, omitTime?: boolean) {
-    let { dateEnv } = this
+    let { dateEnv } = this.state
+
     return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime })
     return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime })
   }
   }
 
 
@@ -778,12 +620,12 @@ export class Calendar {
 
 
   resizeRunner = new DelayedRunner(() => {
   resizeRunner = new DelayedRunner(() => {
     this.triggerResizeHandlers(true) // should window resizes be considered "forced" ?
     this.triggerResizeHandlers(true) // should window resizes be considered "forced" ?
-    this.publiclyTrigger('windowResize', [ this.context.view ])
+    this.publiclyTrigger('windowResize')
   })
   })
 
 
 
 
   handleWindowResize = (ev: UIEvent) => {
   handleWindowResize = (ev: UIEvent) => {
-    let { options } = this.context
+    let { options } = this.state
 
 
     if (
     if (
       options.handleWindowResize &&
       options.handleWindowResize &&
@@ -822,7 +664,7 @@ export class Calendar {
       EventHovering
       EventHovering
     ]
     ]
     let interactionClasses: InteractionClass[] = DEFAULT_INTERACTIONS.concat(
     let interactionClasses: InteractionClass[] = DEFAULT_INTERACTIONS.concat(
-      this.pluginSystem.hooks.componentInteractions
+      this.state.pluginHooks.componentInteractions
     )
     )
     let interactions = interactionClasses.map((interactionClass) => {
     let interactions = interactionClasses.map((interactionClass) => {
       return new interactionClass(settings)
       return new interactionClass(settings)
@@ -871,7 +713,7 @@ export class Calendar {
 
 
     let selection = parseDateSpan(
     let selection = parseDateSpan(
       selectionInput,
       selectionInput,
-      this.dateEnv,
+      this.state.dateEnv,
       createDuration({ days: 1 }) // TODO: cache this?
       createDuration({ days: 1 }) // TODO: cache this?
     )
     )
 
 
@@ -927,11 +769,11 @@ export class Calendar {
   buildDatePointApi(dateSpan: DateSpan) {
   buildDatePointApi(dateSpan: DateSpan) {
     let props = {} as DatePointApi
     let props = {} as DatePointApi
 
 
-    for (let transform of this.pluginSystem.hooks.datePointTransforms) {
+    for (let transform of this.state.pluginHooks.datePointTransforms) {
       __assign(props, transform(dateSpan, this))
       __assign(props, transform(dateSpan, this))
     }
     }
 
 
-    __assign(props, buildDatePointApi(dateSpan, this.dateEnv))
+    __assign(props, buildDatePointApi(dateSpan, this.state.dateEnv))
 
 
     return props
     return props
   }
   }
@@ -940,11 +782,11 @@ export class Calendar {
   buildDateSpanApi(dateSpan: DateSpan) {
   buildDateSpanApi(dateSpan: DateSpan) {
     let props = {} as DateSpanApi
     let props = {} as DateSpanApi
 
 
-    for (let transform of this.pluginSystem.hooks.dateSpanTransforms) {
+    for (let transform of this.state.pluginHooks.dateSpanTransforms) {
       __assign(props, transform(dateSpan, this))
       __assign(props, transform(dateSpan, this))
     }
     }
 
 
-    __assign(props, buildDateSpanApi(dateSpan, this.dateEnv))
+    __assign(props, buildDateSpanApi(dateSpan, this.state.dateEnv))
 
 
     return props
     return props
   }
   }
@@ -956,17 +798,7 @@ export class Calendar {
 
 
   // Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option
   // Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option
   getNow(): DateMarker {
   getNow(): DateMarker {
-    let now = this.opt('now')
-
-    if (typeof now === 'function') {
-      now = now()
-    }
-
-    if (now == null) {
-      return this.dateEnv.createNowMarker()
-    }
-
-    return this.dateEnv.createMarker(now)
+    return getNow(this.state.options, this.state.dateEnv)
   }
   }
 
 
 
 
@@ -981,9 +813,9 @@ export class Calendar {
 
 
     if (allDay) {
     if (allDay) {
       end = startOfDay(end)
       end = startOfDay(end)
-      end = this.dateEnv.add(end, this.defaultAllDayEventDuration)
+      end = this.state.dateEnv.add(end, this.defaultAllDayEventDuration)
     } else {
     } else {
-      end = this.dateEnv.add(end, this.defaultTimedEventDuration)
+      end = this.state.dateEnv.add(end, this.defaultTimedEventDuration)
     }
     }
 
 
     return end
     return end
@@ -1141,7 +973,7 @@ export class Calendar {
       return sourceInput
       return sourceInput
     }
     }
 
 
-    let eventSource = parseEventSource(sourceInput, this)
+    let eventSource = parseEventSource(sourceInput, this.state.pluginHooks, this)
 
 
     if (eventSource) { // TODO: error otherwise?
     if (eventSource) { // TODO: error otherwise?
       this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [ eventSource ] })
       this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [ eventSource ] })
@@ -1183,31 +1015,8 @@ EmitterMixin.mixInto(Calendar)
 // -----------------------------------------------------------------------------------------------------------------
 // -----------------------------------------------------------------------------------------------------------------
 
 
 
 
-function buildDateEnv(rawOptions: any, pluginHooks: PluginHooks, availableLocaleData) {
-  let locale = buildLocale(rawOptions.locale || availableLocaleData.defaultCode, availableLocaleData.map)
-
-  return new DateEnv({
-    calendarSystem: 'gregory', // TODO: make this a setting
-    timeZone: rawOptions.timeZone,
-    namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl,
-    locale,
-    weekNumberCalculation: rawOptions.weekNumberCalculation,
-    firstDay: rawOptions.firstDay,
-    weekText: rawOptions.weekText,
-    cmdFormatter: pluginHooks.cmdFormatter
-  })
-}
-
-
-function buildTheme(rawOptions, pluginHooks: PluginHooks) {
-  let themeClass = pluginHooks.themeClasses[rawOptions.themeSystem] || StandardTheme
-
-  return new themeClass(rawOptions)
-}
-
-
-function buildSelectionConfig(this: Calendar, rawOptions) { // DANGEROUS: `this` context must be a Calendar
-  return processScopedUiProps('select', rawOptions, this)
+function buildSelectionConfig(rawOptions, calendar: Calendar) {
+  return processScopedUiProps('select', rawOptions, calendar)
 }
 }
 
 
 
 
@@ -1239,55 +1048,3 @@ function buildEventUiBases(eventDefs: EventDefHash, eventUiSingleBase: EventUi,
 
 
   return eventUiBases
   return eventUiBases
 }
 }
-
-
-// Title and Date Formatting
-// -----------------------------------------------------------------------------------------------------------------
-
-
-// Computes what the title at the top of the calendar should be for this view
-function computeTitle(dateProfile, dateEnv: DateEnv, viewOptions) {
-  let range: DateRange
-
-  // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
-  if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
-    range = dateProfile.currentRange
-  } else { // for day units or smaller, use the actual day range
-    range = dateProfile.activeRange
-  }
-
-  return dateEnv.formatRange(
-    range.start,
-    range.end,
-    createFormatter(
-      viewOptions.titleFormat || computeTitleFormat(dateProfile),
-      viewOptions.titleRangeSeparator
-    ),
-    { isEndExclusive: dateProfile.isRangeAllDay }
-  )
-}
-
-
-// Generates the format string that should be used to generate the title for the current date range.
-// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
-function computeTitleFormat(dateProfile) {
-  let currentRangeUnit = dateProfile.currentRangeUnit
-
-  if (currentRangeUnit === 'year') {
-    return { year: 'numeric' }
-  } else if (currentRangeUnit === 'month') {
-    return { year: 'numeric', month: 'long' } // like "September 2014"
-  } else {
-    let days = diffWholeDays(
-      dateProfile.currentRange.start,
-      dateProfile.currentRange.end
-    )
-    if (days !== null && days > 1) {
-      // multi-day range. shorter, like "Sep 9 - 10 2014"
-      return { year: 'numeric', month: 'short', day: 'numeric' }
-    } else {
-      // one day. longer, like "September 9 2014"
-      return { year: 'numeric', month: 'long', day: 'numeric' }
-    }
-  }
-}

+ 101 - 45
packages/core/src/CalendarComponent.tsx

@@ -1,18 +1,17 @@
-import { ComponentContext, ComponentContextType, buildContext } from './component/ComponentContext'
+import { ComponentContextType, buildViewContext } from './component/ComponentContext'
 import { ViewSpec } from './structs/view-spec'
 import { ViewSpec } from './structs/view-spec'
 import { ViewProps } from './View'
 import { ViewProps } from './View'
 import { Toolbar } from './Toolbar'
 import { Toolbar } from './Toolbar'
 import { DateProfileGenerator, DateProfile } from './DateProfileGenerator'
 import { DateProfileGenerator, DateProfile } from './DateProfileGenerator'
-import { rangeContainsMarker } from './datelib/date-range'
+import { rangeContainsMarker, DateRange } from './datelib/date-range'
 import { EventUiHash } from './component/event-ui'
 import { EventUiHash } from './component/event-ui'
 import { parseBusinessHours } from './structs/business-hours'
 import { parseBusinessHours } from './structs/business-hours'
 import { memoize } from './util/memoize'
 import { memoize } from './util/memoize'
-import { DateMarker } from './datelib/marker'
+import { DateMarker, diffWholeDays } from './datelib/marker'
 import { CalendarState } from './reducers/types'
 import { CalendarState } from './reducers/types'
 import { ViewPropsTransformerClass } from './plugin-system'
 import { ViewPropsTransformerClass } from './plugin-system'
 import { __assign } from 'tslib'
 import { __assign } from 'tslib'
-import { h, Fragment, createRef } from './vdom'
-import { BaseComponent } from './vdom-util'
+import { h, createRef, Component } from './vdom'
 import { buildDelegationHandler } from './util/dom-event'
 import { buildDelegationHandler } from './util/dom-event'
 import { capitaliseFirstLetter } from './util/misc'
 import { capitaliseFirstLetter } from './util/misc'
 import { ViewContainer } from './ViewContainer'
 import { ViewContainer } from './ViewContainer'
@@ -20,15 +19,19 @@ import { CssDimValue } from './scrollgrid/util'
 import { Theme } from './theme/Theme'
 import { Theme } from './theme/Theme'
 import { getCanVGrowWithinCell } from './util/table-styling'
 import { getCanVGrowWithinCell } from './util/table-styling'
 import { ViewComponent } from './structs/view-config'
 import { ViewComponent } from './structs/view-config'
+import { createFormatter } from './datelib/formatting'
+import { DateEnv } from './datelib/env'
+import { Calendar } from './Calendar'
 
 
 
 
 export interface CalendarComponentProps extends CalendarState {
 export interface CalendarComponentProps extends CalendarState {
   viewSpec: ViewSpec
   viewSpec: ViewSpec
   dateProfileGenerator: DateProfileGenerator // for the current view
   dateProfileGenerator: DateProfileGenerator // for the current view
   eventUiBases: EventUiHash
   eventUiBases: EventUiHash
-  title: string
   onClassNameChange?: (classNameHash) => void // will be fired with [] on cleanup
   onClassNameChange?: (classNameHash) => void // will be fired with [] on cleanup
   onHeightChange?: (height: CssDimValue) => void // will be fired with '' on cleanup
   onHeightChange?: (height: CssDimValue) => void // will be fired with '' on cleanup
+  toolbarConfig
+  calendar: Calendar
 }
 }
 
 
 interface CalendarComponentState {
 interface CalendarComponentState {
@@ -36,10 +39,13 @@ interface CalendarComponentState {
 }
 }
 
 
 
 
-export class CalendarComponent extends BaseComponent<CalendarComponentProps, CalendarComponentState> {
+export class CalendarComponent extends Component<CalendarComponentProps, CalendarComponentState> {
+
+  context: never
 
 
-  private buildViewContext = memoize(buildContext)
-  private parseBusinessHours = memoize((input) => parseBusinessHours(input, this.context.calendar))
+  private computeTitle = memoize(computeTitle)
+  private buildViewContext = memoize(buildViewContext)
+  private parseBusinessHours = memoize((input) => parseBusinessHours(input, this.props.calendar))
   private buildViewPropTransformers = memoize(buildViewPropTransformers)
   private buildViewPropTransformers = memoize(buildViewPropTransformers)
   private buildToolbarProps = memoize(buildToolbarProps)
   private buildToolbarProps = memoize(buildToolbarProps)
   private reportClassNames = memoize(reportClassNames)
   private reportClassNames = memoize(reportClassNames)
@@ -59,16 +65,17 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
   /*
   /*
   renders INSIDE of an outer div
   renders INSIDE of an outer div
   */
   */
-  render(props: CalendarComponentProps, state: CalendarComponentState, context: ComponentContext) {
-    let { calendar, options, headerToolbar, footerToolbar } = context
+  render(props: CalendarComponentProps, state: CalendarComponentState) {
+    let { toolbarConfig, theme, dateEnv, options, calendar } = props
+    let viewTitle = this.computeTitle(props.dateProfile, dateEnv, options)
 
 
     let toolbarProps = this.buildToolbarProps(
     let toolbarProps = this.buildToolbarProps(
       props.viewSpec,
       props.viewSpec,
       props.dateProfile,
       props.dateProfile,
       props.dateProfileGenerator,
       props.dateProfileGenerator,
       props.currentDate,
       props.currentDate,
-      calendar.getNow(),
-      props.title
+      calendar.getNow(), // TODO: use NowTimer????
+      viewTitle
     )
     )
 
 
     let calendarHeight: string | number = ''
     let calendarHeight: string | number = ''
@@ -88,20 +95,31 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
     }
     }
 
 
     if (props.onClassNameChange) {
     if (props.onClassNameChange) {
-      this.reportClassNames(props.onClassNameChange, state.forPrint, options.direction, context.theme)
+      this.reportClassNames(props.onClassNameChange, state.forPrint, options.direction, theme)
     }
     }
 
 
     if (props.onHeightChange) {
     if (props.onHeightChange) {
       this.reportHeight(props.onHeightChange, calendarHeight)
       this.reportHeight(props.onHeightChange, calendarHeight)
     }
     }
 
 
+    let viewContext = this.buildViewContext(
+      props.viewSpec,
+      viewTitle,
+      props.dateProfile,
+      props.dateProfileGenerator,
+      props.dateEnv,
+      props.pluginHooks,
+      props.theme,
+      props.calendar
+    )
+
     return (
     return (
-      <Fragment>
-        {headerToolbar &&
+      <ComponentContextType.Provider value={viewContext}>
+        {toolbarConfig.headerToolbar &&
           <Toolbar
           <Toolbar
             ref={this.headerRef}
             ref={this.headerRef}
             extraClassName='fc-header-toolbar'
             extraClassName='fc-header-toolbar'
-            model={headerToolbar}
+            model={toolbarConfig.headerToolbar}
             { ...toolbarProps }
             { ...toolbarProps }
           />
           />
         }
         }
@@ -111,18 +129,18 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
           aspectRatio={viewAspectRatio}
           aspectRatio={viewAspectRatio}
           onClick={this.handleNavLinkClick}
           onClick={this.handleNavLinkClick}
         >
         >
-          {this.renderView(props, this.context)}
+          {this.renderView(props)}
           {this.buildAppendContent()}
           {this.buildAppendContent()}
         </ViewContainer>
         </ViewContainer>
-        {footerToolbar &&
+        {toolbarConfig.footerToolbar &&
           <Toolbar
           <Toolbar
             ref={this.footerRef}
             ref={this.footerRef}
             extraClassName='fc-footer-toolbar'
             extraClassName='fc-footer-toolbar'
-            model={footerToolbar}
+            model={toolbarConfig.footerToolbar}
             { ...toolbarProps }
             { ...toolbarProps }
           />
           />
         }
         }
-      </Fragment>
+      </ComponentContextType.Provider>
     )
     )
   }
   }
 
 
@@ -131,13 +149,13 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
     window.addEventListener('beforeprint', this.handleBeforePrint)
     window.addEventListener('beforeprint', this.handleBeforePrint)
     window.addEventListener('afterprint', this.handleAfterPrint)
     window.addEventListener('afterprint', this.handleAfterPrint)
 
 
-    this.context.calendar.publiclyTrigger('datesDidUpdate')
+    this.props.calendar.publiclyTrigger('datesDidUpdate')
   }
   }
 
 
 
 
   componentDidUpdate(prevProps: CalendarComponentProps) {
   componentDidUpdate(prevProps: CalendarComponentProps) {
     if (prevProps.dateProfile !== this.props.dateProfile) {
     if (prevProps.dateProfile !== this.props.dateProfile) {
-      this.context.calendar.publiclyTrigger('datesDidUpdate')
+      this.props.calendar.publiclyTrigger('datesDidUpdate')
     }
     }
   }
   }
 
 
@@ -166,7 +184,7 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
 
 
 
 
   _handleNavLinkClick(ev: UIEvent, anchorEl: HTMLElement) {
   _handleNavLinkClick(ev: UIEvent, anchorEl: HTMLElement) {
-    let { dateEnv, calendar } = this.context
+    let { dateEnv, calendar } = this.props
 
 
     let navLinkOptions: any = anchorEl.getAttribute('data-navlink')
     let navLinkOptions: any = anchorEl.getAttribute('data-navlink')
     navLinkOptions = navLinkOptions ? JSON.parse(navLinkOptions) : {}
     navLinkOptions = navLinkOptions ? JSON.parse(navLinkOptions) : {}
@@ -191,7 +209,7 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
 
 
 
 
   buildAppendContent() {
   buildAppendContent() {
-    let { pluginHooks, calendar } = this.context
+    let { pluginHooks, calendar } = this.props
 
 
     return pluginHooks.viewContainerAppends.map(
     return pluginHooks.viewContainerAppends.map(
       (buildAppendContent) => buildAppendContent(calendar)
       (buildAppendContent) => buildAppendContent(calendar)
@@ -199,14 +217,11 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
   }
   }
 
 
 
 
-  renderView(props: CalendarComponentProps, context: ComponentContext) {
-    let { pluginHooks, options } = context
+  renderView(props: CalendarComponentProps) {
+    let { pluginHooks, options } = props
     let { viewSpec } = props
     let { viewSpec } = props
 
 
     let viewProps: ViewProps = {
     let viewProps: ViewProps = {
-      viewSpec,
-      dateProfileGenerator: props.dateProfileGenerator,
-      dateProfile: props.dateProfile,
       businessHours: this.parseBusinessHours(viewSpec.options.businessHours),
       businessHours: this.parseBusinessHours(viewSpec.options.businessHours),
       eventStore: props.eventStore,
       eventStore: props.eventStore,
       eventUiBases: props.eventUiBases,
       eventUiBases: props.eventUiBases,
@@ -227,24 +242,13 @@ export class CalendarComponent extends BaseComponent<CalendarComponentProps, Cal
       )
       )
     }
     }
 
 
-    let viewContext = this.buildViewContext(
-      context.calendar,
-      context.pluginHooks,
-      context.dateEnv,
-      context.theme,
-      context.view,
-      viewSpec.options
-    )
-
     let ViewComponent = viewSpec.component
     let ViewComponent = viewSpec.component
 
 
     return (
     return (
-      <ComponentContextType.Provider value={viewContext}>
-        <ViewComponent
-          ref={this.viewRef}
-          { ...viewProps }
-          />
-      </ComponentContextType.Provider>
+      <ViewComponent
+        ref={this.viewRef}
+        { ...viewProps }
+      />
     )
     )
   }
   }
 
 
@@ -320,3 +324,55 @@ function buildViewPropTransformers(theClasses: ViewPropsTransformerClass[]) {
     return new theClass()
     return new theClass()
   })
   })
 }
 }
+
+
+// Title and Date Formatting
+// -----------------------------------------------------------------------------------------------------------------
+
+
+// Computes what the title at the top of the calendar should be for this view
+function computeTitle(dateProfile, dateEnv: DateEnv, viewOptions) {
+  let range: DateRange
+
+  // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
+  if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
+    range = dateProfile.currentRange
+  } else { // for day units or smaller, use the actual day range
+    range = dateProfile.activeRange
+  }
+
+  return dateEnv.formatRange(
+    range.start,
+    range.end,
+    createFormatter(
+      viewOptions.titleFormat || computeTitleFormat(dateProfile),
+      viewOptions.titleRangeSeparator
+    ),
+    { isEndExclusive: dateProfile.isRangeAllDay }
+  )
+}
+
+
+// Generates the format string that should be used to generate the title for the current date range.
+// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
+function computeTitleFormat(dateProfile) {
+  let currentRangeUnit = dateProfile.currentRangeUnit
+
+  if (currentRangeUnit === 'year') {
+    return { year: 'numeric' }
+  } else if (currentRangeUnit === 'month') {
+    return { year: 'numeric', month: 'long' } // like "September 2014"
+  } else {
+    let days = diffWholeDays(
+      dateProfile.currentRange.start,
+      dateProfile.currentRange.end
+    )
+    if (days !== null && days > 1) {
+      // multi-day range. shorter, like "Sep 9 - 10 2014"
+      return { year: 'numeric', month: 'short', day: 'numeric' }
+    } else {
+      // one day. longer, like "September 9 2014"
+      return { year: 'numeric', month: 'long', day: 'numeric' }
+    }
+  }
+}

+ 9 - 10
packages/core/src/DateProfileGenerator.ts

@@ -3,7 +3,6 @@ import { Duration, createDuration, getWeeksFromInput, asRoughDays, asRoughMs, gr
 import { DateRange, OpenDateRange, constrainMarkerToRange, intersectRanges, rangesIntersect, parseRange, rangesEqual } from './datelib/date-range'
 import { DateRange, OpenDateRange, constrainMarkerToRange, intersectRanges, rangesIntersect, parseRange, rangesEqual } from './datelib/date-range'
 import { ViewSpec } from './structs/view-spec'
 import { ViewSpec } from './structs/view-spec'
 import { DateEnv } from './datelib/env'
 import { DateEnv } from './datelib/env'
-import { Calendar } from './Calendar'
 import { computeVisibleDayRange } from './util/misc'
 import { computeVisibleDayRange } from './util/misc'
 
 
 
 
@@ -23,22 +22,22 @@ export interface DateProfile {
 
 
 export class DateProfileGenerator {
 export class DateProfileGenerator {
 
 
-  viewSpec: ViewSpec
   slotMinTime: Duration
   slotMinTime: Duration
   slotMaxTime: Duration
   slotMaxTime: Duration
   options: any
   options: any
-  dateEnv: DateEnv
-  calendar: Calendar // avoid
   isHiddenDayHash: boolean[]
   isHiddenDayHash: boolean[]
 
 
 
 
-  constructor(viewSpec: ViewSpec, calendar: Calendar) {
+  constructor(
+    protected viewSpec: ViewSpec,
+    protected dateEnv: DateEnv,
+    protected nowDate: DateMarker
+  ) {
     this.viewSpec = viewSpec
     this.viewSpec = viewSpec
+
     this.options = viewSpec.options
     this.options = viewSpec.options
-    this.slotMinTime = calendar.slotMinTime
-    this.slotMaxTime = calendar.slotMaxTime
-    this.dateEnv = calendar.dateEnv
-    this.calendar = calendar
+    this.slotMinTime = createDuration(viewSpec.options.slotMinTime)
+    this.slotMaxTime = createDuration(viewSpec.options.slotMaxTime)
 
 
     this.initHiddenDays()
     this.initHiddenDays()
   }
   }
@@ -154,7 +153,7 @@ export class DateProfileGenerator {
   // Indicates the minimum/maximum dates to display.
   // Indicates the minimum/maximum dates to display.
   // not responsible for trimming hidden days.
   // not responsible for trimming hidden days.
   buildValidRange(): OpenDateRange {
   buildValidRange(): OpenDateRange {
-    return this.getRangeOption('validRange', this.calendar.getNow()) ||
+    return this.getRangeOption('validRange', this.nowDate) ||
       { start: null, end: null } // completely open-ended
       { start: null, end: null } // completely open-ended
   }
   }
 
 

+ 24 - 52
packages/core/src/OptionsManager.ts

@@ -4,65 +4,37 @@ import { organizeRawLocales, buildLocale } from './datelib/locale'
 import { __assign } from 'tslib'
 import { __assign } from 'tslib'
 
 
 
 
-export class OptionsManager {
-
-  localeDefaults: any // option defaults related to current locale
-  overrides: any // option overrides given to the fullCalendar constructor
-  dynamicOverrides: any // options set with dynamic setter method. higher precedence than view overrides.
-  computed: any
-
-
-  constructor(overrides) {
-    this.overrides = { ...overrides } // make a copy
-    this.dynamicOverrides = {}
-    this.compute()
-  }
-
-
-  mutate(updates, removals: string[], isDynamic?: boolean) {
-
-    if (!Object.keys(updates).length && !removals.length) {
-      return
-    }
-
-    let overrideHash = isDynamic ? this.dynamicOverrides : this.overrides
-
-    __assign(overrideHash, updates)
-
-    for (let propName of removals) {
-      delete overrideHash[propName]
-    }
-
-    this.compute()
-  }
+export function compileOptions(overrides, dynamicOverrides) {
+  return compileOptionsAdvanced(overrides, dynamicOverrides).combined
+}
 
 
 
 
-  // Computes the flattened options hash for the calendar and assigns to `this.options`.
-  // Assumes this.overrides and this.dynamicOverrides have already been initialized.
-  compute() {
+export function compileOptionsAdvanced(overrides, dynamicOverrides, viewDefaults?, viewOverrides?) {
+  let locales = firstDefined( // explicit locale option given?
+    dynamicOverrides.locales,
+    overrides.locales,
+    globalDefaults.locales
+  )
 
 
-    // TODO: not a very efficient system
-    let locales = firstDefined( // explicit locale option given?
-      this.dynamicOverrides.locales,
-      this.overrides.locales,
-      globalDefaults.locales
-    )
-    let locale = firstDefined( // explicit locales option given?
-      this.dynamicOverrides.locale,
-      this.overrides.locale,
-      globalDefaults.locale
-    )
-    let available = organizeRawLocales(locales) // also done in Calendar :(
-    let localeDefaults = buildLocale(locale || available.defaultCode, available.map).options
+  let locale = firstDefined( // explicit locales option given?
+    dynamicOverrides.locale,
+    overrides.locale,
+    globalDefaults.locale
+  )
 
 
-    this.localeDefaults = localeDefaults
+  let availableLocaleData = organizeRawLocales(locales) // also done in Calendar :(
+  let localeDefaults = buildLocale(locale || availableLocaleData.defaultCode, availableLocaleData.map).options
 
 
-    this.computed = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
+  return {
+    availableLocaleData,
+    localeDefaults,
+    combined: mergeOptions([ // merge defaults and overrides. lowest to highest precedence
       globalDefaults, // global defaults
       globalDefaults, // global defaults
+      viewDefaults || {},
       localeDefaults,
       localeDefaults,
-      this.overrides,
-      this.dynamicOverrides
+      overrides,
+      viewOverrides || {},
+      dynamicOverrides
     ])
     ])
   }
   }
-
 }
 }

+ 3 - 2
packages/core/src/Toolbar.tsx

@@ -1,6 +1,7 @@
 import { h } from './vdom'
 import { h } from './vdom'
 import { BaseComponent } from './vdom-util'
 import { BaseComponent } from './vdom-util'
 import { ToolbarModel, ToolbarWidget } from './toolbar-parse'
 import { ToolbarModel, ToolbarWidget } from './toolbar-parse'
+import { ComponentContext } from './component/ComponentContext'
 
 
 
 
 export interface ToolbarProps extends ToolbarContent {
 export interface ToolbarProps extends ToolbarContent {
@@ -80,8 +81,8 @@ interface ToolbarSectionProps extends ToolbarContent {
 
 
 class ToolbarSection extends BaseComponent<ToolbarSectionProps> {
 class ToolbarSection extends BaseComponent<ToolbarSectionProps> {
 
 
-  render(props: ToolbarSectionProps) {
-    let { theme } = this.context
+  render(props: ToolbarSectionProps, state: {}, context: ComponentContext) {
+    let { theme } = context
 
 
     return (
     return (
       <div class='fc-toolbar-chunk'>
       <div class='fc-toolbar-chunk'>

+ 2 - 6
packages/core/src/View.ts

@@ -1,5 +1,4 @@
-import { DateProfileGenerator, DateProfile } from './DateProfileGenerator'
-import { ViewSpec } from './structs/view-spec'
+import { DateProfile } from './DateProfileGenerator'
 import { EventStore } from './structs/event-store'
 import { EventStore } from './structs/event-store'
 import { EventUiHash } from './component/event-ui'
 import { EventUiHash } from './component/event-ui'
 import { sliceEventStore, EventRenderRange } from './component/event-rendering'
 import { sliceEventStore, EventRenderRange } from './component/event-rendering'
@@ -10,9 +9,6 @@ import { Duration } from './datelib/duration'
 
 
 
 
 export interface ViewProps {
 export interface ViewProps {
-  viewSpec: ViewSpec
-  dateProfileGenerator: DateProfileGenerator
-  dateProfile: DateProfile
   businessHours: EventStore
   businessHours: EventStore
   eventStore: EventStore
   eventStore: EventStore
   eventUiBases: EventUiHash
   eventUiBases: EventUiHash
@@ -31,7 +27,7 @@ export interface ViewProps {
 if nextDayThreshold is specified, slicing is done in an all-day fashion.
 if nextDayThreshold is specified, slicing is done in an all-day fashion.
 you can get nextDayThreshold from context.nextDayThreshold
 you can get nextDayThreshold from context.nextDayThreshold
 */
 */
-export function sliceEvents(props: ViewProps & { nextDayThreshold: Duration }, allDay?: boolean): EventRenderRange[] {
+export function sliceEvents(props: ViewProps & { dateProfile: DateProfile, nextDayThreshold: Duration }, allDay?: boolean): EventRenderRange[] {
   return sliceEventStore(
   return sliceEventStore(
     props.eventStore,
     props.eventStore,
     props.eventUiBases,
     props.eventUiBases,

+ 6 - 6
packages/core/src/api/EventApi.ts

@@ -68,7 +68,7 @@ export class EventApi {
   }
   }
 
 
   setStart(startInput: DateInput, options: { granularity?: string, maintainDuration?: boolean } = {}) {
   setStart(startInput: DateInput, options: { granularity?: string, maintainDuration?: boolean } = {}) {
-    let { dateEnv } = this._calendar
+    let { dateEnv } = this._calendar.state
     let start = dateEnv.createMarker(startInput)
     let start = dateEnv.createMarker(startInput)
 
 
     if (start && this._instance) { // TODO: warning if parsed bad
     if (start && this._instance) { // TODO: warning if parsed bad
@@ -84,7 +84,7 @@ export class EventApi {
   }
   }
 
 
   setEnd(endInput: DateInput | null, options: { granularity?: string } = {}) {
   setEnd(endInput: DateInput | null, options: { granularity?: string } = {}) {
-    let { dateEnv } = this._calendar
+    let { dateEnv } = this._calendar.state
     let end
     let end
 
 
     if (endInput != null) {
     if (endInput != null) {
@@ -106,7 +106,7 @@ export class EventApi {
   }
   }
 
 
   setDates(startInput: DateInput, endInput: DateInput | null, options: { allDay?: boolean, granularity?: string } = {}) {
   setDates(startInput: DateInput, endInput: DateInput | null, options: { allDay?: boolean, granularity?: string } = {}) {
-    let { dateEnv } = this._calendar
+    let { dateEnv } = this._calendar.state
     let standardProps = { allDay: options.allDay } as any
     let standardProps = { allDay: options.allDay } as any
     let start = dateEnv.createMarker(startInput)
     let start = dateEnv.createMarker(startInput)
     let end
     let end
@@ -190,7 +190,7 @@ export class EventApi {
   }
   }
 
 
   formatRange(formatInput: FormatterInput) {
   formatRange(formatInput: FormatterInput) {
-    let { dateEnv } = this._calendar
+    let { dateEnv } = this._calendar.state
     let instance = this._instance
     let instance = this._instance
     let formatter = createFormatter(formatInput, this._calendar.opt('defaultRangeSeparator'))
     let formatter = createFormatter(formatInput, this._calendar.opt('defaultRangeSeparator'))
 
 
@@ -245,13 +245,13 @@ export class EventApi {
 
 
   get start(): Date | null {
   get start(): Date | null {
     return this._instance ?
     return this._instance ?
-      this._calendar.dateEnv.toDate(this._instance.range.start) :
+      this._calendar.state.dateEnv.toDate(this._instance.range.start) :
       null
       null
   }
   }
 
 
   get end(): Date | null {
   get end(): Date | null {
     return (this._instance && this._def.hasEnd) ?
     return (this._instance && this._def.hasEnd) ?
-      this._calendar.dateEnv.toDate(this._instance.range.end) :
+      this._calendar.state.dateEnv.toDate(this._instance.range.end) :
       null
       null
   }
   }
 
 

+ 2 - 8
packages/core/src/common/DayCellRoot.tsx

@@ -1,7 +1,6 @@
 import { Ref, ComponentChildren, h } from '../vdom'
 import { Ref, ComponentChildren, h } from '../vdom'
 import { DateMarker } from '../datelib/marker'
 import { DateMarker } from '../datelib/marker'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
-import { DateProfile } from '../DateProfileGenerator'
 import { ComponentContext } from '../component/ComponentContext'
 import { ComponentContext } from '../component/ComponentContext'
 import { getDateMeta, getDayClassNames, DateMeta } from '../component/date-rendering'
 import { getDateMeta, getDayClassNames, DateMeta } from '../component/date-rendering'
 import { formatDayString, createFormatter } from '../datelib/formatting'
 import { formatDayString, createFormatter } from '../datelib/formatting'
@@ -16,7 +15,6 @@ interface DayCellHookPropOrigin {
   date: DateMarker // generic
   date: DateMarker // generic
   todayRange: DateRange
   todayRange: DateRange
   showDayNumber?: boolean // defaults to false
   showDayNumber?: boolean // defaults to false
-  dateProfile?: DateProfile // for other/disabled days
 }
 }
 
 
 export interface DayCellHookProps extends DateMeta {
 export interface DayCellHookProps extends DateMeta {
@@ -30,7 +28,6 @@ export interface DayCellHookProps extends DateMeta {
 export interface DayCellRootProps {
 export interface DayCellRootProps {
   elRef?: Ref<any>
   elRef?: Ref<any>
   date: DateMarker
   date: DateMarker
-  dateProfile?: DateProfile // for other/disabled days
   todayRange: DateRange
   todayRange: DateRange
   showDayNumber?: boolean // defaults to false
   showDayNumber?: boolean // defaults to false
   extraHookProps?: object
   extraHookProps?: object
@@ -50,7 +47,6 @@ export class DayCellRoot extends BaseComponent<DayCellRootProps> {
   render(props: DayCellRootProps, state: {}, context: ComponentContext) {
   render(props: DayCellRootProps, state: {}, context: ComponentContext) {
     let hookPropsOrigin: DayCellHookPropOrigin = {
     let hookPropsOrigin: DayCellHookPropOrigin = {
       date: props.date,
       date: props.date,
-      dateProfile: props.dateProfile,
       todayRange: props.todayRange,
       todayRange: props.todayRange,
       showDayNumber: props.showDayNumber
       showDayNumber: props.showDayNumber
     }
     }
@@ -81,7 +77,6 @@ export class DayCellRoot extends BaseComponent<DayCellRootProps> {
 
 
 export interface DayCellContentProps {
 export interface DayCellContentProps {
   date: DateMarker
   date: DateMarker
-  dateProfile?: DateProfile // for other/disabled days
   todayRange: DateRange
   todayRange: DateRange
   showDayNumber?: boolean // defaults to false
   showDayNumber?: boolean // defaults to false
   extraHookProps?: object
   extraHookProps?: object
@@ -97,7 +92,6 @@ export class DayCellContent extends BaseComponent<DayCellContentProps> {
   render(props: DayCellContentProps, state: {}, context: ComponentContext) {
   render(props: DayCellContentProps, state: {}, context: ComponentContext) {
     let hookPropsOrigin: DayCellHookPropOrigin = {
     let hookPropsOrigin: DayCellHookPropOrigin = {
       date: props.date,
       date: props.date,
-      dateProfile: props.dateProfile,
       todayRange: props.todayRange,
       todayRange: props.todayRange,
       showDayNumber: props.showDayNumber
       showDayNumber: props.showDayNumber
     }
     }
@@ -119,11 +113,11 @@ export class DayCellContent extends BaseComponent<DayCellContentProps> {
 function massageHooksProps(input: DayCellHookPropOrigin, context: ComponentContext): DayCellHookProps {
 function massageHooksProps(input: DayCellHookPropOrigin, context: ComponentContext): DayCellHookProps {
   let { dateEnv } = context
   let { dateEnv } = context
   let { date } = input
   let { date } = input
-  let dayMeta = getDateMeta(date, input.todayRange, null, input.dateProfile)
+  let dayMeta = getDateMeta(date, input.todayRange, null, context.dateProfile)
 
 
   return {
   return {
     date: dateEnv.toDate(date),
     date: dateEnv.toDate(date),
-    view: context.view,
+    view: context.viewApi,
     ...dayMeta,
     ...dayMeta,
     dayNumberText: input.showDayNumber ? dateEnv.format(date, DAY_NUM_FORMAT) : ''
     dayNumberText: input.showDayNumber ? dateEnv.format(date, DAY_NUM_FORMAT) : ''
   }
   }

+ 0 - 3
packages/core/src/common/DayHeader.tsx

@@ -1,7 +1,6 @@
 import { BaseComponent } from '../vdom-util'
 import { BaseComponent } from '../vdom-util'
 import { ComponentContext } from '../component/ComponentContext'
 import { ComponentContext } from '../component/ComponentContext'
 import { DateMarker } from '../datelib/marker'
 import { DateMarker } from '../datelib/marker'
-import { DateProfile } from '../DateProfileGenerator'
 import { createFormatter } from '../datelib/formatting'
 import { createFormatter } from '../datelib/formatting'
 import { computeFallbackHeaderFormat } from './table-utils'
 import { computeFallbackHeaderFormat } from './table-utils'
 import { VNode, h } from '../vdom'
 import { VNode, h } from '../vdom'
@@ -13,7 +12,6 @@ import { memoize } from '../util/memoize'
 
 
 export interface DayHeaderProps {
 export interface DayHeaderProps {
   dates: DateMarker[]
   dates: DateMarker[]
-  dateProfile: DateProfile
   datesRepDistinctDays: boolean
   datesRepDistinctDays: boolean
   renderIntro?: () => VNode
   renderIntro?: () => VNode
 }
 }
@@ -43,7 +41,6 @@ export class DayHeader extends BaseComponent<DayHeaderProps> { // TODO: rename t
                 key={date.toISOString()}
                 key={date.toISOString()}
                 date={date}
                 date={date}
                 todayRange={todayRange}
                 todayRange={todayRange}
-                dateProfile={props.dateProfile}
                 colCnt={dates.length}
                 colCnt={dates.length}
                 dayHeaderFormat={dayHeaderFormat}
                 dayHeaderFormat={dayHeaderFormat}
               /> :
               /> :

+ 1 - 1
packages/core/src/common/EventRoot.tsx

@@ -47,7 +47,7 @@ export class EventRoot extends BaseComponent<EventRootProps> {
 
 
     let hookProps: EventMeta = {
     let hookProps: EventMeta = {
       event: new EventApi(context.calendar, eventRange.def, eventRange.instance),
       event: new EventApi(context.calendar, eventRange.def, eventRange.instance),
-      view: context.view,
+      view: context.viewApi,
       timeText: props.timeText,
       timeText: props.timeText,
       textColor: ui.textColor,
       textColor: ui.textColor,
       backgroundColor: ui.backgroundColor,
       backgroundColor: ui.backgroundColor,

+ 1 - 1
packages/core/src/common/NowIndicatorRoot.tsx

@@ -17,7 +17,7 @@ export const NowIndicatorRoot = (props: NowIndicatorRootProps) => (
       let hookProps = {
       let hookProps = {
         isAxis: props.isAxis,
         isAxis: props.isAxis,
         date: context.dateEnv.toDate(props.date),
         date: context.dateEnv.toDate(props.date),
-        view: context.view
+        view: context.viewApi
       }
       }
 
 
       return (
       return (

+ 3 - 5
packages/core/src/common/TableDateCell.tsx

@@ -1,7 +1,6 @@
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
 import { getDayClassNames, getDateMeta, DateMeta } from '../component/date-rendering'
 import { getDayClassNames, getDateMeta, DateMeta } from '../component/date-rendering'
 import { DateMarker, addDays } from '../datelib/marker'
 import { DateMarker, addDays } from '../datelib/marker'
-import { DateProfile } from '../DateProfileGenerator'
 import { ComponentContext } from '../component/ComponentContext'
 import { ComponentContext } from '../component/ComponentContext'
 import { h } from '../vdom'
 import { h } from '../vdom'
 import { __assign } from 'tslib'
 import { __assign } from 'tslib'
@@ -14,7 +13,6 @@ import { ViewApi } from '../ViewApi'
 
 
 export interface TableDateCellProps {
 export interface TableDateCellProps {
   date: DateMarker
   date: DateMarker
-  dateProfile: DateProfile
   todayRange: DateRange
   todayRange: DateRange
   colCnt: number
   colCnt: number
   dayHeaderFormat: DateFormatter
   dayHeaderFormat: DateFormatter
@@ -39,7 +37,7 @@ export class TableDateCell extends BaseComponent<TableDateCellProps> { // BAD na
   render(props: TableDateCellProps, state: {}, context: ComponentContext) {
   render(props: TableDateCellProps, state: {}, context: ComponentContext) {
     let { dateEnv, options } = context
     let { dateEnv, options } = context
     let { date } = props
     let { date } = props
-    let dayMeta = getDateMeta(date, props.todayRange, null, props.dateProfile)
+    let dayMeta = getDateMeta(date, props.todayRange, null, context.dateProfile)
 
 
     let classNames = [ CLASS_NAME ].concat(
     let classNames = [ CLASS_NAME ].concat(
       getDayClassNames(dayMeta, context.theme)
       getDayClassNames(dayMeta, context.theme)
@@ -53,7 +51,7 @@ export class TableDateCell extends BaseComponent<TableDateCellProps> { // BAD na
 
 
     let hookProps: DateHeaderCellHookProps = {
     let hookProps: DateHeaderCellHookProps = {
       date: dateEnv.toDate(date),
       date: dateEnv.toDate(date),
-      view: context.view,
+      view: context.viewApi,
       ...props.extraHookProps,
       ...props.extraHookProps,
       text,
       text,
       ...dayMeta
       ...dayMeta
@@ -127,7 +125,7 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
     let hookProps: DateHeaderCellHookProps = {
     let hookProps: DateHeaderCellHookProps = {
       date,
       date,
       ...dateMeta,
       ...dateMeta,
-      view: context.view,
+      view: context.viewApi,
       ...props.extraHookProps,
       ...props.extraHookProps,
       text
       text
     }
     }

+ 1 - 1
packages/core/src/common/ViewRoot.tsx

@@ -18,7 +18,7 @@ export class ViewRoot extends BaseComponent<ViewRootProps> {
 
 
 
 
   render(props: ViewRootProps, state: {}, context: ComponentContext) {
   render(props: ViewRootProps, state: {}, context: ComponentContext) {
-    let hookProps = { view: context.view }
+    let hookProps = { view: context.viewApi }
     let customClassNames = this.buildClassNames(hookProps, context)
     let customClassNames = this.buildClassNames(hookProps, context)
 
 
     return (
     return (

+ 46 - 25
packages/core/src/component/ComponentContext.ts

@@ -7,53 +7,75 @@ import { parseFieldSpecs } from '../util/misc'
 import { createDuration, Duration } from '../datelib/duration'
 import { createDuration, Duration } from '../datelib/duration'
 import { PluginHooks } from '../plugin-system'
 import { PluginHooks } from '../plugin-system'
 import { createContext } from '../vdom'
 import { createContext } from '../vdom'
-import { parseToolbars, ToolbarModel } from '../toolbar-parse'
 import { ScrollResponder, ScrollRequestHandler } from '../ScrollResponder'
 import { ScrollResponder, ScrollRequestHandler } from '../ScrollResponder'
-
+import { DateProfile, DateProfileGenerator } from '../DateProfileGenerator'
+import { ViewSpec } from '../structs/view-spec'
 
 
 export const ComponentContextType = createContext<ComponentContext>({} as any) // for Components
 export const ComponentContextType = createContext<ComponentContext>({} as any) // for Components
 
 
+// TODO: rename file
+// TODO: rename to ViewContext
 
 
 export interface ComponentContext {
 export interface ComponentContext {
-  calendar: Calendar
-  pluginHooks: PluginHooks
+  viewSpec: ViewSpec
+  viewApi: ViewApi
+  options: any
+  dateProfile: DateProfile
+  dateProfileGenerator: DateProfileGenerator
   dateEnv: DateEnv
   dateEnv: DateEnv
+  pluginHooks: PluginHooks
   theme: Theme
   theme: Theme
-  view: ViewApi
-  options: any
-  isRtl: boolean
-  eventOrderSpecs: any
-  nextDayThreshold: Duration
-  headerToolbar: ToolbarModel | null
-  footerToolbar: ToolbarModel | null
-  viewsWithButtons: string[]
+  calendar: Calendar
+
   addResizeHandler: (handler: ResizeHandler) => void
   addResizeHandler: (handler: ResizeHandler) => void
   removeResizeHandler: (handler: ResizeHandler) => void
   removeResizeHandler: (handler: ResizeHandler) => void
   createScrollResponder: (execFunc: ScrollRequestHandler) => ScrollResponder
   createScrollResponder: (execFunc: ScrollRequestHandler) => ScrollResponder
+
+  // computed from options...
+  isRtl: boolean
+  eventOrderSpecs: any
+  nextDayThreshold: Duration
 }
 }
 
 
 
 
-export function buildContext(
-  calendar: Calendar,
-  pluginHooks: PluginHooks,
+export function buildViewContext(
+  viewSpec: ViewSpec,
+  viewTitle: string,
+  dateProfile: DateProfile,
+  dateProfileGenerator: DateProfileGenerator,
   dateEnv: DateEnv,
   dateEnv: DateEnv,
+  pluginHooks: PluginHooks,
   theme: Theme,
   theme: Theme,
-  view: ViewApi,
-  options: any
+  calendar: Calendar,
 ): ComponentContext {
 ): ComponentContext {
   return {
   return {
-    calendar,
-    pluginHooks,
+    viewSpec,
+    viewApi: buildViewApi(viewSpec, viewTitle, dateProfile, dateEnv),
+    options: viewSpec.options,
+    dateProfile,
+    dateProfileGenerator,
     dateEnv,
     dateEnv,
+    pluginHooks,
     theme,
     theme,
-    view,
-    options,
-    ...computeContextProps(options, theme, calendar),
+    calendar,
     addResizeHandler: calendar.addResizeHandler,
     addResizeHandler: calendar.addResizeHandler,
     removeResizeHandler: calendar.removeResizeHandler,
     removeResizeHandler: calendar.removeResizeHandler,
     createScrollResponder(execFunc: ScrollRequestHandler) {
     createScrollResponder(execFunc: ScrollRequestHandler) {
       return new ScrollResponder(calendar, execFunc)
       return new ScrollResponder(calendar, execFunc)
-    }
+    },
+    ...computeContextProps(viewSpec.options, theme, calendar)
+  }
+}
+
+
+function buildViewApi(viewSpec: ViewSpec, viewTitle: string, dateProfile: DateProfile, dateEnv: DateEnv) {
+  return {
+    type: viewSpec.type,
+    title: viewTitle,
+    activeStart: dateEnv.toDate(dateProfile.activeRange.start),
+    activeEnd: dateEnv.toDate(dateProfile.activeRange.end),
+    currentStart: dateEnv.toDate(dateProfile.currentRange.start),
+    currentEnd: dateEnv.toDate(dateProfile.currentRange.end)
   }
   }
 }
 }
 
 
@@ -64,7 +86,6 @@ function computeContextProps(options: any, theme: Theme, calendar: Calendar) {
   return {
   return {
     isRtl,
     isRtl,
     eventOrderSpecs: parseFieldSpecs(options.eventOrder),
     eventOrderSpecs: parseFieldSpecs(options.eventOrder),
-    nextDayThreshold: createDuration(options.nextDayThreshold),
-    ...parseToolbars(options, theme, isRtl, calendar)
+    nextDayThreshold: createDuration(options.nextDayThreshold)
   }
   }
 }
 }

+ 1 - 1
packages/core/src/event-sources/func-event-source.ts

@@ -28,7 +28,7 @@ let eventSourceDef: EventSourceDef = {
   },
   },
 
 
   fetch(arg, success, failure) {
   fetch(arg, success, failure) {
-    let dateEnv = arg.calendar.dateEnv
+    let dateEnv = arg.calendar.state.dateEnv
     let func = arg.eventSource.meta as EventSourceFunc
     let func = arg.eventSource.meta as EventSourceFunc
 
 
     unpromisify(
     unpromisify(

+ 1 - 1
packages/core/src/event-sources/json-feed-event-source.ts

@@ -55,7 +55,7 @@ export const jsonFeedEventSourcePlugin = createPlugin({
 })
 })
 
 
 function buildRequestParams(meta: JsonFeedMeta, range: DateRange, calendar: Calendar) {
 function buildRequestParams(meta: JsonFeedMeta, range: DateRange, calendar: Calendar) {
-  const dateEnv = calendar.dateEnv
+  const dateEnv = calendar.state.dateEnv
   let startParam
   let startParam
   let endParam
   let endParam
   let timeZoneParam
   let timeZoneParam

+ 2 - 2
packages/core/src/interactions/EventClicking.ts

@@ -22,7 +22,7 @@ export class EventClicking extends Interaction {
 
 
   handleSegClick = (ev: Event, segEl: HTMLElement) => {
   handleSegClick = (ev: Event, segEl: HTMLElement) => {
     let { component } = this
     let { component } = this
-    let { calendar, view } = component.context
+    let { calendar, viewApi } = component.context
     let seg = getElSeg(segEl)
     let seg = getElSeg(segEl)
 
 
     if (
     if (
@@ -44,7 +44,7 @@ export class EventClicking extends Interaction {
             seg.eventRange.instance
             seg.eventRange.instance
           ),
           ),
           jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
           jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
-          view
+          view: viewApi
         }
         }
       ])
       ])
 
 

+ 2 - 2
packages/core/src/interactions/EventHovering.ts

@@ -57,7 +57,7 @@ export class EventHovering extends Interaction {
 
 
   triggerEvent(publicEvName: 'eventMouseEnter' | 'eventMouseLeave', ev: Event | null, segEl: HTMLElement) {
   triggerEvent(publicEvName: 'eventMouseEnter' | 'eventMouseLeave', ev: Event | null, segEl: HTMLElement) {
     let { component } = this
     let { component } = this
-    let { calendar, view } = component.context
+    let { calendar, viewApi } = component.context
     let seg = getElSeg(segEl)!
     let seg = getElSeg(segEl)!
 
 
     if (!ev || component.isValidSegDownEl(ev.target as HTMLElement)) {
     if (!ev || component.isValidSegDownEl(ev.target as HTMLElement)) {
@@ -70,7 +70,7 @@ export class EventHovering extends Interaction {
             seg.eventRange.instance
             seg.eventRange.instance
           ),
           ),
           jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
           jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
-          view
+          view: viewApi
         }
         }
       ])
       ])
     }
     }

+ 2 - 8
packages/core/src/option-change-handlers.ts

@@ -1,4 +1,4 @@
-import { createPlugin, PluginDef } from './plugin-system'
+import { createPlugin } from './plugin-system'
 import { Calendar } from './main'
 import { Calendar } from './main'
 import { hashValuesToArray } from './util/object'
 import { hashValuesToArray } from './util/object'
 import { EventSource } from './structs/event-source'
 import { EventSource } from './structs/event-source'
@@ -8,8 +8,7 @@ export const changeHandlerPlugin = createPlugin({
     events(events, calendar) {
     events(events, calendar) {
       handleEventSources([ events ], calendar)
       handleEventSources([ events ], calendar)
     },
     },
-    eventSources: handleEventSources,
-    plugins: handlePlugins
+    eventSources: handleEventSources
   }
   }
 })
 })
 
 
@@ -47,8 +46,3 @@ function handleEventSources(inputs, calendar: Calendar) {
     calendar.addEventSource(newInput)
     calendar.addEventSource(newInput)
   }
   }
 }
 }
-
-// shortcoming: won't remove plugins
-function handlePlugins(pluginDefs: PluginDef[], calendar: Calendar) {
-  calendar.addPluginDefs(pluginDefs) // will gracefully handle duplicates
-}

+ 46 - 44
packages/core/src/plugin-system.ts

@@ -22,6 +22,7 @@ import { guid } from './util/misc'
 import { ComponentChildren } from './vdom'
 import { ComponentChildren } from './vdom'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
 import { ContentTypeHandlers } from './common/render-hook'
 import { ContentTypeHandlers } from './common/render-hook'
+import { globalPlugins } from './global-plugins'
 
 
 // TODO: easier way to add new hooks? need to update a million things
 // TODO: easier way to add new hooks? need to update a million things
 
 
@@ -134,58 +135,59 @@ export function createPlugin(input: PluginDefInput): PluginDef {
   }
   }
 }
 }
 
 
-export class PluginSystem {
-
-  hooks: PluginHooks
-  addedHash: { [pluginId: string]: true }
-
-  constructor() {
-    this.hooks = {
-      reducers: [],
-      eventDefParsers: [],
-      isDraggableTransformers: [],
-      eventDragMutationMassagers: [],
-      eventDefMutationAppliers: [],
-      dateSelectionTransformers: [],
-      datePointTransforms: [],
-      dateSpanTransforms: [],
-      views: {},
-      viewPropsTransformers: [],
-      isPropsValid: null,
-      externalDefTransforms: [],
-      eventResizeJoinTransforms: [],
-      viewContainerAppends: [],
-      eventDropTransformers: [],
-      componentInteractions: [],
-      calendarInteractions: [],
-      themeClasses: {},
-      eventSourceDefs: [],
-      cmdFormatter: null,
-      recurringTypes: [],
-      namedTimeZonedImpl: null,
-      initialView: '',
-      elementDraggingImpl: null,
-      optionChangeHandlers: {},
-      scrollGridImpl: null,
-      contentTypeHandlers: {}
-    }
-    this.addedHash = {}
-  }
 
 
-  add(plugin: PluginDef) {
-    if (!this.addedHash[plugin.id]) {
-      this.addedHash[plugin.id] = true
+export function buildPluginHooks(pluginDefs?: PluginDef[]): PluginHooks {
+  let isAdded: { [pluginId: string]: boolean } = {}
+  let hooks: PluginHooks = {
+    reducers: [],
+    eventDefParsers: [],
+    isDraggableTransformers: [],
+    eventDragMutationMassagers: [],
+    eventDefMutationAppliers: [],
+    dateSelectionTransformers: [],
+    datePointTransforms: [],
+    dateSpanTransforms: [],
+    views: {},
+    viewPropsTransformers: [],
+    isPropsValid: null,
+    externalDefTransforms: [],
+    eventResizeJoinTransforms: [],
+    viewContainerAppends: [],
+    eventDropTransformers: [],
+    componentInteractions: [],
+    calendarInteractions: [],
+    themeClasses: {},
+    eventSourceDefs: [],
+    cmdFormatter: null,
+    recurringTypes: [],
+    namedTimeZonedImpl: null,
+    initialView: '',
+    elementDraggingImpl: null,
+    optionChangeHandlers: {},
+    scrollGridImpl: null,
+    contentTypeHandlers: {}
+  }
 
 
-      for (let dep of plugin.deps) {
-        this.add(dep)
+  function addDefs(defs: PluginDef[]) {
+    for (let def of defs) {
+      if (!isAdded[def.id]) {
+        isAdded[def.id] = true
+        addDefs(def.deps)
+        hooks = combineHooks(hooks, def)
       }
       }
-
-      this.hooks = combineHooks(this.hooks, plugin)
     }
     }
   }
   }
 
 
+  addDefs(globalPlugins)
+
+  if (pluginDefs) {
+    addDefs(pluginDefs)
+  }
+
+  return hooks
 }
 }
 
 
+
 function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
 function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
   return {
   return {
     reducers: hooks0.reducers.concat(hooks1.reducers),
     reducers: hooks0.reducers.concat(hooks1.reducers),

+ 174 - 0
packages/core/src/reducers/CalendarStateReducer.ts

@@ -0,0 +1,174 @@
+
+import { organizeRawLocales, buildLocale, RawLocaleInfo } from '../datelib/locale'
+import { memoize } from '../util/memoize'
+import { Action, CalendarState } from './types'
+import { PluginHooks, buildPluginHooks } from '../plugin-system'
+import { DateEnv } from '../datelib/env'
+import { compileOptions } from '../OptionsManager'
+import { Calendar } from '../Calendar'
+import { StandardTheme } from '../theme/StandardTheme'
+import { EventSourceHash } from '../structs/event-source'
+import { buildViewSpecs, ViewSpecHash } from '../structs/view-spec'
+import { mapHash } from '../util/object'
+import { DateProfileGenerator } from '../DateProfileGenerator'
+import { reduceViewType } from './view-type'
+import { reduceCurrentDate, getInitialDate, getNow } from './current-date'
+import { reduceDateProfile } from './date-profile'
+import { reduceEventSources } from './eventSources'
+import { reduceEventStore } from './eventStore'
+import { reduceDateSelection } from './date-selection'
+import { reduceSelectedEvent } from './selected-event'
+import { reduceEventDrag } from './event-drag'
+import { reduceEventResize } from './event-resize'
+
+
+export class CalendarStateReducer {
+
+  private compileOptions = memoize(compileOptions)
+  private buildPluginHooks = memoize(buildPluginHooks)
+  private organizeRawLocales = memoize(organizeRawLocales)
+  private buildDateEnv = memoize(buildDateEnv)
+  private buildTheme = memoize(buildTheme)
+  private buildViewSpecs = memoize(buildViewSpecs)
+  private buildDateProfileGenerator = memoize(buildDateProfileGenerators)
+
+
+  reduce(state: CalendarState, action: Action, calendar: Calendar): CalendarState {
+    let optionOverrides = state.optionOverrides || {}
+    let dynamicOptionOverrides = state.dynamicOptionOverrides || {}
+
+    switch (action.type) {
+      case 'INIT':
+        optionOverrides = action.optionOverrides
+        break
+
+      case 'SET_OPTION':
+        dynamicOptionOverrides = { ...dynamicOptionOverrides, [action.optionName]: action.optionValue }
+        break
+
+      case 'MUTATE_OPTIONS':
+        let { updates, removals, isDynamic } = action
+
+        if (Object.keys(updates).length || removals.length) {
+          let hash = isDynamic
+            ? (dynamicOptionOverrides = { ...dynamicOptionOverrides, updates })
+            : (optionOverrides = { ...optionOverrides, updates })
+
+          for (let removal of removals) {
+            delete hash[removal]
+          }
+        }
+        break
+    }
+
+    let options = this.compileOptions(optionOverrides, dynamicOptionOverrides) // TODO: this is also done elsewhere
+    let pluginHooks = this.buildPluginHooks(options.plugins)
+    let viewSpecs = this.buildViewSpecs(pluginHooks.views, optionOverrides, dynamicOptionOverrides)
+    let availableLocaleData = this.organizeRawLocales(options.locales)
+    let prevDateEnv = state.dateEnv
+    let dateEnv = this.buildDateEnv(
+      options.timeZone,
+      options.locale,
+      options.weekNumberCalculation,
+      options.firstDay,
+      options.weekText,
+      pluginHooks,
+      availableLocaleData
+    )
+    let dateProfileGenerators = this.buildDateProfileGenerator(viewSpecs, dateEnv)
+    let theme = this.buildTheme(options, pluginHooks)
+
+    let viewType = state.viewType || options.initialView || pluginHooks.initialView // weird how we do INIT
+    viewType = reduceViewType(viewType, action, pluginHooks.views)
+
+    let currentDate = state.currentDate || getInitialDate(options, dateEnv) // weird how we do INIT
+    let dateProfileGenerator = dateProfileGenerators[viewType]
+    let dateProfile = reduceDateProfile(state.dateProfile, action, currentDate, dateProfileGenerator)
+    currentDate = reduceCurrentDate(currentDate, action, dateProfile)
+
+    let eventSources = reduceEventSources(state.eventSources, action, dateProfile, pluginHooks, options, calendar)
+
+    let nextState = {
+      ...state, // preserve previous state from plugin reducers
+      optionOverrides,
+      dynamicOptionOverrides,
+      options,
+      dateEnv,
+      pluginHooks,
+      availableRawLocales: availableLocaleData.map,
+      theme,
+      viewSpecs,
+      viewType,
+      dateProfileGenerator,
+      dateProfile,
+      currentDate,
+      eventSources,
+      eventStore: reduceEventStore(state.eventStore, action, eventSources, dateProfile, dateEnv, prevDateEnv, calendar),
+      dateSelection: reduceDateSelection(state.dateSelection, action),
+      eventSelection: reduceSelectedEvent(state.eventSelection, action),
+      eventDrag: reduceEventDrag(state.eventDrag, action),
+      eventResize: reduceEventResize(state.eventResize, action),
+      eventSourceLoadingLevel: computeLoadingLevel(eventSources),
+      loadingLevel: computeLoadingLevel(eventSources)
+    }
+
+    for (let reducerFunc of pluginHooks.reducers) {
+      nextState = reducerFunc(nextState, action, options, calendar)
+    }
+
+    return nextState
+  }
+}
+
+
+function buildDateEnv(
+  timeZone: string,
+  explicitLocale: string,
+  weekNumberCalculation,
+  firstDay,
+  weekText,
+  pluginHooks: PluginHooks,
+  availableLocaleData: RawLocaleInfo
+) {
+  let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map)
+
+  return new DateEnv({
+    calendarSystem: 'gregory', // TODO: make this a setting
+    timeZone: timeZone,
+    namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl,
+    locale,
+    weekNumberCalculation,
+    firstDay,
+    weekText,
+    cmdFormatter: pluginHooks.cmdFormatter
+  })
+}
+
+
+function buildTheme(rawOptions, pluginHooks: PluginHooks) {
+  let ThemeClass = pluginHooks.themeClasses[rawOptions.themeSystem] || StandardTheme
+
+  return new ThemeClass(rawOptions)
+}
+
+
+function buildDateProfileGenerators(viewSpecs: ViewSpecHash, dateEnv: DateEnv) {
+  return mapHash(viewSpecs, (viewSpec) => {
+    let DateProfileGeneratorClass = viewSpec.options.dateProfileGeneratorClass || DateProfileGenerator
+
+    return new DateProfileGeneratorClass(viewSpec, dateEnv, getNow(viewSpec.options, dateEnv))
+  })
+}
+
+
+function computeLoadingLevel(eventSources: EventSourceHash): number {
+  let cnt = 0
+
+  for (let sourceId in eventSources) {
+    if (eventSources[sourceId].isFetching) {
+      cnt++
+    }
+  }
+
+  return cnt
+}

+ 58 - 0
packages/core/src/reducers/current-date.ts

@@ -0,0 +1,58 @@
+import { DateMarker } from '../datelib/marker'
+import { Action } from './types'
+import { DateProfile } from '../DateProfileGenerator'
+import { rangeContainsMarker } from '../datelib/date-range'
+
+
+export function reduceCurrentDate(currentDate: DateMarker, action: Action, dateProfile: DateProfile): DateMarker {
+  // on INIT, currentDate will already be set
+
+  switch (action.type) {
+    case 'PREV':
+    case 'NEXT':
+      if (!rangeContainsMarker(dateProfile.currentRange, currentDate)) {
+        return dateProfile.currentRange.start
+      } else {
+        return currentDate
+      }
+
+    case 'SET_DATE':
+    case 'SET_VIEW_TYPE':
+      let newDate = action.dateMarker || currentDate
+      if (dateProfile.activeRange && !rangeContainsMarker(dateProfile.activeRange, newDate)) {
+        return dateProfile.currentRange.start
+      } else {
+        return newDate
+      }
+
+    default:
+      return currentDate
+  }
+}
+
+
+export function getInitialDate(options, dateEnv) { // NOT used in this reducer. TODO: do INIT in reducer
+  let initialDateInput = options.initialDate
+
+  // compute the initial ambig-timezone date
+  if (initialDateInput != null) {
+    return dateEnv.createMarker(initialDateInput)
+  } else {
+    return getNow(options, dateEnv) // getNow already returns unzoned
+  }
+}
+
+
+export function getNow(options, dateEnv) {
+  let now = options.now
+
+  if (typeof now === 'function') {
+    now = now()
+  }
+
+  if (now == null) {
+    return dateEnv.createNowMarker()
+  }
+
+  return dateEnv.createMarker(now)
+}

+ 58 - 0
packages/core/src/reducers/date-profile.ts

@@ -0,0 +1,58 @@
+import { DateProfile, DateProfileGenerator, isDateProfilesEqual } from '../DateProfileGenerator'
+import { Action } from './types'
+import { DateMarker } from '../datelib/marker'
+import { rangeContainsMarker } from '../datelib/date-range'
+
+
+export function reduceDateProfile(currentDateProfile: DateProfile | null, action: Action, currentDate: DateMarker, dateProfileGenerator: DateProfileGenerator): DateProfile {
+  let newDateProfile: DateProfile
+
+  switch (action.type) {
+    case 'INIT':
+      newDateProfile = dateProfileGenerator.build(
+        currentDate,
+        undefined,
+        true // forceToValid
+      )
+      break
+
+    case 'PREV':
+      newDateProfile = dateProfileGenerator.buildPrev(currentDateProfile, currentDate)
+      break
+
+    case 'NEXT':
+      newDateProfile = dateProfileGenerator.buildNext(currentDateProfile, currentDate)
+      break
+
+    case 'SET_DATE':
+      if (
+        !currentDateProfile.activeRange ||
+        !rangeContainsMarker(currentDateProfile.currentRange, action.dateMarker)
+      ) {
+        newDateProfile = dateProfileGenerator.build(
+          action.dateMarker,
+          undefined,
+          true // forceToValid
+        )
+      }
+      break
+
+    case 'SET_VIEW_TYPE':
+      newDateProfile = dateProfileGenerator.build(
+        action.dateMarker || currentDate,
+        undefined,
+        true // forceToValid
+      )
+      break
+  }
+
+  if (
+    newDateProfile &&
+    newDateProfile.isValid &&
+    !(currentDateProfile && isDateProfilesEqual(currentDateProfile, newDateProfile))
+  ) {
+    return newDateProfile
+  } else {
+    return currentDateProfile
+  }
+}

+ 17 - 0
packages/core/src/reducers/date-selection.ts

@@ -0,0 +1,17 @@
+import { DateSpan } from '../structs/date-span'
+import { Action } from './types'
+
+
+export function reduceDateSelection(currentSelection: DateSpan | null, action: Action) {
+  switch (action.type) {
+    case 'INIT':
+    case 'UNSELECT_DATES':
+      return null
+
+    case 'SELECT_DATES':
+      return action.selection
+
+    default:
+      return currentSelection
+  }
+}

+ 23 - 0
packages/core/src/reducers/event-drag.ts

@@ -0,0 +1,23 @@
+import { Action } from './types'
+import { EventInteractionState } from '../interactions/event-interaction-state'
+
+
+export function reduceEventDrag(currentDrag: EventInteractionState | null, action: Action): EventInteractionState | null {
+  switch (action.type) {
+    case 'INIT':
+    case 'UNSET_EVENT_DRAG':
+      return null
+
+    case 'SET_EVENT_DRAG':
+      let newDrag = action.state
+
+      return {
+        affectedEvents: newDrag.affectedEvents,
+        mutatedEvents: newDrag.mutatedEvents,
+        isEvent: newDrag.isEvent
+      }
+
+    default:
+      return currentDrag
+  }
+}

+ 23 - 0
packages/core/src/reducers/event-resize.ts

@@ -0,0 +1,23 @@
+import { EventInteractionState } from '../interactions/event-interaction-state'
+import { Action } from './types'
+
+
+export function reduceEventResize(currentResize: EventInteractionState | null, action: Action): EventInteractionState | null {
+  switch (action.type) {
+    case 'INIT':
+    case 'UNSET_EVENT_RESIZE':
+      return null
+
+    case 'SET_EVENT_RESIZE':
+      let newResize = action.state
+
+      return {
+        affectedEvents: newResize.affectedEvents,
+        mutatedEvents: newResize.mutatedEvents,
+        isEvent: newResize.isEvent
+      }
+
+    default:
+      return currentResize
+  }
+}

+ 67 - 20
packages/core/src/reducers/eventSources.ts

@@ -1,16 +1,28 @@
-import { EventSource, EventSourceHash, doesSourceNeedRange } from '../structs/event-source'
+import { EventSource, EventSourceHash, doesSourceNeedRange, parseEventSource } from '../structs/event-source'
 import { Calendar } from '../Calendar'
 import { Calendar } from '../Calendar'
 import { arrayToHash, filterHash } from '../util/object'
 import { arrayToHash, filterHash } from '../util/object'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
 import { DateProfile } from '../DateProfileGenerator'
 import { DateProfile } from '../DateProfileGenerator'
 import { Action } from './types'
 import { Action } from './types'
 import { guid } from '../util/misc'
 import { guid } from '../util/misc'
+import { PluginHooks } from '../plugin-system'
+
+export function reduceEventSources(eventSources: EventSourceHash, action: Action, dateProfile: DateProfile | null, pluginHooks: PluginHooks, rawOptions, calendar: Calendar): EventSourceHash {
+  let activeRange = dateProfile ? dateProfile.activeRange : null
 
 
-export function reduceEventSources(eventSources: EventSourceHash, action: Action, dateProfile: DateProfile | null, calendar: Calendar): EventSourceHash {
   switch (action.type) {
   switch (action.type) {
+    case 'INIT':
+      return addSources(
+        eventSources,
+        parseInitialSources(action.optionOverrides, pluginHooks, calendar),
+        activeRange,
+        pluginHooks,
+        rawOptions,
+        calendar
+      )
 
 
     case 'ADD_EVENT_SOURCES': // already parsed
     case 'ADD_EVENT_SOURCES': // already parsed
-      return addSources(eventSources, action.sources, dateProfile ? dateProfile.activeRange : null, calendar)
+      return addSources(eventSources, action.sources, activeRange, pluginHooks, rawOptions, calendar)
 
 
     case 'REMOVE_EVENT_SOURCE':
     case 'REMOVE_EVENT_SOURCE':
       return removeSource(eventSources, action.sourceId)
       return removeSource(eventSources, action.sourceId)
@@ -20,22 +32,35 @@ export function reduceEventSources(eventSources: EventSourceHash, action: Action
     case 'SET_DATE':
     case 'SET_DATE':
     case 'SET_VIEW_TYPE':
     case 'SET_VIEW_TYPE':
       if (dateProfile) {
       if (dateProfile) {
-        return fetchDirtySources(eventSources, dateProfile.activeRange, calendar)
+        return fetchDirtySources(eventSources, activeRange, pluginHooks, rawOptions, calendar)
       } else {
       } else {
         return eventSources
         return eventSources
       }
       }
 
 
     case 'FETCH_EVENT_SOURCES':
     case 'FETCH_EVENT_SOURCES':
-    case 'CHANGE_TIMEZONE':
       return fetchSourcesByIds(
       return fetchSourcesByIds(
         eventSources,
         eventSources,
-        (action as any).sourceIds ?
+        (action as any).sourceIds ? // why no type?
           arrayToHash((action as any).sourceIds) :
           arrayToHash((action as any).sourceIds) :
-          excludeStaticSources(eventSources, calendar),
-        dateProfile ? dateProfile.activeRange : null,
+          excludeStaticSources(eventSources, pluginHooks),
+        activeRange,
+        pluginHooks,
         calendar
         calendar
       )
       )
 
 
+    case 'SET_OPTION':
+      if (action.optionName === 'timeZone') {
+        return fetchSourcesByIds(
+          eventSources,
+          excludeStaticSources(eventSources, pluginHooks),
+          activeRange,
+          pluginHooks,
+          calendar
+        )
+      } else {
+        return eventSources
+      }
+
     case 'RECEIVE_EVENTS':
     case 'RECEIVE_EVENTS':
     case 'RECEIVE_EVENT_ERROR':
     case 'RECEIVE_EVENT_ERROR':
       return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange)
       return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange)
@@ -49,7 +74,7 @@ export function reduceEventSources(eventSources: EventSourceHash, action: Action
 }
 }
 
 
 
 
-function addSources(eventSourceHash: EventSourceHash, sources: EventSource[], fetchRange: DateRange | null, calendar: Calendar): EventSourceHash {
+function addSources(eventSourceHash: EventSourceHash, sources: EventSource[], fetchRange: DateRange | null, pluginHooks: PluginHooks, rawOptions, calendar: Calendar): EventSourceHash {
   let hash: EventSourceHash = {}
   let hash: EventSourceHash = {}
 
 
   for (let source of sources) {
   for (let source of sources) {
@@ -57,7 +82,7 @@ function addSources(eventSourceHash: EventSourceHash, sources: EventSource[], fe
   }
   }
 
 
   if (fetchRange) {
   if (fetchRange) {
-    hash = fetchDirtySources(hash, fetchRange, calendar)
+    hash = fetchDirtySources(hash, fetchRange, pluginHooks, rawOptions, calendar)
   }
   }
 
 
   return { ...eventSourceHash, ...hash }
   return { ...eventSourceHash, ...hash }
@@ -71,24 +96,25 @@ function removeSource(eventSourceHash: EventSourceHash, sourceId: string): Event
 }
 }
 
 
 
 
-function fetchDirtySources(sourceHash: EventSourceHash, fetchRange: DateRange, calendar: Calendar): EventSourceHash {
+function fetchDirtySources(sourceHash: EventSourceHash, fetchRange: DateRange, pluginHooks: PluginHooks, rawOptions, calendar: Calendar): EventSourceHash {
   return fetchSourcesByIds(
   return fetchSourcesByIds(
     sourceHash,
     sourceHash,
     filterHash(sourceHash, function(eventSource) {
     filterHash(sourceHash, function(eventSource) {
-      return isSourceDirty(eventSource, fetchRange, calendar)
+      return isSourceDirty(eventSource, fetchRange, rawOptions, pluginHooks)
     }),
     }),
     fetchRange,
     fetchRange,
+    pluginHooks,
     calendar
     calendar
   )
   )
 }
 }
 
 
 
 
-function isSourceDirty(eventSource: EventSource, fetchRange: DateRange, calendar: Calendar) {
+function isSourceDirty(eventSource: EventSource, fetchRange: DateRange, rawOptions, pluginHooks: PluginHooks) {
 
 
-  if (!doesSourceNeedRange(eventSource, calendar)) {
+  if (!doesSourceNeedRange(eventSource, pluginHooks)) {
     return !eventSource.latestFetchId
     return !eventSource.latestFetchId
   } else {
   } else {
-    return !calendar.opt('lazyFetching') ||
+    return !rawOptions.lazyFetching ||
       !eventSource.fetchRange ||
       !eventSource.fetchRange ||
       eventSource.isFetching || // always cancel outdated in-progress fetches
       eventSource.isFetching || // always cancel outdated in-progress fetches
       fetchRange.start < eventSource.fetchRange.start ||
       fetchRange.start < eventSource.fetchRange.start ||
@@ -101,6 +127,7 @@ function fetchSourcesByIds(
   prevSources: EventSourceHash,
   prevSources: EventSourceHash,
   sourceIdHash: { [sourceId: string]: any },
   sourceIdHash: { [sourceId: string]: any },
   fetchRange: DateRange,
   fetchRange: DateRange,
+  pluginHooks: PluginHooks,
   calendar: Calendar
   calendar: Calendar
 ): EventSourceHash {
 ): EventSourceHash {
   let nextSources: EventSourceHash = {}
   let nextSources: EventSourceHash = {}
@@ -109,7 +136,7 @@ function fetchSourcesByIds(
     let source = prevSources[sourceId]
     let source = prevSources[sourceId]
 
 
     if (sourceIdHash[sourceId]) {
     if (sourceIdHash[sourceId]) {
-      nextSources[sourceId] = fetchSource(source, fetchRange, calendar)
+      nextSources[sourceId] = fetchSource(source, fetchRange, pluginHooks, calendar)
     } else {
     } else {
       nextSources[sourceId] = source
       nextSources[sourceId] = source
     }
     }
@@ -119,8 +146,8 @@ function fetchSourcesByIds(
 }
 }
 
 
 
 
-function fetchSource(eventSource: EventSource, fetchRange: DateRange, calendar: Calendar) {
-  let sourceDef = calendar.pluginSystem.hooks.eventSourceDefs[eventSource.sourceDefId]
+function fetchSource(eventSource: EventSource, fetchRange: DateRange, pluginHooks: PluginHooks, calendar: Calendar) {
+  let sourceDef = pluginHooks.eventSourceDefs[eventSource.sourceDefId]
   let fetchId = guid()
   let fetchId = guid()
 
 
   sourceDef.fetch(
   sourceDef.fetch(
@@ -196,8 +223,28 @@ function receiveResponse(sourceHash: EventSourceHash, sourceId: string, fetchId:
 }
 }
 
 
 
 
-function excludeStaticSources(eventSources: EventSourceHash, calendar: Calendar): EventSourceHash {
+function excludeStaticSources(eventSources: EventSourceHash, pluginHooks: PluginHooks): EventSourceHash {
   return filterHash(eventSources, function(eventSource) {
   return filterHash(eventSources, function(eventSource) {
-    return doesSourceNeedRange(eventSource, calendar)
+    return doesSourceNeedRange(eventSource, pluginHooks)
   })
   })
 }
 }
+
+
+function parseInitialSources(rawOptions, pluginHooks, calendar: Calendar) {
+  let rawSources = rawOptions.eventSources || []
+  let singleRawSource = rawOptions.events
+  let sources = [] // parsed
+
+  if (singleRawSource) {
+    rawSources.unshift(singleRawSource)
+  }
+
+  for (let rawSource of rawSources) {
+    let source = parseEventSource(rawSource, pluginHooks, calendar)
+    if (source) {
+      sources.push(source)
+    }
+  }
+
+  return sources
+}

+ 9 - 3
packages/core/src/reducers/eventStore.ts

@@ -20,8 +20,10 @@ import { DateEnv } from '../datelib/env'
 import { EventUiHash } from '../component/event-ui'
 import { EventUiHash } from '../component/event-ui'
 
 
 
 
-export function reduceEventStore(eventStore: EventStore, action: Action, eventSources: EventSourceHash, dateProfile: DateProfile, calendar: Calendar): EventStore {
+export function reduceEventStore(eventStore: EventStore, action: Action, eventSources: EventSourceHash, dateProfile: DateProfile, dateEnv: DateEnv, prevDateEnv: DateEnv, calendar: Calendar): EventStore {
   switch (action.type) {
   switch (action.type) {
+    case 'INIT':
+      return createEmptyEventStore()
 
 
     case 'RECEIVE_EVENTS': // raw
     case 'RECEIVE_EVENTS': // raw
       return receiveRawEvents(
       return receiveRawEvents(
@@ -54,8 +56,12 @@ export function reduceEventStore(eventStore: EventStore, action: Action, eventSo
         return eventStore
         return eventStore
       }
       }
 
 
-    case 'CHANGE_TIMEZONE':
-      return rezoneDates(eventStore, action.oldDateEnv, calendar.dateEnv)
+    case 'SET_OPTION':
+      if (action.optionName === 'timeZone') {
+        return rezoneDates(eventStore, prevDateEnv, dateEnv)
+      } else {
+        return eventStore
+      }
 
 
     case 'MUTATE_EVENTS':
     case 'MUTATE_EVENTS':
       return applyMutationToRelated(eventStore, action.instanceId, action.mutation, action.fromApi, calendar)
       return applyMutationToRelated(eventStore, action.instanceId, action.mutation, action.fromApi, calendar)

+ 0 - 204
packages/core/src/reducers/main.ts

@@ -1,204 +0,0 @@
-import { Calendar } from '../Calendar'
-import { reduceEventSources } from './eventSources'
-import { reduceEventStore } from './eventStore'
-import { DateProfile, isDateProfilesEqual } from '../DateProfileGenerator'
-import { DateSpan } from '../structs/date-span'
-import { EventInteractionState } from '../interactions/event-interaction-state'
-import { CalendarState, Action } from './types'
-import { EventSourceHash } from '../structs/event-source'
-import { DateMarker } from '../datelib/marker'
-import { rangeContainsMarker } from '../datelib/date-range'
-
-export function reduce(state: CalendarState, action: Action, calendar: Calendar): CalendarState {
-
-  let viewType = reduceViewType(state.viewType, action)
-  let dateProfile = reduceDateProfile(state.dateProfile, action, state.currentDate, viewType, calendar)
-  let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendar)
-
-  let nextState = {
-    ...state,
-    viewType,
-    dateProfile,
-    currentDate: reduceCurrentDate(state.currentDate, action, dateProfile),
-    eventSources,
-    eventStore: reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendar),
-    dateSelection: reduceDateSelection(state.dateSelection, action, calendar),
-    eventSelection: reduceSelectedEvent(state.eventSelection, action),
-    eventDrag: reduceEventDrag(state.eventDrag, action, eventSources, calendar),
-    eventResize: reduceEventResize(state.eventResize, action, eventSources, calendar),
-    eventSourceLoadingLevel: computeLoadingLevel(eventSources),
-    loadingLevel: computeLoadingLevel(eventSources)
-  }
-
-  for (let reducerFunc of calendar.pluginSystem.hooks.reducers) {
-    nextState = reducerFunc(nextState, action, calendar)
-  }
-
-  // console.log(action.type, nextState)
-
-  return nextState
-}
-
-function reduceViewType(currentViewType: string, action: Action): string {
-  switch (action.type) {
-    case 'SET_VIEW_TYPE':
-      return action.viewType
-    default:
-      return currentViewType
-  }
-}
-
-function reduceDateProfile(currentDateProfile: DateProfile | null, action: Action, currentDate: DateMarker, viewType: string, calendar: Calendar): DateProfile {
-  let newDateProfile: DateProfile
-
-  switch (action.type) {
-
-    case 'PREV':
-      newDateProfile = calendar.dateProfileGenerators[viewType].buildPrev(currentDateProfile, currentDate)
-      break
-
-    case 'NEXT':
-      newDateProfile = calendar.dateProfileGenerators[viewType].buildNext(currentDateProfile, currentDate)
-      break
-
-    case 'SET_DATE':
-      if (
-        !currentDateProfile.activeRange ||
-        !rangeContainsMarker(currentDateProfile.currentRange, action.dateMarker)
-      ) {
-        newDateProfile = calendar.dateProfileGenerators[viewType].build(
-          action.dateMarker,
-          undefined,
-          true // forceToValid
-        )
-      }
-      break
-
-    case 'SET_VIEW_TYPE':
-      let generator = calendar.dateProfileGenerators[viewType]
-
-      if (!generator) {
-        throw new Error(
-          viewType ?
-            'The FullCalendar view "' + viewType + '" does not exist. Make sure your plugins are loaded correctly.' :
-            'No available FullCalendar view plugins.'
-        )
-      }
-
-      newDateProfile = generator.build(
-        action.dateMarker || currentDate,
-        undefined,
-        true // forceToValid
-      )
-      break
-  }
-
-  if (
-    newDateProfile &&
-    newDateProfile.isValid &&
-    !(currentDateProfile && isDateProfilesEqual(currentDateProfile, newDateProfile))
-  ) {
-    return newDateProfile
-  } else {
-    return currentDateProfile
-  }
-}
-
-function reduceCurrentDate(currentDate: DateMarker, action: Action, dateProfile: DateProfile): DateMarker {
-  switch (action.type) {
-
-    case 'PREV':
-    case 'NEXT':
-      if (!rangeContainsMarker(dateProfile.currentRange, currentDate)) {
-        return dateProfile.currentRange.start
-      } else {
-        return currentDate
-      }
-
-    case 'SET_DATE':
-    case 'SET_VIEW_TYPE':
-      let newDate = action.dateMarker || currentDate
-      if (dateProfile.activeRange && !rangeContainsMarker(dateProfile.activeRange, newDate)) {
-        return dateProfile.currentRange.start
-      } else {
-        return newDate
-      }
-
-    default:
-      return currentDate
-  }
-}
-
-function reduceDateSelection(currentSelection: DateSpan | null, action: Action, calendar: Calendar) {
-  switch (action.type) {
-    case 'SELECT_DATES':
-      return action.selection
-    case 'UNSELECT_DATES':
-      return null
-    default:
-      return currentSelection
-  }
-}
-
-function reduceSelectedEvent(currentInstanceId: string, action: Action): string {
-  switch (action.type) {
-    case 'SELECT_EVENT':
-      return action.eventInstanceId
-    case 'UNSELECT_EVENT':
-      return ''
-    default:
-      return currentInstanceId
-  }
-}
-
-function reduceEventDrag(currentDrag: EventInteractionState | null, action: Action, sources: EventSourceHash, calendar: Calendar): EventInteractionState | null {
-  switch (action.type) {
-
-    case 'SET_EVENT_DRAG':
-      let newDrag = action.state
-
-      return {
-        affectedEvents: newDrag.affectedEvents,
-        mutatedEvents: newDrag.mutatedEvents,
-        isEvent: newDrag.isEvent
-      }
-
-    case 'UNSET_EVENT_DRAG':
-      return null
-
-    default:
-      return currentDrag
-  }
-}
-
-function reduceEventResize(currentResize: EventInteractionState | null, action: Action, sources: EventSourceHash, calendar: Calendar): EventInteractionState | null {
-  switch (action.type) {
-
-    case 'SET_EVENT_RESIZE':
-      let newResize = action.state
-
-      return {
-        affectedEvents: newResize.affectedEvents,
-        mutatedEvents: newResize.mutatedEvents,
-        isEvent: newResize.isEvent
-      }
-
-    case 'UNSET_EVENT_RESIZE':
-      return null
-
-    default:
-      return currentResize
-  }
-}
-
-function computeLoadingLevel(eventSources: EventSourceHash): number {
-  let cnt = 0
-
-  for (let sourceId in eventSources) {
-    if (eventSources[sourceId].isFetching) {
-      cnt++
-    }
-  }
-
-  return cnt
-}

+ 16 - 0
packages/core/src/reducers/selected-event.ts

@@ -0,0 +1,16 @@
+import { Action } from './types'
+
+
+export function reduceSelectedEvent(currentInstanceId: string, action: Action): string {
+  switch (action.type) {
+    case 'INIT':
+    case 'UNSELECT_EVENT':
+      return ''
+
+    case 'SELECT_EVENT':
+      return action.eventInstanceId
+
+    default:
+      return currentInstanceId
+  }
+}

+ 19 - 4
packages/core/src/reducers/types.ts

@@ -3,12 +3,16 @@ import { DateRange } from '../datelib/date-range'
 import { EventStore } from '../structs/event-store'
 import { EventStore } from '../structs/event-store'
 import { EventMutation } from '../structs/event-mutation'
 import { EventMutation } from '../structs/event-mutation'
 import { EventSource, EventSourceHash, EventSourceError } from '../structs/event-source'
 import { EventSource, EventSourceHash, EventSourceError } from '../structs/event-source'
-import { DateProfile } from '../DateProfileGenerator'
+import { DateProfile, DateProfileGenerator } from '../DateProfileGenerator'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { DateSpan } from '../structs/date-span'
 import { DateSpan } from '../structs/date-span'
 import { DateEnv } from '../datelib/env'
 import { DateEnv } from '../datelib/env'
 import { Calendar } from '../Calendar'
 import { Calendar } from '../Calendar'
 import { DateMarker } from '../datelib/marker'
 import { DateMarker } from '../datelib/marker'
+import { PluginHooks } from '../plugin-system'
+import { RawLocaleMap } from '../datelib/locale'
+import { Theme } from '../theme/Theme'
+import { ViewSpecHash } from '../structs/view-spec'
 
 
 export interface CalendarState {
 export interface CalendarState {
   eventSources: EventSourceHash
   eventSources: EventSourceHash
@@ -22,13 +26,25 @@ export interface CalendarState {
   eventSelection: string
   eventSelection: string
   eventDrag: EventInteractionState | null
   eventDrag: EventInteractionState | null
   eventResize: EventInteractionState | null
   eventResize: EventInteractionState | null
+  options: any
+  optionOverrides: any
+  dynamicOptionOverrides: any
+  dateEnv: DateEnv
+  pluginHooks: PluginHooks
+  availableRawLocales: RawLocaleMap
+  theme: Theme
+  dateProfileGenerator: DateProfileGenerator
+  viewSpecs: ViewSpecHash
 }
 }
 
 
-export type reducerFunc = (state: CalendarState, action: Action, calendar: Calendar) => CalendarState
+export type reducerFunc = (state: CalendarState, action: Action, rawOptions: any, calendar: Calendar) => CalendarState
 
 
 export type Action =
 export type Action =
 
 
-  { type: 'INIT' } | // wont it create another rerender?
+  { type: 'INIT', optionOverrides: object } | // wont it create another rerender?
+
+  { type: 'SET_OPTION', optionName: string, optionValue: any } | // TODO: more strictly type
+  { type: 'MUTATE_OPTIONS', updates: object, removals: string[], isDynamic: boolean } |
 
 
   { type: 'PREV' } |
   { type: 'PREV' } |
   { type: 'NEXT' } |
   { type: 'NEXT' } |
@@ -52,7 +68,6 @@ export type Action =
   { type: 'REMOVE_ALL_EVENT_SOURCES' } |
   { type: 'REMOVE_ALL_EVENT_SOURCES' } |
 
 
   { type: 'FETCH_EVENT_SOURCES', sourceIds?: string[] } | // if no sourceIds, fetch all
   { type: 'FETCH_EVENT_SOURCES', sourceIds?: string[] } | // if no sourceIds, fetch all
-  { type: 'CHANGE_TIMEZONE', oldDateEnv: DateEnv } |
 
 
   { type: 'RECEIVE_EVENTS', sourceId: string, fetchId: string, fetchRange: DateRange | null, rawEvents: EventInput[] } |
   { type: 'RECEIVE_EVENTS', sourceId: string, fetchId: string, fetchRange: DateRange | null, rawEvents: EventInput[] } |
   { type: 'RECEIVE_EVENT_ERROR', sourceId: string, fetchId: string, fetchRange: DateRange | null, error: EventSourceError } | // need all these?
   { type: 'RECEIVE_EVENT_ERROR', sourceId: string, fetchId: string, fetchRange: DateRange | null, error: EventSourceError } | // need all these?

+ 17 - 0
packages/core/src/reducers/view-type.ts

@@ -0,0 +1,17 @@
+import { Action } from './types'
+
+
+export function reduceViewType(viewType: string, action: Action, availableViewHash): string {
+  // for INIT, viewType will have already been set
+
+  switch (action.type) {
+    case 'SET_VIEW_TYPE':
+      return viewType = action.viewType
+  }
+
+  if (!availableViewHash[viewType]) {
+    throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`)
+  }
+
+  return viewType
+}

+ 2 - 2
packages/core/src/structs/event-mutation.ts

@@ -27,7 +27,7 @@ export function applyMutationToEventStore(eventStore: EventStore, eventConfigBas
   for (let defId in eventStore.defs) {
   for (let defId in eventStore.defs) {
     let def = eventStore.defs[defId]
     let def = eventStore.defs[defId]
 
 
-    dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, calendar.pluginSystem.hooks.eventDefMutationAppliers, calendar)
+    dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, calendar.state.pluginHooks.eventDefMutationAppliers, calendar)
   }
   }
 
 
   for (let instanceId in eventStore.instances) {
   for (let instanceId in eventStore.instances) {
@@ -86,7 +86,7 @@ function applyMutationToEventInstance(
   mutation: EventMutation,
   mutation: EventMutation,
   calendar: Calendar
   calendar: Calendar
 ): EventInstance {
 ): EventInstance {
-  let dateEnv = calendar.dateEnv
+  let dateEnv = calendar.state.dateEnv
   let forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true
   let forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true
   let clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false
   let clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false
   let copy = { ...eventInstance } as EventInstance
   let copy = { ...eventInstance } as EventInstance

+ 7 - 4
packages/core/src/structs/event-source.ts

@@ -5,6 +5,7 @@ import { DateRange } from '../datelib/date-range'
 import { EventSourceFunc } from '../event-sources/func-event-source'
 import { EventSourceFunc } from '../event-sources/func-event-source'
 import { EventUi, processUnscopedUiProps } from '../component/event-ui'
 import { EventUi, processUnscopedUiProps } from '../component/event-ui'
 import { ConstraintInput, AllowFunc } from '../validation'
 import { ConstraintInput, AllowFunc } from '../validation'
+import { PluginHooks } from '../plugin-system'
 
 
 /*
 /*
 Parsing and normalization of the EventSource data type, which defines how event data is fetched.
 Parsing and normalization of the EventSource data type, which defines how event data is fetched.
@@ -108,14 +109,15 @@ const SIMPLE_SOURCE_PROPS = {
 }
 }
 
 
 
 
-export function doesSourceNeedRange(eventSource: EventSource, calendar: Calendar) {
-  let defs = calendar.pluginSystem.hooks.eventSourceDefs
+export function doesSourceNeedRange(eventSource: EventSource, pluginHooks: PluginHooks) {
+  let defs = pluginHooks.eventSourceDefs
 
 
   return !defs[eventSource.sourceDefId].ignoreRange
   return !defs[eventSource.sourceDefId].ignoreRange
 }
 }
 
 
-export function parseEventSource(raw: EventSourceInput, calendar: Calendar): EventSource | null {
-  let defs = calendar.pluginSystem.hooks.eventSourceDefs
+
+export function parseEventSource(raw: EventSourceInput, pluginHooks: PluginHooks, calendar: Calendar): EventSource | null {
+  let defs = pluginHooks.eventSourceDefs
 
 
   for (let i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
   for (let i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
     let def = defs[i]
     let def = defs[i]
@@ -137,6 +139,7 @@ export function parseEventSource(raw: EventSourceInput, calendar: Calendar): Eve
   return null
   return null
 }
 }
 
 
+
 function parseEventSourceProps(raw: ExtendedEventSourceInput, meta: object, sourceDefId: number, calendar: Calendar): EventSource {
 function parseEventSourceProps(raw: ExtendedEventSourceInput, meta: object, sourceDefId: number, calendar: Calendar): EventSource {
   let leftovers0 = {}
   let leftovers0 = {}
   let props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0)
   let props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0)

+ 2 - 2
packages/core/src/structs/event-store.ts

@@ -55,7 +55,7 @@ export function eventTupleToStore(tuple: EventTuple, eventStore: EventStore = cr
 }
 }
 
 
 export function expandRecurring(eventStore: EventStore, framingRange: DateRange, calendar: Calendar): EventStore {
 export function expandRecurring(eventStore: EventStore, framingRange: DateRange, calendar: Calendar): EventStore {
-  let dateEnv = calendar.dateEnv
+  let dateEnv = calendar.state.dateEnv
   let { defs, instances } = eventStore
   let { defs, instances } = eventStore
 
 
   // remove existing recurring instances
   // remove existing recurring instances
@@ -75,7 +75,7 @@ export function expandRecurring(eventStore: EventStore, framingRange: DateRange,
           calendar.defaultTimedEventDuration
           calendar.defaultTimedEventDuration
       }
       }
 
 
-      let starts = expandRecurringRanges(def, duration, framingRange, calendar.dateEnv, calendar.pluginSystem.hooks.recurringTypes)
+      let starts = expandRecurringRanges(def, duration, framingRange, calendar.state.dateEnv, calendar.state.pluginHooks.recurringTypes)
 
 
       for (let start of starts) {
       for (let start of starts) {
         let instance = createEventInstance(defId, {
         let instance = createEventInstance(defId, {

+ 6 - 6
packages/core/src/structs/event.ts

@@ -83,8 +83,8 @@ export function parseEvent(raw: EventInput, sourceId: string, calendar: Calendar
   let recurringRes = parseRecurring(
   let recurringRes = parseRecurring(
     raw, // raw, but with single-event stuff stripped out
     raw, // raw, but with single-event stuff stripped out
     defaultAllDay,
     defaultAllDay,
-    calendar.dateEnv,
-    calendar.pluginSystem.hooks.recurringTypes,
+    calendar.state.dateEnv,
+    calendar.state.pluginHooks.recurringTypes,
     leftovers0 // will populate with non-recurring props
     leftovers0 // will populate with non-recurring props
   )
   )
 
 
@@ -129,7 +129,7 @@ export function parseEventDef(raw: EventNonDateInput, sourceId: string, allDay:
   def.allDay = allDay
   def.allDay = allDay
   def.hasEnd = hasEnd
   def.hasEnd = hasEnd
 
 
-  for (let eventDefParser of calendar.pluginSystem.hooks.eventDefParsers) {
+  for (let eventDefParser of calendar.state.pluginHooks.eventDefParsers) {
     let newLeftovers = {}
     let newLeftovers = {}
     eventDefParser(def, leftovers, newLeftovers)
     eventDefParser(def, leftovers, newLeftovers)
     leftovers = newLeftovers
     leftovers = newLeftovers
@@ -173,7 +173,7 @@ function parseSingle(raw: EventInput, defaultAllDay: boolean | null, calendar: C
   let endMeta
   let endMeta
   let endMarker = null
   let endMarker = null
 
 
-  startMeta = calendar.dateEnv.createMarkerMeta(props.start)
+  startMeta = calendar.state.dateEnv.createMarkerMeta(props.start)
 
 
   if (startMeta) {
   if (startMeta) {
     startMarker = startMeta.marker
     startMarker = startMeta.marker
@@ -182,7 +182,7 @@ function parseSingle(raw: EventInput, defaultAllDay: boolean | null, calendar: C
   }
   }
 
 
   if (props.end != null) {
   if (props.end != null) {
-    endMeta = calendar.dateEnv.createMarkerMeta(props.end)
+    endMeta = calendar.state.dateEnv.createMarkerMeta(props.end)
   }
   }
 
 
   if (allDay == null) {
   if (allDay == null) {
@@ -216,7 +216,7 @@ function parseSingle(raw: EventInput, defaultAllDay: boolean | null, calendar: C
   } else if (!allowOpenRange) {
   } else if (!allowOpenRange) {
     hasEnd = calendar.opt('forceEventDuration') || false
     hasEnd = calendar.opt('forceEventDuration') || false
 
 
-    endMarker = calendar.dateEnv.add(
+    endMarker = calendar.state.dateEnv.add(
       startMarker,
       startMarker,
       allDay ?
       allDay ?
         calendar.defaultAllDayEventDuration :
         calendar.defaultAllDayEventDuration :

+ 1 - 1
packages/core/src/structs/view-config.tsx

@@ -71,7 +71,7 @@ function createViewHookComponent(options) {
     return (
     return (
       <ComponentContextType.Consumer>
       <ComponentContextType.Consumer>
         {(context: ComponentContext) => (
         {(context: ComponentContext) => (
-          <ViewRoot viewSpec={viewProps.viewSpec}>
+          <ViewRoot viewSpec={context.viewSpec}>
             {(rootElRef, viewClassNames) => {
             {(rootElRef, viewClassNames) => {
               let hookProps = { ...viewProps, nextDayThreshold: context.nextDayThreshold }
               let hookProps = { ...viewProps, nextDayThreshold: context.nextDayThreshold }
               return (
               return (

+ 20 - 20
packages/core/src/structs/view-spec.ts

@@ -1,6 +1,6 @@
 import { ViewDef, compileViewDefs } from './view-def'
 import { ViewDef, compileViewDefs } from './view-def'
 import { Duration, createDuration, greatestDurationDenominator, getWeeksFromInput } from '../datelib/duration'
 import { Duration, createDuration, greatestDurationDenominator, getWeeksFromInput } from '../datelib/duration'
-import { OptionsManager } from '../OptionsManager'
+import { compileOptionsAdvanced } from '../OptionsManager'
 import { mapHash } from '../util/object'
 import { mapHash } from '../util/object'
 import { globalDefaults } from '../options'
 import { globalDefaults } from '../options'
 import { ViewConfigInputHash, parseViewConfigs, ViewConfigHash, ViewComponentType } from './view-config'
 import { ViewConfigInputHash, parseViewConfigs, ViewConfigHash, ViewComponentType } from './view-config'
@@ -26,22 +26,24 @@ export interface ViewSpec {
 
 
 export type ViewSpecHash = { [viewType: string]: ViewSpec }
 export type ViewSpecHash = { [viewType: string]: ViewSpec }
 
 
-export function buildViewSpecs(defaultInputs: ViewConfigInputHash, optionsManager: OptionsManager): ViewSpecHash {
+
+export function buildViewSpecs(defaultInputs: ViewConfigInputHash, optionOverrides, dynamicOptionOverrides): ViewSpecHash {
   let defaultConfigs = parseViewConfigs(defaultInputs)
   let defaultConfigs = parseViewConfigs(defaultInputs)
-  let overrideConfigs = parseViewConfigs(optionsManager.overrides.views)
+  let overrideConfigs = parseViewConfigs(optionOverrides.views)
   let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs)
   let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs)
 
 
   return mapHash(viewDefs, function(viewDef) {
   return mapHash(viewDefs, function(viewDef) {
-    return buildViewSpec(viewDef, overrideConfigs, optionsManager)
+    return buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides)
   })
   })
 }
 }
 
 
-function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, optionsManager: OptionsManager): ViewSpec {
+
+function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, optionOverrides, dynamicOptionOverrides): ViewSpec {
   let durationInput =
   let durationInput =
     viewDef.overrides.duration ||
     viewDef.overrides.duration ||
     viewDef.defaults.duration ||
     viewDef.defaults.duration ||
-    optionsManager.dynamicOverrides.duration ||
-    optionsManager.overrides.duration
+    dynamicOptionOverrides.duration ||
+    optionOverrides.duration
 
 
   let duration = null
   let duration = null
   let durationUnit = ''
   let durationUnit = ''
@@ -83,30 +85,28 @@ function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, option
     }
     }
   }
   }
 
 
+  let { combined, localeDefaults } = compileOptionsAdvanced(
+    optionOverrides,
+    dynamicOptionOverrides,
+    viewDef.defaults,
+    { ...singleUnitOverrides, ...viewDef.overrides }
+  )
+
   return {
   return {
     type: viewDef.type,
     type: viewDef.type,
     component: viewDef.component,
     component: viewDef.component,
     duration,
     duration,
     durationUnit,
     durationUnit,
     singleUnit,
     singleUnit,
-
-    options: {
-      ...globalDefaults,
-      ...viewDef.defaults,
-      ...optionsManager.localeDefaults,
-      ...optionsManager.overrides,
-      ...singleUnitOverrides,
-      ...viewDef.overrides,
-      ...optionsManager.dynamicOverrides
-    },
+    options: combined,
 
 
     buttonTextOverride:
     buttonTextOverride:
-      queryButtonText(optionsManager.dynamicOverrides) ||
-      queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence
+      queryButtonText(dynamicOptionOverrides) ||
+      queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence
       viewDef.overrides.buttonText, // `buttonText` for view-specific options is a string
       viewDef.overrides.buttonText, // `buttonText` for view-specific options is a string
 
 
     buttonTextDefault:
     buttonTextDefault:
-      queryButtonText(optionsManager.localeDefaults) ||
+      queryButtonText(localeDefaults) ||
       viewDef.defaults.buttonText ||
       viewDef.defaults.buttonText ||
       queryButtonText(globalDefaults) ||
       queryButtonText(globalDefaults) ||
       viewDef.type // fall back to given view name
       viewDef.type // fall back to given view name

+ 5 - 5
packages/core/src/toolbar-parse.ts

@@ -31,11 +31,11 @@ function parseToolbar(raw, theme: Theme, isRtl: boolean, calendar: Calendar, vie
 BAD: querying icons and text here. should be done at render time
 BAD: querying icons and text here. should be done at render time
 */
 */
 function parseSection(sectionStr: string, theme: Theme, isRtl: boolean, calendar: Calendar, viewsWithButtons: string[]): ToolbarWidget[][] {
 function parseSection(sectionStr: string, theme: Theme, isRtl: boolean, calendar: Calendar, viewsWithButtons: string[]): ToolbarWidget[][] {
-  let optionsManager = calendar.optionsManager
-  let viewSpecs = calendar.viewSpecs
-  let calendarCustomButtons = optionsManager.computed.customButtons || {}
-  let calendarButtonTextOverrides = optionsManager.overrides.buttonText || {}
-  let calendarButtonText = optionsManager.computed.buttonText || {}
+  let calendarState = calendar.state
+  let { viewSpecs } = calendarState
+  let calendarCustomButtons = calendarState.options.customButtons || {}
+  let calendarButtonTextOverrides = calendarState.optionOverrides.buttonText || {}
+  let calendarButtonText = calendarState.options.buttonText || {}
   let sectionSubstrs = sectionStr ? sectionStr.split(' ') : []
   let sectionSubstrs = sectionStr ? sectionStr.split(' ') : []
 
 
   return sectionSubstrs.map((buttonGroupStr, i): ToolbarWidget[] => {
   return sectionSubstrs.map((buttonGroupStr, i): ToolbarWidget[] => {

+ 1 - 1
packages/core/src/validation.ts

@@ -43,7 +43,7 @@ function isNewPropsValid(newProps, calendar: Calendar) {
     ...newProps
     ...newProps
   }
   }
 
 
-  return (calendar.pluginSystem.hooks.isPropsValid || isPropsValid)(props, calendar)
+  return (calendar.state.pluginHooks.isPropsValid || isPropsValid)(props, calendar)
 }
 }
 
 
 export function isPropsValid(state: SplittableProps, calendar: Calendar, dateSpanMeta = {}, filterConfig?): boolean {
 export function isPropsValid(state: SplittableProps, calendar: Calendar, dateSpanMeta = {}, filterConfig?): boolean {

+ 2 - 5
packages/daygrid/src/DayTable.tsx

@@ -1,6 +1,5 @@
 import {
 import {
   h, createRef, VNode,
   h, createRef, VNode,
-  DateProfile,
   EventStore,
   EventStore,
   EventUiHash,
   EventUiHash,
   DateSpan,
   DateSpan,
@@ -20,7 +19,6 @@ import { TableSeg } from './TableSeg'
 
 
 
 
 export interface DayTableProps {
 export interface DayTableProps {
-  dateProfile: DateProfile
   dayTableModel: DayTableModel
   dayTableModel: DayTableModel
   nextDayThreshold: Duration
   nextDayThreshold: Duration
   businessHours: EventStore
   businessHours: EventStore
@@ -49,15 +47,14 @@ export class DayTable extends DateComponent<DayTableProps, ComponentContext> {
 
 
 
 
   render(props: DayTableProps, state: {}, context: ComponentContext) {
   render(props: DayTableProps, state: {}, context: ComponentContext) {
-    let { dateProfile, dayTableModel } = props
+    let { dayTableModel } = props
 
 
     return (
     return (
       <Table
       <Table
         ref={this.tableRef}
         ref={this.tableRef}
         elRef={this.handleRootEl}
         elRef={this.handleRootEl}
-        { ...this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, context.calendar, dayTableModel) }
+        { ...this.slicer.sliceProps(props, context.dateProfile, props.nextDayThreshold, context.calendar, dayTableModel) }
         cells={dayTableModel.cells}
         cells={dayTableModel.cells}
-        dateProfile={dateProfile}
         colGroupNode={props.colGroupNode}
         colGroupNode={props.colGroupNode}
         tableMinWidth={props.tableMinWidth}
         tableMinWidth={props.tableMinWidth}
         renderRowIntro={props.renderRowIntro}
         renderRowIntro={props.renderRowIntro}

+ 2 - 5
packages/daygrid/src/DayTableView.tsx

@@ -22,14 +22,12 @@ export class DayTableView extends TableView {
 
 
 
 
   render(props: ViewProps, state: {}, context: ComponentContext) {
   render(props: ViewProps, state: {}, context: ComponentContext) {
-    let { options } = context
-    let { dateProfile } = props
-    let dayTableModel = this.buildDayTableModel(dateProfile, props.dateProfileGenerator)
+    let { options, dateProfile, dateProfileGenerator } = context
+    let dayTableModel = this.buildDayTableModel(dateProfile, dateProfileGenerator)
 
 
     let headerContent = options.dayHeaders &&
     let headerContent = options.dayHeaders &&
       <DayHeader
       <DayHeader
         ref={this.headerRef}
         ref={this.headerRef}
-        dateProfile={dateProfile}
         dates={dayTableModel.headerDates}
         dates={dayTableModel.headerDates}
         datesRepDistinctDays={dayTableModel.rowCnt === 1}
         datesRepDistinctDays={dayTableModel.rowCnt === 1}
       />
       />
@@ -37,7 +35,6 @@ export class DayTableView extends TableView {
     let bodyContent = (contentArg: ChunkContentCallbackArgs) => (
     let bodyContent = (contentArg: ChunkContentCallbackArgs) => (
       <DayTable
       <DayTable
         ref={this.tableRef}
         ref={this.tableRef}
-        dateProfile={dateProfile}
         dayTableModel={dayTableModel}
         dayTableModel={dayTableModel}
         businessHours={props.businessHours}
         businessHours={props.businessHours}
         dateSelection={props.dateSelection}
         dateSelection={props.dateSelection}

+ 3 - 5
packages/daygrid/src/Table.tsx

@@ -15,7 +15,6 @@ import {
   DateRange,
   DateRange,
   NowTimer,
   NowTimer,
   DateMarker,
   DateMarker,
-  DateProfile,
   EventApi
   EventApi
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import { TableSeg, splitSegsByRow, splitInteractionByRow } from './TableSeg'
 import { TableSeg, splitSegsByRow, splitInteractionByRow } from './TableSeg'
@@ -27,7 +26,6 @@ import { MorePopover } from './MorePopover'
 export interface TableProps {
 export interface TableProps {
   elRef?: Ref<HTMLDivElement>
   elRef?: Ref<HTMLDivElement>
   cells: TableCellModel[][] // cells-BY-ROW
   cells: TableCellModel[][] // cells-BY-ROW
-  dateProfile: DateProfile
   renderRowIntro?: () => VNode
   renderRowIntro?: () => VNode
   colGroupNode: VNode
   colGroupNode: VNode
   tableMinWidth: CssDimValue
   tableMinWidth: CssDimValue
@@ -129,7 +127,7 @@ export class Table extends DateComponent<TableProps, TableState> {
                   showDayNumbers={rowCnt > 1}
                   showDayNumbers={rowCnt > 1}
                   showWeekNumbers={props.showWeekNumbers}
                   showWeekNumbers={props.showWeekNumbers}
                   todayRange={todayRange}
                   todayRange={todayRange}
-                  dateProfile={props.dateProfile}
+                  dateProfile={context.dateProfile}
                   cells={cells}
                   cells={cells}
                   renderIntro={props.renderRowIntro}
                   renderIntro={props.renderRowIntro}
                   businessHourSegs={businessHourSegsByRow[row]}
                   businessHourSegs={businessHourSegsByRow[row]}
@@ -176,7 +174,7 @@ export class Table extends DateComponent<TableProps, TableState> {
 
 
 
 
   handleMoreLinkClick = (arg: MoreLinkArg) => { // TODO: bad names "more link click" versus "more click"
   handleMoreLinkClick = (arg: MoreLinkArg) => { // TODO: bad names "more link click" versus "more click"
-    let { calendar, view, options, dateEnv } = this.context
+    let { calendar, viewApi, options, dateEnv } = this.context
     let clickOption = options.moreLinkClick
     let clickOption = options.moreLinkClick
 
 
     function segForPublic(seg: TableSeg) {
     function segForPublic(seg: TableSeg) {
@@ -201,7 +199,7 @@ export class Table extends DateComponent<TableProps, TableState> {
           allSegs: arg.allSegs.map(segForPublic),
           allSegs: arg.allSegs.map(segForPublic),
           hiddenSegs: arg.hiddenSegs.map(segForPublic),
           hiddenSegs: arg.hiddenSegs.map(segForPublic),
           jsEvent: arg.ev as MouseEvent, // TODO: better
           jsEvent: arg.ev as MouseEvent, // TODO: better
-          view
+          view: viewApi
         }
         }
       ])
       ])
     }
     }

+ 1 - 7
packages/daygrid/src/TableCell.tsx

@@ -6,7 +6,6 @@ import {
   DateComponent,
   DateComponent,
   ComponentContext,
   ComponentContext,
   CssDimValue,
   CssDimValue,
-  DateProfile,
   DateRange,
   DateRange,
   buildNavLinkData,
   buildNavLinkData,
   DayCellHookProps,
   DayCellHookProps,
@@ -38,7 +37,6 @@ export interface TableCellProps {
   moreMarginTop: number
   moreMarginTop: number
   showDayNumber: boolean
   showDayNumber: boolean
   showWeekNumber: boolean
   showWeekNumber: boolean
-  dateProfile: DateProfile
   todayRange: DateRange
   todayRange: DateRange
   buildMoreLinkText: (num: number) => string
   buildMoreLinkText: (num: number) => string
   onMoreClick?: (arg: MoreLinkArg) => void
   onMoreClick?: (arg: MoreLinkArg) => void
@@ -84,7 +82,6 @@ export class TableCell extends DateComponent<TableCellProps> {
       <DayCellRoot
       <DayCellRoot
         date={date}
         date={date}
         todayRange={props.todayRange}
         todayRange={props.todayRange}
-        dateProfile={props.dateProfile}
         showDayNumber={props.showDayNumber}
         showDayNumber={props.showDayNumber}
         extraHookProps={props.extraHookProps}
         extraHookProps={props.extraHookProps}
         elRef={props.elRef}
         elRef={props.elRef}
@@ -114,7 +111,6 @@ export class TableCell extends DateComponent<TableCellProps> {
                 <TableCellTop
                 <TableCellTop
                   date={date}
                   date={date}
                   showDayNumber={props.showDayNumber}
                   showDayNumber={props.showDayNumber}
-                  dateProfile={props.dateProfile}
                   todayRange={props.todayRange}
                   todayRange={props.todayRange}
                   extraHookProps={props.extraHookProps}
                   extraHookProps={props.extraHookProps}
                 />
                 />
@@ -128,7 +124,7 @@ export class TableCell extends DateComponent<TableCellProps> {
                 {Boolean(props.moreCnt) &&
                 {Boolean(props.moreCnt) &&
                   <div class='fc-daygrid-day-bottom' style={{ marginTop: props.moreMarginTop }}>
                   <div class='fc-daygrid-day-bottom' style={{ marginTop: props.moreMarginTop }}>
                     <RenderHook name='moreLink'
                     <RenderHook name='moreLink'
-                      hookProps={{ num: props.moreCnt, text: props.buildMoreLinkText(props.moreCnt), view: context.view }}
+                      hookProps={{ num: props.moreCnt, text: props.buildMoreLinkText(props.moreCnt), view: context.viewApi }}
                       defaultContent={renderMoreLinkInner}
                       defaultContent={renderMoreLinkInner}
                     >
                     >
                       {(rootElRef, classNames, innerElRef, innerContent) => (
                       {(rootElRef, classNames, innerElRef, innerContent) => (
@@ -218,7 +214,6 @@ function resliceDaySegs(segs, dayDate) {
 interface TableCellTopProps {
 interface TableCellTopProps {
   date: DateMarker
   date: DateMarker
   showDayNumber: boolean
   showDayNumber: boolean
-  dateProfile: DateProfile
   todayRange: DateRange
   todayRange: DateRange
   extraHookProps?: object
   extraHookProps?: object
 }
 }
@@ -230,7 +225,6 @@ class TableCellTop extends BaseComponent<TableCellTopProps> {
       <DayCellContent
       <DayCellContent
         date={props.date}
         date={props.date}
         todayRange={props.todayRange}
         todayRange={props.todayRange}
-        dateProfile={props.dateProfile}
         showDayNumber={props.showDayNumber}
         showDayNumber={props.showDayNumber}
         extraHookProps={props.extraHookProps}
         extraHookProps={props.extraHookProps}
         defaultContent={renderTopInner}
         defaultContent={renderTopInner}

+ 0 - 1
packages/daygrid/src/TableRow.tsx

@@ -129,7 +129,6 @@ export class TableRow extends DateComponent<TableRowProps, TableRowState> {
               date={cell.date}
               date={cell.date}
               showDayNumber={props.showDayNumbers || showWeekNumber /* for spacing, we need to force day-numbers if week numbers */}
               showDayNumber={props.showDayNumbers || showWeekNumber /* for spacing, we need to force day-numbers if week numbers */}
               showWeekNumber={showWeekNumber}
               showWeekNumber={showWeekNumber}
-              dateProfile={props.dateProfile}
               todayRange={props.todayRange}
               todayRange={props.todayRange}
               extraHookProps={cell.extraHookProps}
               extraHookProps={cell.extraHookProps}
               extraDataAttrs={cell.extraDataAttrs}
               extraDataAttrs={cell.extraDataAttrs}

+ 2 - 2
packages/daygrid/src/TableView.tsx

@@ -53,7 +53,7 @@ export abstract class TableView<State={}> extends DateComponent<ViewProps, State
     })
     })
 
 
     return (
     return (
-      <ViewRoot viewSpec={props.viewSpec}>
+      <ViewRoot viewSpec={context.viewSpec}>
         {(rootElRef, classNames) => (
         {(rootElRef, classNames) => (
           <div ref={rootElRef} class={[ 'fc-daygrid' ].concat(classNames).join(' ')}>
           <div ref={rootElRef} class={[ 'fc-daygrid' ].concat(classNames).join(' ')}>
             <SimpleScrollGrid
             <SimpleScrollGrid
@@ -115,7 +115,7 @@ export abstract class TableView<State={}> extends DateComponent<ViewProps, State
     }
     }
 
 
     return (
     return (
-      <ViewRoot viewSpec={props.viewSpec}>
+      <ViewRoot viewSpec={context.viewSpec}>
         {(rootElRef, classNames) => (
         {(rootElRef, classNames) => (
           <div ref={rootElRef} class={[ 'fc-daygrid' ].concat(classNames).join(' ')}>
           <div ref={rootElRef} class={[ 'fc-daygrid' ].concat(classNames).join(' ')}>
             <ScrollGrid
             <ScrollGrid

+ 1 - 1
packages/google-calendar/src/main.ts

@@ -66,7 +66,7 @@ let eventSourceDef: EventSourceDef = {
         arg.range,
         arg.range,
         apiKey,
         apiKey,
         meta.data,
         meta.data,
-        calendar.dateEnv
+        calendar.state.dateEnv
       )
       )
 
 
       requestJson('GET', url, requestParams, function(body, xhr) {
       requestJson('GET', url, requestParams, function(body, xhr) {

+ 4 - 4
packages/interaction/src/interactions-external/ExternalElementDragging.ts

@@ -127,7 +127,7 @@ export class ExternalElementDragging {
 
 
     if (receivingCalendar && droppableEvent) {
     if (receivingCalendar && droppableEvent) {
       let finalHit = this.hitDragging.finalHit!
       let finalHit = this.hitDragging.finalHit!
-      let finalView = finalHit.component.context.view
+      let finalView = finalHit.component.context.viewApi
       let dragMeta = this.dragMeta!
       let dragMeta = this.dragMeta!
       let arg = {
       let arg = {
         ...receivingCalendar.buildDatePointApi(finalHit.dateSpan),
         ...receivingCalendar.buildDatePointApi(finalHit.dateSpan),
@@ -207,7 +207,7 @@ export class ExternalElementDragging {
 function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, calendar: Calendar): EventTuple {
 function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, calendar: Calendar): EventTuple {
   let defProps = { ...dragMeta.leftoverProps }
   let defProps = { ...dragMeta.leftoverProps }
 
 
-  for (let transform of calendar.pluginSystem.hooks.externalDefTransforms) {
+  for (let transform of calendar.state.pluginHooks.externalDefTransforms) {
     __assign(defProps, transform(dateSpan, dragMeta))
     __assign(defProps, transform(dateSpan, dragMeta))
   }
   }
 
 
@@ -224,11 +224,11 @@ function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, calenda
   // only rely on time info if drop zone is all-day,
   // only rely on time info if drop zone is all-day,
   // otherwise, we already know the time
   // otherwise, we already know the time
   if (dateSpan.allDay && dragMeta.startTime) {
   if (dateSpan.allDay && dragMeta.startTime) {
-    start = calendar.dateEnv.add(start, dragMeta.startTime)
+    start = calendar.state.dateEnv.add(start, dragMeta.startTime)
   }
   }
 
 
   let end = dragMeta.duration ?
   let end = dragMeta.duration ?
-    calendar.dateEnv.add(start, dragMeta.duration) :
+    calendar.state.dateEnv.add(start, dragMeta.duration) :
     calendar.getDefaultEventEnd(dateSpan.allDay, start)
     calendar.getDefaultEventEnd(dateSpan.allDay, start)
 
 
   let instance = createEventInstance(def.defId, { start, end })
   let instance = createEventInstance(def.defId, { start, end })

+ 2 - 2
packages/interaction/src/interactions/DateClicking.ts

@@ -39,7 +39,7 @@ export class DateClicking extends Interaction {
   // won't even fire if moving was ignored
   // won't even fire if moving was ignored
   handleDragEnd = (ev: PointerDragEvent) => {
   handleDragEnd = (ev: PointerDragEvent) => {
     let { component } = this
     let { component } = this
-    let { calendar, view } = component.context
+    let { calendar, viewApi } = component.context
     let { pointer } = this.dragging
     let { pointer } = this.dragging
 
 
     if (!pointer.wasTouchScroll) {
     if (!pointer.wasTouchScroll) {
@@ -49,7 +49,7 @@ export class DateClicking extends Interaction {
         calendar.triggerDateClick(
         calendar.triggerDateClick(
           initialHit.dateSpan,
           initialHit.dateSpan,
           initialHit.dayEl,
           initialHit.dayEl,
-          view,
+          viewApi,
           ev.origEvent
           ev.origEvent
         )
         )
       }
       }

+ 6 - 6
packages/interaction/src/interactions/EventDragging.ts

@@ -122,7 +122,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
           el: this.subjectEl,
           el: this.subjectEl,
           event: new EventApi(initialCalendar, eventRange.def, eventRange.instance),
           event: new EventApi(initialCalendar, eventRange.def, eventRange.instance),
           jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
           jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
-          view: context.view
+          view: context.viewApi
         }
         }
       ])
       ])
     }
     }
@@ -158,7 +158,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
         initialCalendar === receivingCalendar ||
         initialCalendar === receivingCalendar ||
         receivingOptions.editable && receivingOptions.droppable
         receivingOptions.editable && receivingOptions.droppable
       ) {
       ) {
-        mutation = computeEventMutation(initialHit, hit, receivingCalendar.pluginSystem.hooks.eventDragMutationMassagers)
+        mutation = computeEventMutation(initialHit, hit, receivingCalendar.state.pluginHooks.eventDragMutationMassagers)
 
 
         if (mutation) {
         if (mutation) {
           mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingCalendar.eventUiBases, mutation, receivingCalendar)
           mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingCalendar.eventUiBases, mutation, receivingCalendar)
@@ -220,7 +220,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
     if (this.isDragging) {
     if (this.isDragging) {
       let { context } = this.component
       let { context } = this.component
       let initialCalendar = context.calendar
       let initialCalendar = context.calendar
-      let initialView = context.view
+      let initialView = context.viewApi
       let { receivingCalendar, validMutation } = this
       let { receivingCalendar, validMutation } = this
       let eventDef = this.eventRange!.def
       let eventDef = this.eventRange!.def
       let eventInstance = this.eventRange!.instance
       let eventInstance = this.eventRange!.instance
@@ -252,7 +252,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
 
 
           let transformed: ReturnType<EventDropTransformers> = {}
           let transformed: ReturnType<EventDropTransformers> = {}
 
 
-          for (let transformer of initialCalendar.pluginSystem.hooks.eventDropTransformers) {
+          for (let transformer of initialCalendar.state.pluginHooks.eventDropTransformers) {
             __assign(transformed, transformer(validMutation, initialCalendar))
             __assign(transformed, transformer(validMutation, initialCalendar))
           }
           }
 
 
@@ -310,7 +310,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
             ...receivingCalendar.buildDatePointApi(finalHit.dateSpan),
             ...receivingCalendar.buildDatePointApi(finalHit.dateSpan),
             draggedEl: ev.subjectEl as HTMLElement,
             draggedEl: ev.subjectEl as HTMLElement,
             jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
             jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
-            view: finalHit.component.context.view
+            view: finalHit.component.context.viewApi
           }
           }
           receivingCalendar.publiclyTrigger('drop', [ dropArg ])
           receivingCalendar.publiclyTrigger('drop', [ dropArg ])
 
 
@@ -322,7 +322,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
                 mutatedRelevantEvents.defs[eventDef.defId],
                 mutatedRelevantEvents.defs[eventDef.defId],
                 mutatedRelevantEvents.instances[eventInstance.instanceId]
                 mutatedRelevantEvents.instances[eventInstance.instanceId]
               ),
               ),
-              view: finalHit.component.context.view
+              view: finalHit.component.context.viewApi
             }
             }
           ])
           ])
         }
         }

+ 5 - 5
packages/interaction/src/interactions/EventResizing.ts

@@ -67,7 +67,7 @@ export class EventResizing extends Interaction {
   }
   }
 
 
   handleDragStart = (ev: PointerDragEvent) => {
   handleDragStart = (ev: PointerDragEvent) => {
-    let { calendar, view } = this.component.context
+    let { calendar, viewApi } = this.component.context
     let eventRange = this.eventRange!
     let eventRange = this.eventRange!
 
 
     this.relevantEvents = getRelevantEvents(
     this.relevantEvents = getRelevantEvents(
@@ -85,7 +85,7 @@ export class EventResizing extends Interaction {
         el: segEl,
         el: segEl,
         event: new EventApi(calendar, eventRange.def, eventRange.instance),
         event: new EventApi(calendar, eventRange.def, eventRange.instance),
         jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
         jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
-        view
+        view: viewApi
       }
       }
     ])
     ])
   }
   }
@@ -154,7 +154,7 @@ export class EventResizing extends Interaction {
   }
   }
 
 
   handleDragEnd = (ev: PointerDragEvent) => {
   handleDragEnd = (ev: PointerDragEvent) => {
-    let { calendar, view } = this.component.context
+    let { calendar, viewApi } = this.component.context
     let eventDef = this.eventRange!.def
     let eventDef = this.eventRange!.def
     let eventInstance = this.eventRange!.instance
     let eventInstance = this.eventRange!.instance
     let eventApi = new EventApi(calendar, eventDef, eventInstance)
     let eventApi = new EventApi(calendar, eventDef, eventInstance)
@@ -166,7 +166,7 @@ export class EventResizing extends Interaction {
         el: this.draggingSegEl,
         el: this.draggingSegEl,
         event: eventApi,
         event: eventApi,
         jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
         jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
-        view
+        view: viewApi
       }
       }
     ])
     ])
 
 
@@ -194,7 +194,7 @@ export class EventResizing extends Interaction {
             })
             })
           },
           },
           jsEvent: ev.origEvent,
           jsEvent: ev.origEvent,
-          view
+          view: viewApi
         }
         }
       ])
       ])
 
 

+ 2 - 2
packages/interaction/src/interactions/HitDragging.ts

@@ -187,8 +187,8 @@ export class HitDragging {
             hit &&
             hit &&
             (
             (
               // make sure the hit is within activeRange, meaning it's not a deal cell
               // make sure the hit is within activeRange, meaning it's not a deal cell
-              !component.props.dateProfile || // hack for MorePopover
-              rangeContainsRange(component.props.dateProfile.activeRange, hit.dateSpan.range)
+              !component.context.dateProfile || // hack for MorePopover
+              rangeContainsRange(component.context.dateProfile.activeRange, hit.dateSpan.range)
             ) &&
             ) &&
             (!bestHit || hit.layer > bestHit.layer)
             (!bestHit || hit.layer > bestHit.layer)
           ) {
           ) {

+ 4 - 4
packages/list/src/ListView.tsx

@@ -43,11 +43,11 @@ export class ListView extends DateComponent<ViewProps> {
       context.options.stickyHeaderDates !== false ? 'fc-list-sticky' : ''
       context.options.stickyHeaderDates !== false ? 'fc-list-sticky' : ''
     ]
     ]
 
 
-    let { dayDates, dayRanges } = this.computeDateVars(props.dateProfile)
+    let { dayDates, dayRanges } = this.computeDateVars(context.dateProfile)
     let eventSegs = this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges)
     let eventSegs = this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges)
 
 
     return (
     return (
-      <ViewRoot viewSpec={props.viewSpec} elRef={this.setRootEl}>
+      <ViewRoot viewSpec={context.viewSpec} elRef={this.setRootEl}>
         {(rootElRef, classNames) => (
         {(rootElRef, classNames) => (
           <div ref={rootElRef} class={extraClassNames.concat(classNames).join(' ')}>
           <div ref={rootElRef} class={extraClassNames.concat(classNames).join(' ')}>
             <Scroller
             <Scroller
@@ -82,7 +82,7 @@ export class ListView extends DateComponent<ViewProps> {
     let { context } = this
     let { context } = this
     let hookProps = {
     let hookProps = {
       text: context.options.noEventsText,
       text: context.options.noEventsText,
-      view: context.view
+      view: context.viewApi
     }
     }
 
 
     return (
     return (
@@ -152,7 +152,7 @@ export class ListView extends DateComponent<ViewProps> {
       sliceEventStore(
       sliceEventStore(
         eventStore,
         eventStore,
         eventUiBases,
         eventUiBases,
-        this.props.dateProfile.activeRange,
+        this.context.dateProfile.activeRange,
         this.context.nextDayThreshold
         this.context.nextDayThreshold
       ).fg,
       ).fg,
       dayRanges
       dayRanges

+ 1 - 1
packages/list/src/ListViewEventRow.tsx

@@ -122,7 +122,7 @@ function buildTimeContent(seg: Seg, timeFormat: DateFormatter, context: Componen
     if (doAllDay) {
     if (doAllDay) {
       let hookProps = {
       let hookProps = {
         text: context.options.allDayText,
         text: context.options.allDayText,
-        view: context.view
+        view: context.viewApi
       }
       }
 
 
       return (
       return (

+ 1 - 1
packages/list/src/ListViewHeaderRow.tsx

@@ -33,7 +33,7 @@ export class ListViewHeaderRow extends BaseComponent<ListViewHeaderRowProps> {
 
 
     let hookProps: HookProps = {
     let hookProps: HookProps = {
       date: dateEnv.toDate(dayDate),
       date: dateEnv.toDate(dayDate),
-      view: context.view,
+      view: context.viewApi,
       text,
       text,
       sideText,
       sideText,
       navLinkData,
       navLinkData,

+ 3 - 3
packages/luxon/src/main.ts

@@ -8,8 +8,8 @@ export function toLuxonDateTime(date: Date, calendar: Calendar): LuxonDateTime {
   }
   }
 
 
   return LuxonDateTime.fromJSDate(date, {
   return LuxonDateTime.fromJSDate(date, {
-    zone: calendar.dateEnv.timeZone,
-    locale: calendar.dateEnv.locale.codes[0]
+    zone: calendar.state.dateEnv.timeZone,
+    locale: calendar.state.dateEnv.locale.codes[0]
   })
   })
 }
 }
 
 
@@ -21,7 +21,7 @@ export function toLuxonDuration(duration: Duration, calendar: Calendar): LuxonDu
 
 
   return LuxonDuration.fromObject({
   return LuxonDuration.fromObject({
     ...duration,
     ...duration,
-    locale: calendar.dateEnv.locale.codes[0]
+    locale: calendar.state.dateEnv.locale.codes[0]
   })
   })
 }
 }
 
 

+ 2 - 2
packages/moment/src/main.ts

@@ -10,9 +10,9 @@ export function toMoment(date: Date, calendar: Calendar): moment.Moment {
 
 
   return convertToMoment(
   return convertToMoment(
     date,
     date,
-    calendar.dateEnv.timeZone,
+    calendar.state.dateEnv.timeZone,
     null,
     null,
-    calendar.dateEnv.locale.codes[0]
+    calendar.state.dateEnv.locale.codes[0]
   )
   )
 }
 }
 
 

+ 2 - 4
packages/timegrid/src/DayTimeCols.tsx

@@ -24,7 +24,6 @@ import { TimeSlatMeta } from './TimeColsSlats'
 
 
 
 
 export interface DayTimeColsProps {
 export interface DayTimeColsProps {
-  dateProfile: DateProfile | null
   dayTableModel: DayTableModel
   dayTableModel: DayTableModel
   axis: boolean
   axis: boolean
   slotDuration: Duration
   slotDuration: Duration
@@ -54,8 +53,8 @@ export class DayTimeCols extends DateComponent<DayTimeColsProps> {
 
 
 
 
   render(props: DayTimeColsProps, state: {}, context: ComponentContext) {
   render(props: DayTimeColsProps, state: {}, context: ComponentContext) {
-    let { dateEnv, options, calendar } = context
-    let { dateProfile, dayTableModel } = props
+    let { dateEnv, options, calendar, dateProfile } = context
+    let { dayTableModel } = props
     let dayRanges = this.buildDayRanges(dayTableModel, dateProfile, dateEnv)
     let dayRanges = this.buildDayRanges(dayTableModel, dateProfile, dateEnv)
 
 
     // give it the first row of cells
     // give it the first row of cells
@@ -67,7 +66,6 @@ export class DayTimeCols extends DateComponent<DayTimeColsProps> {
             ref={this.timeColsRef}
             ref={this.timeColsRef}
             rootElRef={this.handleRootEl}
             rootElRef={this.handleRootEl}
             {...this.slicer.sliceProps(props, dateProfile, null, context.calendar, dayRanges)}
             {...this.slicer.sliceProps(props, dateProfile, null, context.calendar, dayRanges)}
-            dateProfile={dateProfile}
             axis={props.axis}
             axis={props.axis}
             slatMetas={props.slatMetas}
             slatMetas={props.slatMetas}
             slotDuration={props.slotDuration}
             slotDuration={props.slotDuration}

+ 1 - 5
packages/timegrid/src/DayTimeColsView.tsx

@@ -24,8 +24,7 @@ export class DayTimeColsView extends TimeColsView {
 
 
 
 
   render(props: ViewProps, state: {}, context: ComponentContext) {
   render(props: ViewProps, state: {}, context: ComponentContext) {
-    let { dateProfile, dateProfileGenerator } = props
-    let { nextDayThreshold, options, dateEnv } = context
+    let { nextDayThreshold, options, dateEnv, dateProfile, dateProfileGenerator } = context
     let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator)
     let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator)
     let splitProps = this.allDaySplitter.splitProps(props)
     let splitProps = this.allDaySplitter.splitProps(props)
     let slotDuration = this.parseSlotDuration(options.slotDuration)
     let slotDuration = this.parseSlotDuration(options.slotDuration)
@@ -34,7 +33,6 @@ export class DayTimeColsView extends TimeColsView {
 
 
     let headerContent = options.dayHeaders &&
     let headerContent = options.dayHeaders &&
       <DayHeader
       <DayHeader
-        dateProfile={dateProfile}
         dates={dayTableModel.headerDates}
         dates={dayTableModel.headerDates}
         datesRepDistinctDays={true}
         datesRepDistinctDays={true}
         renderIntro={dayMinWidth ? null : this.renderHeadAxis}
         renderIntro={dayMinWidth ? null : this.renderHeadAxis}
@@ -43,7 +41,6 @@ export class DayTimeColsView extends TimeColsView {
     let allDayContent = options.allDaySlot && ((contentArg: ChunkContentCallbackArgs) => (
     let allDayContent = options.allDaySlot && ((contentArg: ChunkContentCallbackArgs) => (
       <DayTable
       <DayTable
         {...splitProps['allDay']}
         {...splitProps['allDay']}
-        dateProfile={dateProfile}
         dayTableModel={dayTableModel}
         dayTableModel={dayTableModel}
         nextDayThreshold={nextDayThreshold}
         nextDayThreshold={nextDayThreshold}
         tableMinWidth={contentArg.tableMinWidth}
         tableMinWidth={contentArg.tableMinWidth}
@@ -61,7 +58,6 @@ export class DayTimeColsView extends TimeColsView {
     let timeGridContent = (contentArg: ChunkContentCallbackArgs) => (
     let timeGridContent = (contentArg: ChunkContentCallbackArgs) => (
       <DayTimeCols
       <DayTimeCols
         {...splitProps['timed']}
         {...splitProps['timed']}
-        dateProfile={dateProfile}
         dayTableModel={dayTableModel}
         dayTableModel={dayTableModel}
         axis={!dayMinWidth}
         axis={!dayMinWidth}
         slotDuration={slotDuration}
         slotDuration={slotDuration}

+ 2 - 3
packages/timegrid/src/TimeCol.tsx

@@ -1,4 +1,4 @@
-import { Ref, DateMarker, BaseComponent, ComponentContext, h, EventSegUiInteractionState, Seg, getSegMeta, DateRange, DateProfile, Fragment, DayCellRoot, NowIndicatorRoot, DayCellContent, BgEvent, renderFill } from '@fullcalendar/core'
+import { Ref, DateMarker, BaseComponent, ComponentContext, h, EventSegUiInteractionState, Seg, getSegMeta, DateRange, Fragment, DayCellRoot, NowIndicatorRoot, DayCellContent, BgEvent, renderFill } from '@fullcalendar/core'
 import { TimeColsSeg } from './TimeColsSeg'
 import { TimeColsSeg } from './TimeColsSeg'
 import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
 import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
 import { computeSegCoords, computeSegVerticals } from './event-placement'
 import { computeSegCoords, computeSegVerticals } from './event-placement'
@@ -8,7 +8,6 @@ import { TimeColEvent } from './TimeColEvent'
 export interface TimeColProps {
 export interface TimeColProps {
   elRef?: Ref<HTMLTableCellElement>
   elRef?: Ref<HTMLTableCellElement>
   date: DateMarker
   date: DateMarker
-  dateProfile: DateProfile
   nowDate: DateMarker
   nowDate: DateMarker
   todayRange: DateRange
   todayRange: DateRange
   extraDataAttrs?: any
   extraDataAttrs?: any
@@ -43,7 +42,7 @@ export class TimeCol extends BaseComponent<TimeColProps> {
       {}
       {}
 
 
     return (
     return (
-      <DayCellRoot elRef={props.elRef} date={props.date} todayRange={props.todayRange} extraHookProps={props.extraHookProps} dateProfile={props.dateProfile}>
+      <DayCellRoot elRef={props.elRef} date={props.date} todayRange={props.todayRange} extraHookProps={props.extraHookProps}>
         {(rootElRef, classNames, dataAttrs) => (
         {(rootElRef, classNames, dataAttrs) => (
           <td
           <td
             ref={rootElRef}
             ref={rootElRef}

+ 5 - 10
packages/timegrid/src/TimeCols.tsx

@@ -7,7 +7,6 @@ import {
   DateMarker,
   DateMarker,
   BaseComponent,
   BaseComponent,
   EventSegUiInteractionState,
   EventSegUiInteractionState,
-  DateProfile,
   memoize,
   memoize,
   CssDimValue,
   CssDimValue,
   PositionCache,
   PositionCache,
@@ -24,7 +23,6 @@ import { TimeColsSeg } from './TimeColsSeg'
 
 
 
 
 export interface TimeColsProps {
 export interface TimeColsProps {
-  dateProfile: DateProfile
   cells: TableCellModel[]
   cells: TableCellModel[]
   slotDuration: Duration
   slotDuration: Duration
   nowDate: DateMarker
   nowDate: DateMarker
@@ -65,8 +63,6 @@ export class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
 
 
 
 
   render(props: TimeColsProps, state: TimeColsState) {
   render(props: TimeColsProps, state: TimeColsState) {
-    let { dateProfile } = props
-
     return (
     return (
       <div class='fc-timegrid-body' ref={props.rootElRef} style={{
       <div class='fc-timegrid-body' ref={props.rootElRef} style={{
         // these props are important to give this wrapper correct dimensions for interactions
         // these props are important to give this wrapper correct dimensions for interactions
@@ -75,7 +71,6 @@ export class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
         minWidth: props.tableMinWidth
         minWidth: props.tableMinWidth
       }}>
       }}>
         <TimeColsSlats
         <TimeColsSlats
-          dateProfile={dateProfile}
           axis={props.axis}
           axis={props.axis}
           slatMetas={props.slatMetas}
           slatMetas={props.slatMetas}
           clientWidth={props.clientWidth}
           clientWidth={props.clientWidth}
@@ -86,7 +81,6 @@ export class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
         />
         />
         <TimeColsContent
         <TimeColsContent
           cells={props.cells}
           cells={props.cells}
-          dateProfile={props.dateProfile}
           axis={props.axis}
           axis={props.axis}
           businessHourSegs={props.businessHourSegs}
           businessHourSegs={props.businessHourSegs}
           bgEventSegs={props.bgEventSegs}
           bgEventSegs={props.bgEventSegs}
@@ -115,8 +109,9 @@ export class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
   }
   }
 
 
 
 
-  componentDidUpdate(prevProps: TimeColsProps) {
-    this.scrollResponder.update(this.props.dateProfile !== prevProps.dateProfile)
+  componentDidUpdate(prevProps: TimeColsProps, prevState: TimeColsState) {
+    let didContextUpdate = prevProps === this.props && prevState === this.state // only way to detect context change. if props/start didnt
+    this.scrollResponder.update(didContextUpdate) // if context changed, dateProfile probably changed
   }
   }
 
 
 
 
@@ -155,7 +150,7 @@ export class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
 
 
 
 
   positionToHit(positionLeft, positionTop) {
   positionToHit(positionLeft, positionTop) {
-    let { dateEnv, options } = this.context
+    let { dateProfile, dateEnv, options } = this.context
     let { colCoords } = this
     let { colCoords } = this
     let { slatCoords } = this.state
     let { slatCoords } = this.state
     let { snapDuration, snapsPerSlot } = this.processSlotOptions(this.props.slotDuration, options.snapDuration)
     let { snapDuration, snapsPerSlot } = this.processSlotOptions(this.props.slotDuration, options.snapDuration)
@@ -172,7 +167,7 @@ export class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
 
 
       let dayDate = this.props.cells[colIndex].date
       let dayDate = this.props.cells[colIndex].date
       let time = addDurations(
       let time = addDurations(
-        this.props.dateProfile.slotMinTime,
+        dateProfile.slotMinTime,
         multiplyDuration(snapDuration, snapIndex)
         multiplyDuration(snapDuration, snapIndex)
       )
       )
 
 

+ 0 - 3
packages/timegrid/src/TimeColsContent.tsx

@@ -10,7 +10,6 @@ import {
   ComponentContext,
   ComponentContext,
   memoize,
   memoize,
   DateRange,
   DateRange,
-  DateProfile,
   NowIndicatorRoot
   NowIndicatorRoot
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import { TableCellModel } from '@fullcalendar/daygrid' // TODO: good to use this interface?
 import { TableCellModel } from '@fullcalendar/daygrid' // TODO: good to use this interface?
@@ -22,7 +21,6 @@ import { TimeCol } from './TimeCol'
 export interface TimeColsContentProps {
 export interface TimeColsContentProps {
   axis: boolean
   axis: boolean
   cells: TableCellModel[]
   cells: TableCellModel[]
-  dateProfile: DateProfile
   nowDate: DateMarker
   nowDate: DateMarker
   todayRange: DateRange
   todayRange: DateRange
   businessHourSegs: TimeColsSeg[]
   businessHourSegs: TimeColsSeg[]
@@ -87,7 +85,6 @@ export class TimeColsContent extends BaseComponent<TimeColsContentProps> { // TO
                   key={cell.key}
                   key={cell.key}
                   elRef={this.cellElRefs.createRef(cell.key)}
                   elRef={this.cellElRefs.createRef(cell.key)}
                   date={cell.date}
                   date={cell.date}
-                  dateProfile={props.dateProfile}
                   nowDate={props.nowDate}
                   nowDate={props.nowDate}
                   todayRange={props.todayRange}
                   todayRange={props.todayRange}
                   extraHookProps={cell.extraHookProps}
                   extraHookProps={cell.extraHookProps}

+ 4 - 6
packages/timegrid/src/TimeColsSlats.tsx

@@ -1,7 +1,6 @@
 import {
 import {
   h, VNode,
   h, VNode,
   BaseComponent,
   BaseComponent,
-  DateProfile,
   ComponentContext,
   ComponentContext,
   createDuration,
   createDuration,
   asRoughMs,
   asRoughMs,
@@ -31,7 +30,6 @@ export interface TimeColsSlatsProps extends TimeColsSlatsContentProps {
 }
 }
 
 
 interface TimeColsSlatsContentProps {
 interface TimeColsSlatsContentProps {
-  dateProfile: DateProfile
   axis: boolean
   axis: boolean
   slatMetas: TimeSlatMeta[]
   slatMetas: TimeSlatMeta[]
 }
 }
@@ -112,7 +110,7 @@ export class TimeColsSlats extends BaseComponent<TimeColsSlatsProps> {
             false,
             false,
             true // vertical
             true // vertical
           ),
           ),
-          props.dateProfile,
+          this.context.dateProfile,
           props.slatMetas
           props.slatMetas
         )
         )
       )
       )
@@ -145,7 +143,7 @@ export class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> {
           let hookProps = {
           let hookProps = {
             time: slatMeta.time,
             time: slatMeta.time,
             date: context.dateEnv.toDate(slatMeta.date),
             date: context.dateEnv.toDate(slatMeta.date),
-            view: context.view
+            view: context.viewApi
           }
           }
           let classNames = [
           let classNames = [
             'fc-timegrid-slot',
             'fc-timegrid-slot',
@@ -204,12 +202,12 @@ export function TimeColsAxisCell(props: TimeSlatMeta) {
           )
           )
 
 
         } else {
         } else {
-          let { dateEnv, options, view } = context
+          let { dateEnv, options, viewApi } = context
           let labelFormat = createFormatter(options.slotLabelFormat || DEFAULT_SLAT_LABEL_FORMAT) // TODO: optimize!!!
           let labelFormat = createFormatter(options.slotLabelFormat || DEFAULT_SLAT_LABEL_FORMAT) // TODO: optimize!!!
           let hookProps = {
           let hookProps = {
             time: props.time,
             time: props.time,
             date: dateEnv.toDate(props.date),
             date: dateEnv.toDate(props.date),
-            view: view,
+            view: viewApi,
             text: dateEnv.format(props.date, labelFormat)
             text: dateEnv.format(props.date, labelFormat)
           }
           }
 
 

+ 5 - 5
packages/timegrid/src/TimeColsView.tsx

@@ -94,7 +94,7 @@ export abstract class TimeColsView extends DateComponent<ViewProps> {
     })
     })
 
 
     return (
     return (
-      <ViewRoot viewSpec={props.viewSpec} elRef={this.rootElRef}>
+      <ViewRoot viewSpec={context.viewSpec} elRef={this.rootElRef}>
         {(rootElRef, classNames) => (
         {(rootElRef, classNames) => (
           <div class={[ 'fc-timegrid' ].concat(classNames).join(' ')} ref={rootElRef}>
           <div class={[ 'fc-timegrid' ].concat(classNames).join(' ')} ref={rootElRef}>
             <SimpleScrollGrid
             <SimpleScrollGrid
@@ -203,7 +203,7 @@ export abstract class TimeColsView extends DateComponent<ViewProps> {
     }
     }
 
 
     return (
     return (
-      <ViewRoot viewSpec={props.viewSpec} elRef={this.rootElRef}>
+      <ViewRoot viewSpec={context.viewSpec} elRef={this.rootElRef}>
         {(rootElRef, classNames) => (
         {(rootElRef, classNames) => (
           <div class={[ 'fc-timegrid' ].concat(classNames).join(' ')} ref={rootElRef}>
           <div class={[ 'fc-timegrid' ].concat(classNames).join(' ')} ref={rootElRef}>
             <ScrollGrid
             <ScrollGrid
@@ -249,8 +249,8 @@ export abstract class TimeColsView extends DateComponent<ViewProps> {
 
 
 
 
   renderHeadAxis = () => {
   renderHeadAxis = () => {
-    let { options } = this.context
-    let range = this.props.dateProfile.renderRange
+    let { options, dateProfile } = this.context
+    let range = dateProfile.renderRange
     let dayCnt = diffDays(range.start, range.end)
     let dayCnt = diffDays(range.start, range.end)
     let navLinkData = (options.navLinks && dayCnt === 1) // only do in day views (to avoid doing in week views that dont need it)
     let navLinkData = (options.navLinks && dayCnt === 1) // only do in day views (to avoid doing in week views that dont need it)
       ? buildNavLinkData(range.start, 'week')
       ? buildNavLinkData(range.start, 'week')
@@ -291,7 +291,7 @@ export abstract class TimeColsView extends DateComponent<ViewProps> {
     let { context } = this
     let { context } = this
     let hookProps = {
     let hookProps = {
       text: context.options.allDayText,
       text: context.options.allDayText,
-      view: context.view
+      view: context.viewApi
     }
     }
 
 
     return (
     return (