Răsfoiți Sursa

separate CalendarApi

Adam Shaw 5 ani în urmă
părinte
comite
93f5bb90eb
32 a modificat fișierele cu 852 adăugiri și 776 ștergeri
  1. 1 1
      packages-premium
  2. 3 3
      packages/__tests__/src/datelib/luxon.js
  3. 1 1
      packages/__tests__/src/datelib/main.js
  4. 2 2
      packages/__tests__/src/datelib/moment.js
  5. 52 626
      packages/core/src/Calendar.tsx
  6. 568 0
      packages/core/src/CalendarApi.tsx
  7. 3 3
      packages/core/src/CalendarComponent.tsx
  8. 17 17
      packages/core/src/api/EventApi.ts
  9. 7 9
      packages/core/src/api/EventSourceApi.ts
  10. 21 3
      packages/core/src/calendar-utils.ts
  11. 5 3
      packages/core/src/common/Emitter.ts
  12. 1 1
      packages/core/src/common/EventRoot.tsx
  13. 3 3
      packages/core/src/component/ComponentContext.ts
  14. 4 4
      packages/core/src/interactions/EventClicking.ts
  15. 6 6
      packages/core/src/interactions/EventHovering.ts
  16. 2 1
      packages/core/src/main.ts
  17. 1 1
      packages/core/src/option-change-handlers.ts
  18. 66 8
      packages/core/src/reducers/CalendarStateReducer.ts
  19. 2 2
      packages/core/src/reducers/ReducerContext.ts
  20. 1 1
      packages/core/src/reducers/eventStore.ts
  21. 3 2
      packages/core/src/structs/event-mutation.ts
  22. 1 1
      packages/core/src/structs/event.ts
  23. 10 10
      packages/core/src/toolbar-parse.ts
  24. 11 10
      packages/core/src/validation.ts
  25. 7 6
      packages/daygrid/src/Table.tsx
  26. 4 3
      packages/interaction/src/interactions-external/ExternalElementDragging.ts
  27. 5 5
      packages/interaction/src/interactions/DateSelecting.ts
  28. 18 18
      packages/interaction/src/interactions/EventDragging.ts
  29. 17 17
      packages/interaction/src/interactions/EventResizing.ts
  30. 5 4
      packages/interaction/src/interactions/UnselectAuto.ts
  31. 3 3
      packages/luxon/src/main.ts
  32. 2 2
      packages/moment/src/main.ts

+ 1 - 1
packages-premium

@@ -1 +1 @@
-Subproject commit feda16ea9a71c2ccb6b8d3555063bc6b83a0d8d8
+Subproject commit e295068966285df019008d0d189d28899e05f78b

+ 3 - 3
packages/__tests__/src/datelib/luxon.js

@@ -90,8 +90,8 @@ describe('luxon plugin', function() {
       })
 
       // hacky way to have a duration parsed
-      let timedDuration = toLuxonDuration(calendar.state.computedOptions.defaultTimedEventDuration, calendar)
-      let allDayDuration = toLuxonDuration(calendar.state.computedOptions.defaultAllDayEventDuration, calendar)
+      let timedDuration = toLuxonDuration(calendar.currentState.computedOptions.defaultTimedEventDuration, calendar)
+      let allDayDuration = toLuxonDuration(calendar.currentState.computedOptions.defaultAllDayEventDuration, calendar)
 
       expect(timedDuration.as('hours')).toBe(5)
       expect(allDayDuration.as('days')).toBe(3)
@@ -105,7 +105,7 @@ describe('luxon plugin', function() {
       })
 
       // hacky way to have a duration parsed
-      let timedDuration = toLuxonDuration(calendar.state.computedOptions.defaultTimedEventDuration, calendar)
+      let timedDuration = toLuxonDuration(calendar.currentState.computedOptions.defaultTimedEventDuration, calendar)
 
       expect(timedDuration.locale).toBe('es')
     })

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

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

+ 2 - 2
packages/__tests__/src/datelib/moment.js

@@ -65,8 +65,8 @@ describe('moment plugin', function() {
       })
 
       // hacky way to have a duration parsed
-      let timedDuration = toMomentDuration(calendar.state.computedOptions.defaultTimedEventDuration)
-      let allDayDuration = toMomentDuration(calendar.state.computedOptions.defaultAllDayEventDuration)
+      let timedDuration = toMomentDuration(calendar.currentState.computedOptions.defaultTimedEventDuration)
+      let allDayDuration = toMomentDuration(calendar.currentState.computedOptions.defaultAllDayEventDuration)
 
       expect(timedDuration.asHours()).toBe(5)
       expect(allDayDuration.asDays()).toBe(3)

+ 52 - 626
packages/core/src/Calendar.tsx

@@ -1,72 +1,81 @@
-import { Emitter } from './common/Emitter'
 import { OptionsInput } from './types/input-types'
-import { DateInput } from './datelib/env'
-import { DateMarker, startOfDay } from './datelib/marker'
-import { createFormatter } from './datelib/formatting'
-import { createDuration, DurationInput } from './datelib/duration'
-import { parseDateSpan, DateSpanInput } from './structs/date-span'
-import { DateRangeInput } from './datelib/date-range'
-import { EventSourceInput, parseEventSource } from './structs/event-source'
-import { EventInput, parseEvent } from './structs/event'
 import { CalendarState, Action } from './reducers/types'
-import { EventSourceApi } from './api/EventSourceApi'
-import { EventApi } from './api/EventApi'
-import { eventTupleToStore } from './structs/event-store'
-import { ViewSpec } from './structs/view-spec'
 import { CalendarComponent } from './CalendarComponent'
 import { __assign } from 'tslib'
-import { PointerDragEvent } from './interactions/pointer'
 import { render, h, flushToDom } from './vdom'
-import { TaskRunner, DelayedRunner } from './util/runner'
+import { DelayedRunner } from './util/runner'
 import { guid } from './util/misc'
 import { CssDimValue } from './scrollgrid/util'
 import { applyStyleProp } from './util/dom-manip'
 import { CalendarStateReducer } from './reducers/CalendarStateReducer'
-import { getNow } from './reducers/current-date'
-import { triggerDateSelect, triggerDateUnselect } from './calendar-utils'
+import { CalendarApi } from './CalendarApi'
 
 
-export class Calendar {
+export class Calendar extends CalendarApi {
 
-  state: CalendarState = {} as any
+  el: HTMLElement
   isRendering = false
   isRendered = false
-  emitter = new Emitter(this)
-  reducer: CalendarStateReducer
   renderRunner: DelayedRunner
-  actionRunner: TaskRunner<Action> // guards against nested action calls
-  el: HTMLElement
   currentClassNames: string[] = []
+  currentState: CalendarState
 
-  get view() { return this.state.viewApi } // for public API
+  get view() { return this.currentState.viewApi } // for public API
 
 
   constructor(el: HTMLElement, optionOverrides: OptionsInput = {}) {
-    this.el = el
+    super(new CalendarStateReducer())
 
-    this.reducer = new CalendarStateReducer()
+    this.el = el
+    this.renderRunner = new DelayedRunner(this.handleRenderRequest)
 
-    let renderRunner = this.renderRunner = new DelayedRunner(
-      this.updateComponent.bind(this)
+    this.reducer.init(
+      optionOverrides,
+      this,
+      this.handleAction,
+      this.handleState
     )
+  }
+
+
+  handleAction = (action: Action) => {
+    // actions we know we want to render immediately
+    switch (action.type) {
+      case 'SET_EVENT_DRAG':
+      case 'SET_EVENT_RESIZE':
+        this.renderRunner.tryDrain()
+    }
+  }
 
-    this.actionRunner = new TaskRunner( // do we really need this in a runner?
-      this.runAction.bind(this),
-      () => {
-        renderRunner.request(this.state.options.rerenderDelay)
-      }
-    )
 
-    this.dispatch({
-      type: 'INIT',
-      optionOverrides
-    })
+  handleState = (state: CalendarState) => {
+    this.currentState = state
+    this.renderRunner.request(state.options.rerenderDelay)
   }
 
 
+  handleRenderRequest = () => {
+
+    if (this.isRendering) {
+      this.isRendered = true
+
+      render(
+        <CalendarComponent
+          {...this.currentState}
+          onClassNameChange={this.handleClassNames}
+          onHeightChange={this.handleHeightChange}
+        />,
+        this.el
+      )
+
+    } else if (this.isRendered) {
+      this.isRendered = false
 
-  // Public API for rendering
-  // -----------------------------------------------------------------------------------------------------------------
+      render(null, this.el)
+    }
+
+    flushToDom()
+  }
 
 
   render() {
@@ -88,36 +97,12 @@ export class Calendar {
   }
 
 
-  // Dispatcher
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  dispatch = (action: Action) => {
-    this.actionRunner.request(action)
-
-    // actions we know we want to render immediately. TODO: another param in dispatch instead?
-    switch (action.type) {
-      case 'SET_EVENT_DRAG':
-      case 'SET_EVENT_RESIZE':
-        this.renderRunner.tryDrain()
-    }
-  }
-
-
-  runAction(action: Action) {
-    this.state = this.reducer.reduce(this.state, action, this.dispatch, this.emitter, this.getCurrentState, this)
-  }
-
-
-  getCurrentState = () => {
-    return this.state
+  updateSize() {
+    super.updateSize()
+    flushToDom()
   }
 
 
-  // Rendering
-  // -----------------------------------------------------------------------------------------------------------------
-
-
   batchRendering(func) {
     this.renderRunner.pause('batchRendering')
     func()
@@ -135,39 +120,6 @@ export class Calendar {
   }
 
 
-  updateComponent() {
-    if (this.isRendering) {
-      this.renderComponent()
-      this.isRendered = true
-    } else {
-      if (this.isRendered) {
-        this.destroyComponent()
-        this.isRendered = false
-      }
-    }
-  }
-
-
-  renderComponent() {
-    let { state } = this
-
-    render(
-      <CalendarComponent
-        {...state}
-        onClassNameChange={this.handleClassNames}
-        onHeightChange={this.handleHeightChange}
-      />,
-      this.el
-    )
-    flushToDom()
-  }
-
-
-  destroyComponent() {
-    render(null, this.el)
-  }
-
-
   handleClassNames = (classNames: string[]) => {
     let { classList } = this.el
 
@@ -187,530 +139,4 @@ export class Calendar {
     applyStyleProp(this.el, 'height', height)
   }
 
-
-  // Options
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  setOption(name: string, val) {
-    this.dispatch({
-      type: 'SET_OPTION',
-      optionName: name,
-      optionValue: val
-    })
-  }
-
-
-  getOption(name: string) { // getter, used externally
-    return this.state.calendarOptions[name]
-  }
-
-
-  /*
-  handles option changes (like a diff)
-  */
-  mutateOptions(updates, removals: string[] = [], isDynamic = false) {
-    let changeHandlers = this.state.pluginHooks.optionChangeHandlers
-    let normalUpdates = {}
-    let specialUpdates = {}
-
-    for (let name in updates) {
-      if (changeHandlers[name]) {
-        specialUpdates[name] = updates[name]
-      } else {
-        normalUpdates[name] = updates[name]
-      }
-    }
-
-    this.batchRendering(() => {
-
-      this.dispatch({
-        type: 'MUTATE_OPTIONS',
-        updates: normalUpdates,
-        removals,
-        isDynamic
-      })
-
-      // special updates
-      for (let name in specialUpdates) {
-        changeHandlers[name](specialUpdates[name], this.state)
-      }
-    })
-  }
-
-
-  getAvailableLocaleCodes() {
-    return Object.keys(this.state.availableRawLocales)
-  }
-
-
-  // Trigger
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  on(handlerName: string, handler) {
-    this.emitter.on(handlerName, handler)
-  }
-
-
-  off(handlerName: string, handler) {
-    this.emitter.off(handlerName, handler)
-  }
-
-
-  // View
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput) {
-    this.batchRendering(() => {
-      this.unselect()
-
-      if (dateOrRange) {
-
-        if ((dateOrRange as DateRangeInput).start && (dateOrRange as DateRangeInput).end) { // a range
-          this.dispatch({
-            type: 'CHANGE_VIEW_TYPE',
-            viewType,
-          })
-          this.dispatch({ // not very efficient to do two dispatches
-            type: 'SET_OPTION',
-            optionName: 'visibleRange',
-            optionValue: dateOrRange
-          })
-
-        } else {
-          this.dispatch({
-            type: 'CHANGE_VIEW_TYPE',
-            viewType,
-            dateMarker: this.state.dateEnv.createMarker(dateOrRange as DateInput)
-          })
-        }
-
-      } else {
-        this.dispatch({
-          type: 'CHANGE_VIEW_TYPE',
-          viewType
-        })
-      }
-    })
-  }
-
-
-  // Forces navigation to a view for the given date.
-  // `viewType` can be a specific view name or a generic one like "week" or "day".
-  // needs to change
-  zoomTo(dateMarker: DateMarker, viewType?: string) {
-    let spec
-
-    viewType = viewType || 'day' // day is default zoom
-    spec = this.state.viewSpecs[viewType] || this.getUnitViewSpec(viewType)
-
-    this.unselect()
-
-    if (spec) {
-      this.dispatch({
-        type: 'CHANGE_VIEW_TYPE',
-        viewType: spec.type,
-        dateMarker
-      })
-
-    } else {
-      this.dispatch({
-        type: 'CHANGE_DATE',
-        dateMarker
-      })
-    }
-  }
-
-
-  // Given a duration singular unit, like "week" or "day", finds a matching view spec.
-  // Preference is given to views that have corresponding buttons.
-  private getUnitViewSpec(unit: string): ViewSpec | null {
-    let { viewSpecs, toolbarConfig } = this.state
-    let viewTypes = [].concat(toolbarConfig.viewsWithButtons)
-    let i
-    let spec
-
-    for (let viewType in viewSpecs) {
-      viewTypes.push(viewType)
-    }
-
-    for (i = 0; i < viewTypes.length; i++) {
-      spec = viewSpecs[viewTypes[i]]
-      if (spec) {
-        if (spec.singleUnit === unit) {
-          return spec
-        }
-      }
-    }
-  }
-
-
-  // Current Date
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  prev() {
-    this.unselect()
-    this.dispatch({ type: 'PREV' })
-  }
-
-
-  next() {
-    this.unselect()
-    this.dispatch({ type: 'NEXT' })
-  }
-
-
-  prevYear() {
-    this.unselect()
-    this.dispatch({
-      type: 'CHANGE_DATE',
-      dateMarker: this.state.dateEnv.addYears(this.state.currentDate, -1)
-    })
-  }
-
-
-  nextYear() {
-    this.unselect()
-    this.dispatch({
-      type: 'CHANGE_DATE',
-      dateMarker: this.state.dateEnv.addYears(this.state.currentDate, 1)
-    })
-  }
-
-
-  today() {
-    this.unselect()
-    this.dispatch({
-      type: 'CHANGE_DATE',
-      dateMarker: getNow(this.state)
-    })
-  }
-
-
-  gotoDate(zonedDateInput) {
-    this.unselect()
-    this.dispatch({
-      type: 'CHANGE_DATE',
-      dateMarker: this.state.dateEnv.createMarker(zonedDateInput)
-    })
-  }
-
-
-  incrementDate(deltaInput) { // is public facing
-    let delta = createDuration(deltaInput)
-
-    if (delta) { // else, warn about invalid input?
-      this.unselect()
-      this.dispatch({
-        type: 'CHANGE_DATE',
-        dateMarker: this.state.dateEnv.add(this.state.currentDate, delta)
-      })
-    }
-  }
-
-
-  // for external API
-  getDate(): Date {
-    return this.state.dateEnv.toDate(this.state.currentDate)
-  }
-
-
-  // Date Formatting Utils
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  formatDate(d: DateInput, formatter): string {
-    let { dateEnv } = this.state
-
-    return dateEnv.format(
-      dateEnv.createMarker(d),
-      createFormatter(formatter)
-    )
-  }
-
-
-  // `settings` is for formatter AND isEndExclusive
-  formatRange(d0: DateInput, d1: DateInput, settings) {
-    let { dateEnv, options } = this.state
-
-    return dateEnv.formatRange(
-      dateEnv.createMarker(d0),
-      dateEnv.createMarker(d1),
-      createFormatter(settings, options.defaultRangeSeparator),
-      settings
-    )
-  }
-
-
-  formatIso(d: DateInput, omitTime?: boolean) {
-    let { dateEnv } = this.state
-
-    return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime })
-  }
-
-
-  // Sizing
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  updateSize() { // public
-    this.emitter.trigger('_resize', true)
-    flushToDom()
-  }
-
-
-  // Date Selection / Event Selection / DayClick
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  // this public method receives start/end dates in any format, with any timezone
-  // NOTE: args were changed from v3
-  select(dateOrObj: DateInput | any, endDate?: DateInput) {
-    let selectionInput: DateSpanInput
-
-    if (endDate == null) {
-      if (dateOrObj.start != null) {
-        selectionInput = dateOrObj as DateSpanInput
-      } else {
-        selectionInput = {
-          start: dateOrObj,
-          end: null
-        }
-      }
-    } else {
-      selectionInput = {
-        start: dateOrObj,
-        end: endDate
-      } as DateSpanInput
-    }
-
-    let selection = parseDateSpan(
-      selectionInput,
-      this.state.dateEnv,
-      createDuration({ days: 1 }) // TODO: cache this?
-    )
-
-    if (selection) { // throw parse error otherwise?
-      this.dispatch({ type: 'SELECT_DATES', selection })
-      triggerDateSelect(selection, null, this.state)
-    }
-  }
-
-
-  // public method
-  unselect(pev?: PointerDragEvent) {
-    if (this.state.dateSelection) {
-      this.dispatch({ type: 'UNSELECT_DATES' })
-      triggerDateUnselect(pev, this.state)
-    }
-  }
-
-
-  // Event-Date Utilities
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  // Given an event's allDay status and start date, return what its fallback end date should be.
-  // TODO: rename to computeDefaultEventEnd
-  getDefaultEventEnd(allDay: boolean, marker: DateMarker): DateMarker {
-    let end = marker
-
-    if (allDay) {
-      end = startOfDay(end)
-      end = this.state.dateEnv.add(end, this.state.computedOptions.defaultAllDayEventDuration)
-    } else {
-      end = this.state.dateEnv.add(end, this.state.computedOptions.defaultTimedEventDuration)
-    }
-
-    return end
-  }
-
-
-  // Public Events API
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  addEvent(eventInput: EventInput, sourceInput?: EventSourceApi | string | number): EventApi | null {
-
-    if (eventInput instanceof EventApi) {
-      let def = eventInput._def
-      let instance = eventInput._instance
-
-      // not already present? don't want to add an old snapshot
-      if (!this.state.eventStore.defs[def.defId]) {
-        this.dispatch({
-          type: 'ADD_EVENTS',
-          eventStore: eventTupleToStore({ def, instance }) // TODO: better util for two args?
-        })
-      }
-
-      return eventInput
-    }
-
-    let sourceId
-    if (sourceInput instanceof EventSourceApi) {
-      sourceId = sourceInput.internalEventSource.sourceId
-    } else if (sourceInput != null) {
-      let sourceApi = this.getEventSourceById(sourceInput) // TODO: use an internal function
-
-      if (!sourceApi) {
-        console.warn('Could not find an event source with ID "' + sourceInput + '"') // TODO: test
-        return null
-      } else {
-        sourceId = sourceApi.internalEventSource.sourceId
-      }
-    }
-
-    let tuple = parseEvent(eventInput, sourceId, this.state)
-
-    if (tuple) {
-
-      this.dispatch({
-        type: 'ADD_EVENTS',
-        eventStore: eventTupleToStore(tuple)
-      })
-
-      return new EventApi(
-        this,
-        tuple.def,
-        tuple.def.recurringDef ? null : tuple.instance
-      )
-    }
-
-    return null
-  }
-
-
-  // TODO: optimize
-  getEventById(id: string): EventApi | null {
-    let { defs, instances } = this.state.eventStore
-
-    id = String(id)
-
-    for (let defId in defs) {
-      let def = defs[defId]
-
-      if (def.publicId === id) {
-
-        if (def.recurringDef) {
-          return new EventApi(this, def, null)
-        } else {
-
-          for (let instanceId in instances) {
-            let instance = instances[instanceId]
-
-            if (instance.defId === def.defId) {
-              return new EventApi(this, def, instance)
-            }
-          }
-        }
-      }
-    }
-
-    return null
-  }
-
-
-  getEvents(): EventApi[] {
-    let { defs, instances } = this.state.eventStore
-    let eventApis: EventApi[] = []
-
-    for (let id in instances) {
-      let instance = instances[id]
-      let def = defs[instance.defId]
-
-      eventApis.push(new EventApi(this, def, instance))
-    }
-
-    return eventApis
-  }
-
-
-  removeAllEvents() {
-    this.dispatch({ type: 'REMOVE_ALL_EVENTS' })
-  }
-
-
-  // Public Event Sources API
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  getEventSources(): EventSourceApi[] {
-    let sourceHash = this.state.eventSources
-    let sourceApis: EventSourceApi[] = []
-
-    for (let internalId in sourceHash) {
-      sourceApis.push(new EventSourceApi(this, sourceHash[internalId]))
-    }
-
-    return sourceApis
-  }
-
-
-  getEventSourceById(id: string | number): EventSourceApi | null {
-    let sourceHash = this.state.eventSources
-
-    id = String(id)
-
-    for (let sourceId in sourceHash) {
-      if (sourceHash[sourceId].publicId === id) {
-        return new EventSourceApi(this, sourceHash[sourceId])
-      }
-    }
-
-    return null
-  }
-
-
-  addEventSource(sourceInput: EventSourceInput): EventSourceApi {
-
-    if (sourceInput instanceof EventSourceApi) {
-
-      // not already present? don't want to add an old snapshot
-      if (!this.state.eventSources[sourceInput.internalEventSource.sourceId]) {
-        this.dispatch({
-          type: 'ADD_EVENT_SOURCES',
-          sources: [ sourceInput.internalEventSource ]
-        })
-      }
-
-      return sourceInput
-    }
-
-    let eventSource = parseEventSource(sourceInput, this.state)
-
-    if (eventSource) { // TODO: error otherwise?
-      this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [ eventSource ] })
-
-      return new EventSourceApi(this, eventSource)
-    }
-
-    return null
-  }
-
-
-  removeAllEventSources() {
-    this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' })
-  }
-
-
-  refetchEvents() {
-    this.dispatch({ type: 'FETCH_EVENT_SOURCES' })
-  }
-
-
-  // Scroll
-  // -----------------------------------------------------------------------------------------------------------------
-
-  scrollToTime(timeInput: DurationInput) {
-    let time = createDuration(timeInput)
-
-    if (time) {
-      this.emitter.trigger('_scrollRequest', { time })
-    }
-  }
-
 }

+ 568 - 0
packages/core/src/CalendarApi.tsx

@@ -0,0 +1,568 @@
+import { Emitter } from './common/Emitter'
+import { DateInput } from './datelib/env'
+import { DateMarker } from './datelib/marker'
+import { createFormatter } from './datelib/formatting'
+import { createDuration, DurationInput } from './datelib/duration'
+import { parseDateSpan, DateSpanInput } from './structs/date-span'
+import { DateRangeInput } from './datelib/date-range'
+import { EventSourceInput, parseEventSource } from './structs/event-source'
+import { EventInput, parseEvent } from './structs/event'
+import { CalendarState, Action } from './reducers/types'
+import { EventSourceApi } from './api/EventSourceApi'
+import { EventApi } from './api/EventApi'
+import { eventTupleToStore } from './structs/event-store'
+import { ViewSpec } from './structs/view-spec'
+import { __assign } from 'tslib'
+import { PointerDragEvent } from './interactions/pointer'
+import { getNow } from './reducers/current-date'
+import { triggerDateSelect, triggerDateUnselect } from './calendar-utils'
+import { CalendarStateReducer } from './reducers/CalendarStateReducer'
+
+
+export class CalendarApi {
+
+  // TODO: public should not use, only other iternals
+  dispatch: (action: Action) => void
+  getCurrentState: () => CalendarState
+  emitter: Emitter
+
+  get view() { return this.getCurrentState().viewApi } // for public API
+
+
+  constructor(protected reducer: CalendarStateReducer) {
+    this.dispatch = reducer.dispatch
+    this.getCurrentState = reducer.getCurrentState
+    this.emitter = reducer.emitter
+  }
+
+
+  batchRendering(callback: () => void) { // subclasses should implement
+    callback()
+  }
+
+
+  // TODO: subclasses should have updateSize() call flushToDom
+  updateSize() { // public
+    this.emitter.trigger('_resize', true)
+  }
+
+
+  // Options
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  setOption(name: string, val) {
+    this.dispatch({
+      type: 'SET_OPTION',
+      optionName: name,
+      optionValue: val
+    })
+  }
+
+
+  getOption(name: string) { // getter, used externally
+    return this.getCurrentState().calendarOptions[name]
+  }
+
+
+  /*
+  handles option changes (like a diff)
+  */
+  mutateOptions(updates, removals: string[] = [], isDynamic = false) {
+    let state = this.getCurrentState()
+    let changeHandlers = state.pluginHooks.optionChangeHandlers
+    let normalUpdates = {}
+    let specialUpdates = {}
+
+    for (let name in updates) {
+      if (changeHandlers[name]) {
+        specialUpdates[name] = updates[name]
+      } else {
+        normalUpdates[name] = updates[name]
+      }
+    }
+
+    this.batchRendering(() => {
+
+      this.dispatch({
+        type: 'MUTATE_OPTIONS',
+        updates: normalUpdates,
+        removals,
+        isDynamic
+      })
+
+      // special updates
+      for (let name in specialUpdates) {
+        changeHandlers[name](specialUpdates[name], state)
+      }
+    })
+  }
+
+
+  getAvailableLocaleCodes() {
+    return Object.keys(this.getCurrentState().availableRawLocales)
+  }
+
+
+  // Trigger
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  on(handlerName: string, handler) {
+    this.emitter.on(handlerName, handler)
+  }
+
+
+  off(handlerName: string, handler) {
+    this.emitter.off(handlerName, handler)
+  }
+
+
+  // View
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput) {
+    this.batchRendering(() => {
+      this.unselect()
+
+      if (dateOrRange) {
+
+        if ((dateOrRange as DateRangeInput).start && (dateOrRange as DateRangeInput).end) { // a range
+          this.dispatch({
+            type: 'CHANGE_VIEW_TYPE',
+            viewType,
+          })
+          this.dispatch({ // not very efficient to do two dispatches
+            type: 'SET_OPTION',
+            optionName: 'visibleRange',
+            optionValue: dateOrRange
+          })
+
+        } else {
+          let { dateEnv } = this.getCurrentState()
+
+          this.dispatch({
+            type: 'CHANGE_VIEW_TYPE',
+            viewType,
+            dateMarker: dateEnv.createMarker(dateOrRange as DateInput)
+          })
+        }
+
+      } else {
+        this.dispatch({
+          type: 'CHANGE_VIEW_TYPE',
+          viewType
+        })
+      }
+    })
+  }
+
+
+  // Forces navigation to a view for the given date.
+  // `viewType` can be a specific view name or a generic one like "week" or "day".
+  // needs to change
+  zoomTo(dateMarker: DateMarker, viewType?: string) {
+    let state = this.getCurrentState()
+    let spec
+
+    viewType = viewType || 'day' // day is default zoom
+    spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType)
+
+    this.unselect()
+
+    if (spec) {
+      this.dispatch({
+        type: 'CHANGE_VIEW_TYPE',
+        viewType: spec.type,
+        dateMarker
+      })
+
+    } else {
+      this.dispatch({
+        type: 'CHANGE_DATE',
+        dateMarker
+      })
+    }
+  }
+
+
+  // Given a duration singular unit, like "week" or "day", finds a matching view spec.
+  // Preference is given to views that have corresponding buttons.
+  private getUnitViewSpec(unit: string): ViewSpec | null {
+    let { viewSpecs, toolbarConfig } = this.getCurrentState()
+    let viewTypes = [].concat(toolbarConfig.viewsWithButtons)
+    let i
+    let spec
+
+    for (let viewType in viewSpecs) {
+      viewTypes.push(viewType)
+    }
+
+    for (i = 0; i < viewTypes.length; i++) {
+      spec = viewSpecs[viewTypes[i]]
+      if (spec) {
+        if (spec.singleUnit === unit) {
+          return spec
+        }
+      }
+    }
+  }
+
+
+  // Current Date
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  prev() {
+    this.unselect()
+    this.dispatch({ type: 'PREV' })
+  }
+
+
+  next() {
+    this.unselect()
+    this.dispatch({ type: 'NEXT' })
+  }
+
+
+  prevYear() {
+    let state = this.getCurrentState()
+    this.unselect()
+    this.dispatch({
+      type: 'CHANGE_DATE',
+      dateMarker: state.dateEnv.addYears(state.currentDate, -1)
+    })
+  }
+
+
+  nextYear() {
+    let state = this.getCurrentState()
+
+    this.unselect()
+    this.dispatch({
+      type: 'CHANGE_DATE',
+      dateMarker: state.dateEnv.addYears(state.currentDate, 1)
+    })
+  }
+
+
+  today() {
+    let state = this.getCurrentState()
+
+    this.unselect()
+    this.dispatch({
+      type: 'CHANGE_DATE',
+      dateMarker: getNow(state)
+    })
+  }
+
+
+  gotoDate(zonedDateInput) {
+    let state = this.getCurrentState()
+
+    this.unselect()
+    this.dispatch({
+      type: 'CHANGE_DATE',
+      dateMarker: state.dateEnv.createMarker(zonedDateInput)
+    })
+  }
+
+
+  incrementDate(deltaInput) { // is public facing
+    let state = this.getCurrentState()
+    let delta = createDuration(deltaInput)
+
+    if (delta) { // else, warn about invalid input?
+      this.unselect()
+      this.dispatch({
+        type: 'CHANGE_DATE',
+        dateMarker: state.dateEnv.add(state.currentDate, delta)
+      })
+    }
+  }
+
+
+  // for external API
+  getDate(): Date {
+    let state = this.getCurrentState()
+    return state.dateEnv.toDate(state.currentDate)
+  }
+
+
+  // Date Formatting Utils
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  formatDate(d: DateInput, formatter): string {
+    let { dateEnv } = this.getCurrentState()
+
+    return dateEnv.format(
+      dateEnv.createMarker(d),
+      createFormatter(formatter)
+    )
+  }
+
+
+  // `settings` is for formatter AND isEndExclusive
+  formatRange(d0: DateInput, d1: DateInput, settings) {
+    let { dateEnv, options } = this.getCurrentState()
+
+    return dateEnv.formatRange(
+      dateEnv.createMarker(d0),
+      dateEnv.createMarker(d1),
+      createFormatter(settings, options.defaultRangeSeparator),
+      settings
+    )
+  }
+
+
+  formatIso(d: DateInput, omitTime?: boolean) {
+    let { dateEnv } = this.getCurrentState()
+
+    return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime })
+  }
+
+
+  // Date Selection / Event Selection / DayClick
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  // this public method receives start/end dates in any format, with any timezone
+  // NOTE: args were changed from v3
+  select(dateOrObj: DateInput | any, endDate?: DateInput) {
+    let selectionInput: DateSpanInput
+
+    if (endDate == null) {
+      if (dateOrObj.start != null) {
+        selectionInput = dateOrObj as DateSpanInput
+      } else {
+        selectionInput = {
+          start: dateOrObj,
+          end: null
+        }
+      }
+    } else {
+      selectionInput = {
+        start: dateOrObj,
+        end: endDate
+      } as DateSpanInput
+    }
+
+    let state = this.getCurrentState()
+    let selection = parseDateSpan(
+      selectionInput,
+      state.dateEnv,
+      createDuration({ days: 1 }) // TODO: cache this?
+    )
+
+    if (selection) { // throw parse error otherwise?
+      this.dispatch({ type: 'SELECT_DATES', selection })
+      triggerDateSelect(selection, null, state)
+    }
+  }
+
+
+  // public method
+  unselect(pev?: PointerDragEvent) {
+    let state = this.getCurrentState()
+
+    if (state.dateSelection) {
+      this.dispatch({ type: 'UNSELECT_DATES' })
+      triggerDateUnselect(pev, state)
+    }
+  }
+
+
+  // Public Events API
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  addEvent(eventInput: EventInput, sourceInput?: EventSourceApi | string | number): EventApi | null {
+
+    if (eventInput instanceof EventApi) {
+      let def = eventInput._def
+      let instance = eventInput._instance
+      let { eventStore } = this.getCurrentState()
+
+      // not already present? don't want to add an old snapshot
+      if (!eventStore.defs[def.defId]) {
+        this.dispatch({
+          type: 'ADD_EVENTS',
+          eventStore: eventTupleToStore({ def, instance }) // TODO: better util for two args?
+        })
+      }
+
+      return eventInput
+    }
+
+    let sourceId
+    if (sourceInput instanceof EventSourceApi) {
+      sourceId = sourceInput.internalEventSource.sourceId
+    } else if (sourceInput != null) {
+      let sourceApi = this.getEventSourceById(sourceInput) // TODO: use an internal function
+
+      if (!sourceApi) {
+        console.warn('Could not find an event source with ID "' + sourceInput + '"') // TODO: test
+        return null
+      } else {
+        sourceId = sourceApi.internalEventSource.sourceId
+      }
+    }
+
+    let state = this.getCurrentState()
+    let tuple = parseEvent(eventInput, sourceId, state)
+
+    if (tuple) {
+
+      this.dispatch({
+        type: 'ADD_EVENTS',
+        eventStore: eventTupleToStore(tuple)
+      })
+
+      return new EventApi(
+        state,
+        tuple.def,
+        tuple.def.recurringDef ? null : tuple.instance
+      )
+    }
+
+    return null
+  }
+
+
+  // TODO: optimize
+  getEventById(id: string): EventApi | null {
+    let state = this.getCurrentState()
+    let { defs, instances } = state.eventStore
+
+    id = String(id)
+
+    for (let defId in defs) {
+      let def = defs[defId]
+
+      if (def.publicId === id) {
+
+        if (def.recurringDef) {
+          return new EventApi(state, def, null)
+        } else {
+
+          for (let instanceId in instances) {
+            let instance = instances[instanceId]
+
+            if (instance.defId === def.defId) {
+              return new EventApi(state, def, instance)
+            }
+          }
+        }
+      }
+    }
+
+    return null
+  }
+
+
+  getEvents(): EventApi[] {
+    let state = this.getCurrentState()
+    let { defs, instances } = state.eventStore
+    let eventApis: EventApi[] = []
+
+    for (let id in instances) {
+      let instance = instances[id]
+      let def = defs[instance.defId]
+
+      eventApis.push(new EventApi(state, def, instance))
+    }
+
+    return eventApis
+  }
+
+
+  removeAllEvents() {
+    this.dispatch({ type: 'REMOVE_ALL_EVENTS' })
+  }
+
+
+  // Public Event Sources API
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  getEventSources(): EventSourceApi[] {
+    let state = this.getCurrentState()
+    let sourceHash = state.eventSources
+    let sourceApis: EventSourceApi[] = []
+
+    for (let internalId in sourceHash) {
+      sourceApis.push(new EventSourceApi(state, sourceHash[internalId]))
+    }
+
+    return sourceApis
+  }
+
+
+  getEventSourceById(id: string | number): EventSourceApi | null {
+    let state = this.getCurrentState()
+    let sourceHash = state.eventSources
+
+    id = String(id)
+
+    for (let sourceId in sourceHash) {
+      if (sourceHash[sourceId].publicId === id) {
+        return new EventSourceApi(state, sourceHash[sourceId])
+      }
+    }
+
+    return null
+  }
+
+
+  addEventSource(sourceInput: EventSourceInput): EventSourceApi {
+    let state = this.getCurrentState()
+
+    if (sourceInput instanceof EventSourceApi) {
+
+      // not already present? don't want to add an old snapshot
+      if (!state.eventSources[sourceInput.internalEventSource.sourceId]) {
+        this.dispatch({
+          type: 'ADD_EVENT_SOURCES',
+          sources: [ sourceInput.internalEventSource ]
+        })
+      }
+
+      return sourceInput
+    }
+
+    let eventSource = parseEventSource(sourceInput, state)
+
+    if (eventSource) { // TODO: error otherwise?
+      this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [ eventSource ] })
+
+      return new EventSourceApi(state, eventSource)
+    }
+
+    return null
+  }
+
+
+  removeAllEventSources() {
+    this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' })
+  }
+
+
+  refetchEvents() {
+    this.dispatch({ type: 'FETCH_EVENT_SOURCES' })
+  }
+
+
+  // Scroll
+  // -----------------------------------------------------------------------------------------------------------------
+
+  scrollToTime(timeInput: DurationInput) {
+    let time = createDuration(timeInput)
+
+    if (time) {
+      this.emitter.trigger('_scrollRequest', { time })
+    }
+  }
+
+}

+ 3 - 3
packages/core/src/CalendarComponent.tsx

@@ -108,7 +108,7 @@ export class CalendarComponent extends Component<CalendarComponentProps, Calenda
       props.dispatch,
       props.getCurrentState,
       props.emitter,
-      props.calendar,
+      props.calendarApi,
       this.registerInteractiveComponent,
       this.unregisterInteractiveComponent
     )
@@ -201,7 +201,7 @@ export class CalendarComponent extends Component<CalendarComponentProps, Calenda
 
 
   _handleNavLinkClick(ev: UIEvent, anchorEl: HTMLElement) {
-    let { dateEnv, options, calendar } = this.props
+    let { dateEnv, options, calendarApi } = this.props
 
     let navLinkOptions: any = anchorEl.getAttribute('data-navlink')
     navLinkOptions = navLinkOptions ? JSON.parse(navLinkOptions) : {}
@@ -220,7 +220,7 @@ export class CalendarComponent extends Component<CalendarComponentProps, Calenda
         viewType = customAction
       }
 
-      calendar.zoomTo(dateMarker, viewType)
+      calendarApi.zoomTo(dateMarker, viewType)
     }
   }
 

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

@@ -1,4 +1,3 @@
-import { Calendar } from '../Calendar'
 import { EventDef, EventInstance, NON_DATE_PROPS, DATE_PROPS } from '../structs/event'
 import { UNSCOPED_EVENT_UI_PROPS } from '../component/event-ui'
 import { EventMutation } from '../structs/event-mutation'
@@ -7,15 +6,16 @@ import { diffDates, computeAlignedDayRange } from '../util/misc'
 import { DurationInput, createDuration, durationsEqual } from '../datelib/duration'
 import { createFormatter, FormatterInput } from '../datelib/formatting'
 import { EventSourceApi } from './EventSourceApi'
+import { ReducerContext } from '../reducers/ReducerContext'
 
 export class EventApi {
 
-  _calendar: Calendar
+  _context: ReducerContext
   _def: EventDef
   _instance: EventInstance | null
 
-  constructor(calendar: Calendar, def: EventDef, instance?: EventInstance) {
-    this._calendar = calendar
+  constructor(context: ReducerContext, def: EventDef, instance?: EventInstance) {
+    this._context = context
     this._def = def
     this._instance = instance || null
   }
@@ -68,7 +68,7 @@ export class EventApi {
   }
 
   setStart(startInput: DateInput, options: { granularity?: string, maintainDuration?: boolean } = {}) {
-    let { dateEnv } = this._calendar.state
+    let { dateEnv } = this._context
     let start = dateEnv.createMarker(startInput)
 
     if (start && this._instance) { // TODO: warning if parsed bad
@@ -84,7 +84,7 @@ export class EventApi {
   }
 
   setEnd(endInput: DateInput | null, options: { granularity?: string } = {}) {
-    let { dateEnv } = this._calendar.state
+    let { dateEnv } = this._context
     let end
 
     if (endInput != null) {
@@ -106,7 +106,7 @@ export class EventApi {
   }
 
   setDates(startInput: DateInput, endInput: DateInput | null, options: { allDay?: boolean, granularity?: string } = {}) {
-    let { dateEnv } = this._calendar.state
+    let { dateEnv } = this._context
     let standardProps = { allDay: options.allDay } as any
     let start = dateEnv.createMarker(startInput)
     let end
@@ -179,7 +179,7 @@ export class EventApi {
     let maintainDuration = options.maintainDuration
 
     if (maintainDuration == null) {
-      maintainDuration = this._calendar.state.options.allDayMaintainDuration
+      maintainDuration = this._context.options.allDayMaintainDuration
     }
 
     if (this._def.allDay !== allDay) {
@@ -190,9 +190,9 @@ export class EventApi {
   }
 
   formatRange(formatInput: FormatterInput) {
-    let { dateEnv } = this._calendar.state
+    let { dateEnv } = this._context
     let instance = this._instance
-    let formatter = createFormatter(formatInput, this._calendar.state.options.defaultRangeSeparator)
+    let formatter = createFormatter(formatInput, this._context.options.defaultRangeSeparator)
 
     if (this._def.hasEnd) {
       return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, {
@@ -211,21 +211,21 @@ export class EventApi {
     let instance = this._instance
 
     if (instance) {
-      this._calendar.dispatch({
+      this._context.dispatch({
         type: 'MUTATE_EVENTS',
         instanceId: instance.instanceId,
         mutation,
         fromApi: true
       })
 
-      let eventStore = this._calendar.state.eventStore
+      let { eventStore } = this._context.getCurrentState()
       this._def = eventStore.defs[def.defId]
       this._instance = eventStore.instances[instance.instanceId]
     }
   }
 
   remove() {
-    this._calendar.dispatch({
+    this._context.dispatch({
       type: 'REMOVE_EVENT_DEF',
       defId: this._def.defId
     })
@@ -236,8 +236,8 @@ export class EventApi {
 
     if (sourceId) {
       return new EventSourceApi(
-        this._calendar,
-        this._calendar.state.eventSources[sourceId]
+        this._context,
+        this._context.getCurrentState().eventSources[sourceId]
       )
     }
     return null
@@ -245,13 +245,13 @@ export class EventApi {
 
   get start(): Date | null {
     return this._instance ?
-      this._calendar.state.dateEnv.toDate(this._instance.range.start) :
+      this._context.dateEnv.toDate(this._instance.range.start) :
       null
   }
 
   get end(): Date | null {
     return (this._instance && this._def.hasEnd) ?
-      this._calendar.state.dateEnv.toDate(this._instance.range.end) :
+      this._context.dateEnv.toDate(this._instance.range.end) :
       null
   }
 

+ 7 - 9
packages/core/src/api/EventSourceApi.ts

@@ -1,25 +1,23 @@
-import { Calendar } from '../Calendar'
 import { EventSource } from '../structs/event-source'
+import { ReducerContext } from '../reducers/ReducerContext'
 
 export class EventSourceApi {
 
-  calendar: Calendar
-  internalEventSource: EventSource // rename?
-
-  constructor(calendar: Calendar, internalEventSource: EventSource) {
-    this.calendar = calendar
-    this.internalEventSource = internalEventSource
+  constructor(
+    private context: ReducerContext,
+    public internalEventSource: EventSource // rename?
+  ) {
   }
 
   remove() {
-    this.calendar.dispatch({
+    this.context.dispatch({
       type: 'REMOVE_EVENT_SOURCE',
       sourceId: this.internalEventSource.sourceId
     })
   }
 
   refetch() {
-    this.calendar.dispatch({
+    this.context.dispatch({
       type: 'FETCH_EVENT_SOURCES',
       sourceIds: [ this.internalEventSource.sourceId ]
     })

+ 21 - 3
packages/core/src/calendar-utils.ts

@@ -3,6 +3,7 @@ import { buildDateSpanApi, DateSpanApi, DatePointApi, DateSpan, buildDatePointAp
 import { ReducerContext } from './reducers/ReducerContext'
 import { __assign } from 'tslib'
 import { ViewApi } from './ViewApi'
+import { DateMarker, startOfDay } from './datelib/marker'
 
 
 export interface DateClickApi extends DatePointApi {
@@ -30,7 +31,7 @@ export function triggerDateSelect(selection: DateSpan, pev: PointerDragEvent | n
   const arg = {
     ...buildDateSpanApiWithContext(selection, context),
     jsEvent: pev ? pev.origEvent as MouseEvent : null, // Is this always a mouse event? See #4655
-    view: context.viewApi || context.calendar.view
+    view: context.viewApi || context.calendarApi.view
   }
 
   context.emitter.trigger('select', arg)
@@ -40,7 +41,7 @@ export function triggerDateSelect(selection: DateSpan, pev: PointerDragEvent | n
 export function triggerDateUnselect(pev: PointerDragEvent | null, context: ReducerContext & { viewApi?: ViewApi }) {
   context.emitter.trigger('unselect', {
     jsEvent: pev ? pev.origEvent : null,
-    view: context.viewApi || context.calendar.view
+    view: context.viewApi || context.calendarApi.view
   })
 }
 
@@ -51,7 +52,7 @@ export function triggerDateClick(dateSpan: DateSpan, dayEl: HTMLElement, ev: UIE
     ...buildDatePointApiWithContext(dateSpan, context),
     dayEl,
     jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
-    view: context.viewApi || context.calendar.view
+    view: context.viewApi || context.calendarApi.view
   }
 
   context.emitter.trigger('dateClick', arg)
@@ -82,3 +83,20 @@ export function buildDateSpanApiWithContext(dateSpan: DateSpan, context: Reducer
 
   return props
 }
+
+
+// Given an event's allDay status and start date, return what its fallback end date should be.
+// TODO: rename to computeDefaultEventEnd
+export function getDefaultEventEnd(allDay: boolean, marker: DateMarker, context: ReducerContext): DateMarker {
+  let { dateEnv, computedOptions } = context
+  let end = marker
+
+  if (allDay) {
+    end = startOfDay(end)
+    end = dateEnv.add(end, computedOptions.defaultAllDayEventDuration)
+  } else {
+    end = dateEnv.add(end, computedOptions.defaultTimedEventDuration)
+  }
+
+  return end
+}

+ 5 - 3
packages/core/src/common/Emitter.ts

@@ -3,11 +3,13 @@ import { applyAll } from '../util/misc'
 
 export class Emitter {
 
-  handlers: any = {}
-  options: any
+  private handlers: any = {}
+  private options: any
+  private thisContext: any = null
 
 
-  constructor(private thisContext = null) {
+  setThisContext(thisContext) {
+    this.thisContext = thisContext
   }
 
 

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

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

+ 3 - 3
packages/core/src/component/ComponentContext.ts

@@ -1,4 +1,4 @@
-import { Calendar } from '../Calendar'
+import { CalendarApi } from '../CalendarApi'
 import { ViewApi } from '../ViewApi'
 import { Theme } from '../theme/Theme'
 import { DateEnv } from '../datelib/env'
@@ -45,7 +45,7 @@ export function buildViewContext(
   dispatch: (action: Action) => void,
   getCurrentState: () => CalendarState,
   emitter: Emitter,
-  calendar: Calendar,
+  calendarApi: CalendarApi,
   registerInteractiveComponent: (component: DateComponent<any>, settingsInput: InteractionSettingsInput) => void,
   unregisterInteractiveComponent: (component: DateComponent<any>) => void
 ): ComponentContext {
@@ -57,7 +57,7 @@ export function buildViewContext(
     emitter,
     dispatch,
     getCurrentState,
-    calendar
+    calendarApi
   }
 
   return {

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

@@ -22,7 +22,7 @@ export class EventClicking extends Interaction {
 
   handleSegClick = (ev: Event, segEl: HTMLElement) => {
     let { component } = this
-    let { calendar, viewApi } = component.context
+    let { context } = component
     let seg = getElSeg(segEl)
 
     if (
@@ -35,15 +35,15 @@ export class EventClicking extends Interaction {
       let hasUrlContainer = elementClosest(ev.target as HTMLElement, '.fc-event-forced-url')
       let url = hasUrlContainer ? (hasUrlContainer.querySelector('a[href]') as any).href : ''
 
-      calendar.emitter.trigger('eventClick', {
+      context.emitter.trigger('eventClick', {
         el: segEl,
         event: new EventApi(
-          component.context.calendar,
+          component.context,
           seg.eventRange.def,
           seg.eventRange.instance
         ),
         jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
-        view: viewApi
+        view: context.viewApi
       })
 
       if (url && !ev.defaultPrevented) {

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

@@ -24,12 +24,12 @@ export class EventHovering extends Interaction {
     )
 
     // how to make sure component already has context?
-    component.context.calendar.on('eventElRemove', this.handleEventElRemove)
+    component.context.emitter.on('eventElRemove', this.handleEventElRemove)
   }
 
   destroy() {
     this.removeHoverListeners()
-    this.component.context.calendar.off('eventElRemove', this.handleEventElRemove)
+    this.component.context.emitter.off('eventElRemove', this.handleEventElRemove)
   }
 
   // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
@@ -57,19 +57,19 @@ export class EventHovering extends Interaction {
 
   triggerEvent(publicEvName: 'eventMouseEnter' | 'eventMouseLeave', ev: Event | null, segEl: HTMLElement) {
     let { component } = this
-    let { calendar, viewApi } = component.context
+    let { context } = component
     let seg = getElSeg(segEl)!
 
     if (!ev || component.isValidSegDownEl(ev.target as HTMLElement)) {
-      calendar.emitter.trigger(publicEvName, {
+      context.emitter.trigger(publicEvName, {
         el: segEl,
         event: new EventApi(
-          calendar,
+          context,
           seg.eventRange.def,
           seg.eventRange.instance
         ),
         jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
-        view: viewApi
+        view: context.viewApi
       })
     }
   }

+ 2 - 1
packages/core/src/main.ts

@@ -91,6 +91,7 @@ export { Theme } from './theme/Theme'
 export { ComponentContext, ComponentContextType } from './component/ComponentContext'
 export { DateComponent, Seg, EventSegUiInteractionState } from './component/DateComponent'
 export { Calendar } from './Calendar'
+export { CalendarApi } from './CalendarApi'
 export { ViewProps, sliceEvents } from './View'
 export { ViewApi } from './ViewApi'
 
@@ -201,4 +202,4 @@ export { renderFill, BgEvent, BgEventProps } from './common/bg-fill'
 export { WeekNumberRoot, WeekNumberRootProps } from './common/WeekNumberRoot'
 
 export { ViewRoot, ViewRootProps } from './common/ViewRoot'
-export { triggerDateSelect, triggerDateClick, buildDatePointApiWithContext, DatePointTransform, DateSpanTransform, DateSelectionApi } from './calendar-utils'
+export { triggerDateSelect, triggerDateClick, buildDatePointApiWithContext, DatePointTransform, DateSpanTransform, DateSelectionApi, getDefaultEventEnd } from './calendar-utils'

+ 1 - 1
packages/core/src/option-change-handlers.ts

@@ -43,6 +43,6 @@ function handleEventSources(inputs, context: ReducerContext) {
   }
 
   for (let newInput of newInputs) {
-    context.calendar.addEventSource(newInput)
+    context.calendarApi.addEventSource(newInput)
   }
 }

+ 66 - 8
packages/core/src/reducers/CalendarStateReducer.ts

@@ -3,7 +3,7 @@ import { memoize, memoizeObjArg } from '../util/memoize'
 import { Action, CalendarState } from './types'
 import { PluginHooks, buildPluginHooks } from '../plugin-system'
 import { DateEnv } from '../datelib/env'
-import { Calendar } from '../Calendar'
+import { CalendarApi } from '../CalendarApi'
 import { StandardTheme } from '../theme/StandardTheme'
 import { EventSourceHash } from '../structs/event-source'
 import { buildViewSpecs, ViewSpec } from '../structs/view-spec'
@@ -30,6 +30,7 @@ import { createFormatter } from '../datelib/formatting'
 import { DateRange } from '../datelib/date-range'
 import { ViewApi } from '../ViewApi'
 import { parseBusinessHours } from '../structs/business-hours'
+import { TaskRunner } from '../util/runner'
 
 
 export class CalendarStateReducer {
@@ -52,8 +53,65 @@ export class CalendarStateReducer {
   private buildLocale = memoize(buildLocale)
   private parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours)
 
+  public emitter = new Emitter()
+  private currentState: CalendarState = {} as any
+  private actionRunner = new TaskRunner<Action>(
+    this._handleAction.bind(this),
+    this._handleActionsDrained.bind(this)
+  )
+
+  private calendarApi: CalendarApi
+  private onAction: (action: Action) => void
+  private onState: (state: CalendarState) => void
+
+
+  init(
+    optionOverrides,
+    calendarApi: CalendarApi,
+    onAction?: (action: Action) => void,
+    onState?: (state: CalendarState) => void
+  ) {
+    this.calendarApi = calendarApi
+    this.onAction = onAction
+    this.onState = onState
+
+    this.emitter.setThisContext(calendarApi)
+
+    this.dispatch({
+      type: 'INIT',
+      optionOverrides
+    })
+  }
+
+
+  dispatch = (action) => {
+    this.actionRunner.request(action)
+  }
+
+
+  getCurrentState = () => {
+    return this.currentState
+  }
+
+
+  private _handleAction(action: Action) {
+    this.currentState = this.reduce(this.currentState, action)
+
+    if (this.onAction) {
+      this.onAction(action)
+    }
+  }
+
+
+  private _handleActionsDrained() {
+    if (this.onState) {
+      this.onState(this.currentState)
+    }
+  }
+
 
-  reduce(state: CalendarState, action: Action, dispatch: (action: Action) => void, emitter: Emitter, getCurrentState: () => CalendarState, calendar: Calendar): CalendarState {
+  private reduce(state: CalendarState, action: Action): CalendarState {
+    let { emitter } = this
     let optionOverrides = state.optionOverrides || {}
     let dynamicOptionOverrides = state.dynamicOptionOverrides || {}
 
@@ -143,9 +201,9 @@ export class CalendarStateReducer {
       computedOptions: this.buildComputedOptions(viewOptions),
       pluginHooks,
       emitter,
-      dispatch,
-      getCurrentState,
-      calendar
+      dispatch: this.dispatch,
+      getCurrentState: this.getCurrentState,
+      calendarApi: this.calendarApi
     }
 
     let currentDate = state.currentDate || getInitialDate(reducerContext) // weird how we do INIT
@@ -184,7 +242,7 @@ export class CalendarStateReducer {
     }
 
     let viewTitle = this.computeTitle(dateProfile, viewOptions, dateEnv)
-    let viewApi = this.buildViewApi(viewSpec.type, getCurrentState, dateEnv)
+    let viewApi = this.buildViewApi(viewSpec.type, this.getCurrentState, dateEnv)
 
     let nextState: CalendarState = {
       ...(state as object), // preserve previous state from plugin reducers. tho remove type to make sure all data is provided right now
@@ -211,7 +269,7 @@ export class CalendarStateReducer {
       eventSelection: reduceSelectedEvent(state.eventSelection, action),
       eventDrag: reduceEventDrag(state.eventDrag, action),
       eventResize: reduceEventResize(state.eventResize, action),
-      toolbarConfig: this.parseToolbars(viewOptions, optionOverrides, theme, viewSpecs, calendar),
+      toolbarConfig: this.parseToolbars(viewOptions, optionOverrides, theme, viewSpecs, this.calendarApi),
       viewSpec,
       viewTitle,
       viewApi
@@ -330,7 +388,7 @@ function parseContextBusinessHours(context: ReducerContext) {
 // -----------------------------------------------------------------------------------------------------------------
 
 
-// Computes what the title at the top of the calendar should be for this view
+// Computes what the title at the top of the calendarApi should be for this view
 function computeTitle(dateProfile, viewOptions, dateEnv: DateEnv) {
   let range: DateRange
 

+ 2 - 2
packages/core/src/reducers/ReducerContext.ts

@@ -1,7 +1,7 @@
 import { Action } from './types'
 import { PluginHooks } from '../plugin-system'
 import { DateEnv } from '../datelib/env'
-import { Calendar } from '../Calendar'
+import { CalendarApi } from '../CalendarApi'
 import { Emitter } from '../common/Emitter'
 import { parseFieldSpecs } from '../util/misc'
 import { createDuration, Duration } from '../datelib/duration'
@@ -16,7 +16,7 @@ export interface ReducerContext {
   emitter: Emitter
   dispatch(action: Action): void
   getCurrentState(): CalendarState
-  calendar: Calendar
+  calendarApi: CalendarApi
 }
 
 export interface ComputedOptions {

+ 1 - 1
packages/core/src/reducers/eventStore.ts

@@ -175,7 +175,7 @@ function applyMutationToRelated(eventStore: EventStore, instanceId: string, muta
       textColor: '',
       classNames: []
     } } as EventUiHash :
-    context.calendar.state.eventUiBases
+    context.getCurrentState().eventUiBases
 
 
   relevant = applyMutationToEventStore(relevant, eventConfigBase, mutation, context)

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

@@ -6,6 +6,7 @@ import { startOfDay } from '../datelib/marker'
 import { EventUiHash, EventUi } from '../component/event-ui'
 import { compileEventUis } from '../component/event-rendering'
 import { ReducerContext } from '../reducers/ReducerContext'
+import { getDefaultEventEnd } from '../calendar-utils'
 
 /*
 A data structure for how to modify an EventDef/EventInstance within an EventStore
@@ -119,7 +120,7 @@ function applyMutationToEventInstance(
   if (clearEnd) {
     copy.range = {
       start: copy.range.start,
-      end: context.calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start)
+      end: getDefaultEventEnd(eventDef.allDay, copy.range.start, context)
     }
   }
 
@@ -134,7 +135,7 @@ function applyMutationToEventInstance(
 
   // handle invalid durations
   if (copy.range.end < copy.range.start) {
-    copy.range.end = context.calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start)
+    copy.range.end = getDefaultEventEnd(eventDef.allDay, copy.range.start, context)
   }
 
   return copy

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

@@ -262,7 +262,7 @@ function computeIsDefaultAllDay(sourceId: string, context: ReducerContext): bool
   let res = null
 
   if (sourceId) {
-    let source = context.calendar.state.eventSources[sourceId]
+    let source = context.getCurrentState().eventSources[sourceId]
     res = source.defaultAllDay
   }
 

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

@@ -1,7 +1,7 @@
 import { ViewSpec, ViewSpecHash } from './structs/view-spec'
-import { Calendar } from './Calendar'
 import { Theme } from './theme/Theme'
 import { mapHash } from './util/object'
+import { CalendarApi } from './CalendarApi'
 
 export interface ToolbarModel {
   [sectionName: string]: ToolbarWidget[][]
@@ -21,11 +21,11 @@ export function parseToolbars(
   optionOverrides: any,
   theme: Theme,
   viewSpecs: ViewSpecHash,
-  calendar: Calendar
+  calendarApi: CalendarApi
 ) {
   let viewsWithButtons: string[] = []
-  let headerToolbar = options.headerToolbar ? parseToolbar(options.headerToolbar, options, optionOverrides, theme, viewSpecs, calendar, viewsWithButtons) : null
-  let footerToolbar = options.footerToolbar ? parseToolbar(options.footerToolbar, options, optionOverrides, theme, viewSpecs, calendar, viewsWithButtons) : null
+  let headerToolbar = options.headerToolbar ? parseToolbar(options.headerToolbar, options, optionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) : null
+  let footerToolbar = options.footerToolbar ? parseToolbar(options.footerToolbar, options, optionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) : null
 
   return { headerToolbar, footerToolbar, viewsWithButtons }
 }
@@ -37,10 +37,10 @@ function parseToolbar(
   optionOverrides: any,
   theme: Theme,
   viewSpecs: ViewSpecHash,
-  calendar: Calendar,
+  calendarApi: CalendarApi,
   viewsWithButtons: string[] // dump side effects
 ) : ToolbarModel {
-  return mapHash(sectionStrHash, (sectionStr) => parseSection(sectionStr, options, optionOverrides, theme, viewSpecs, calendar, viewsWithButtons))
+  return mapHash(sectionStrHash, (sectionStr) => parseSection(sectionStr, options, optionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons))
 }
 
 
@@ -53,7 +53,7 @@ function parseSection(
   optionOverrides: any,
   theme: Theme,
   viewSpecs: ViewSpecHash,
-  calendar: Calendar,
+  calendarApi: CalendarApi,
   viewsWithButtons: string[] // dump side effects
 ): ToolbarWidget[][] {
   let isRtl = options.direction === 'rtl'
@@ -89,15 +89,15 @@ function parseSection(
           viewsWithButtons.push(buttonName)
 
           buttonClick = function() {
-            calendar.changeView(buttonName)
+            calendarApi.changeView(buttonName)
           };
           (buttonText = viewSpec.buttonTextOverride) ||
           (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
           (buttonText = viewSpec.buttonTextDefault)
 
-        } else if (calendar[buttonName]) { // a calendar method
+        } else if (calendarApi[buttonName]) { // a calendarApi method
           buttonClick = function() {
-            calendar[buttonName]()
+            calendarApi[buttonName]()
           };
           (buttonText = calendarButtonTextOverrides[buttonName]) ||
           (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||

+ 11 - 10
packages/core/src/validation.ts

@@ -34,7 +34,7 @@ export function isDateSelectionValid(dateSelection: DateSpan, context: ReducerCo
 
 
 function isNewPropsValid(newProps, context: ReducerContext) {
-  let calendarState = context.calendar.state
+  let calendarState = context.getCurrentState()
 
   let props = {
     businessHours: calendarState.businessHours,
@@ -69,7 +69,7 @@ export function isPropsValid(state: SplittableProps, context: ReducerContext, da
 // ------------------------------------------------------------------------------------------------------------------------
 
 function isInteractionPropsValid(state: SplittableProps, context: ReducerContext, dateSpanMeta: any, filterConfig): boolean {
-  let { calendar } = context
+  let currentState = context.getCurrentState()
   let interaction = state.eventDrag // HACK: the eventDrag props is used for ALL interactions
 
   let subjectEventStore = interaction.mutatedEvents
@@ -79,7 +79,7 @@ function isInteractionPropsValid(state: SplittableProps, context: ReducerContext
     subjectDefs,
     interaction.isEvent ?
       state.eventUiBases :
-      { '': calendar.state.selectionConfig } // if not a real event, validate as a selection
+      { '': currentState.selectionConfig } // if not a real event, validate as a selection
   )
 
   if (filterConfig) {
@@ -124,8 +124,8 @@ function isInteractionPropsValid(state: SplittableProps, context: ReducerContext
         }
 
         if (overlapFunc && !overlapFunc(
-          new EventApi(calendar, otherDefs[otherInstance.defId], otherInstance), // still event
-          new EventApi(calendar, subjectDef, subjectInstance) // moving event
+          new EventApi(context, otherDefs[otherInstance.defId], otherInstance), // still event
+          new EventApi(context, subjectDef, subjectInstance) // moving event
         )) {
           return false
         }
@@ -134,7 +134,7 @@ function isInteractionPropsValid(state: SplittableProps, context: ReducerContext
 
     // allow (a function)
 
-    let calendarEventStore = calendar.state.eventStore // need global-to-calendar, not local to component (splittable)state
+    let calendarEventStore = currentState.eventStore // need global-to-calendar, not local to component (splittable)state
 
     for (let subjectAllow of subjectConfig.allows) {
 
@@ -149,9 +149,10 @@ function isInteractionPropsValid(state: SplittableProps, context: ReducerContext
       let eventApi
 
       if (origDef) { // was previously in the calendar
-        eventApi = new EventApi(calendar, origDef, origInstance)
+        eventApi = new EventApi(context, origDef, origInstance)
+
       } else { // was an external event
-        eventApi = new EventApi(calendar, subjectDef) // no instance, because had no dates
+        eventApi = new EventApi(context, subjectDef) // no instance, because had no dates
       }
 
       if (!subjectAllow(
@@ -178,7 +179,7 @@ function isDateSelectionPropsValid(state: SplittableProps, context: ReducerConte
 
   let selection = state.dateSelection
   let selectionRange = selection.range
-  let { selectionConfig } = context.calendar.state
+  let { selectionConfig } = context.getCurrentState()
 
   if (filterConfig) {
     selectionConfig = filterConfig(selectionConfig)
@@ -205,7 +206,7 @@ function isDateSelectionPropsValid(state: SplittableProps, context: ReducerConte
       }
 
       if (overlapFunc && !overlapFunc(
-        new EventApi(context.calendar, relevantDefs[relevantInstance.defId], relevantInstance)
+        new EventApi(context, relevantDefs[relevantInstance.defId], relevantInstance)
       )) {
         return false
       }

+ 7 - 6
packages/daygrid/src/Table.tsx

@@ -177,14 +177,15 @@ export class Table extends DateComponent<TableProps, TableState> {
 
 
   handleMoreLinkClick = (arg: MoreLinkArg) => { // TODO: bad names "more link click" versus "more click"
-    let { calendar, viewApi, options, dateEnv } = this.context
-    let clickOption = options.moreLinkClick
+    let { context } = this
+    let { dateEnv } = context
+    let clickOption = context.options.moreLinkClick
 
     function segForPublic(seg: TableSeg) {
       let { def, instance, range } = seg.eventRange
 
       return {
-        event: new EventApi(calendar, def, instance),
+        event: new EventApi(context, def, instance),
         start: dateEnv.toDate(range.start),
         end: dateEnv.toDate(range.end),
         isStart: seg.isStart,
@@ -195,13 +196,13 @@ export class Table extends DateComponent<TableProps, TableState> {
     if (typeof clickOption === 'function') {
       // the returned value can be an atomic option
       // TODO: weird how we don't use the `clickOption`
-      clickOption = calendar.emitter.trigger('moreLinkClick', {
+      clickOption = context.emitter.trigger('moreLinkClick', {
         date: dateEnv.toDate(arg.date),
         allDay: true,
         allSegs: arg.allSegs.map(segForPublic),
         hiddenSegs: arg.hiddenSegs.map(segForPublic),
         jsEvent: arg.ev as MouseEvent, // TODO: better
-        view: viewApi
+        view: context.viewApi
       })
     }
 
@@ -214,7 +215,7 @@ export class Table extends DateComponent<TableProps, TableState> {
       })
 
     } else if (typeof clickOption === 'string') { // a view name
-      calendar.zoomTo(arg.date, clickOption)
+      context.calendarApi.zoomTo(arg.date, clickOption)
     }
   }
 

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

@@ -15,7 +15,8 @@ import {
   ElementDragging,
   ViewApi,
   ReducerContext,
-  buildDatePointApiWithContext
+  buildDatePointApiWithContext,
+  getDefaultEventEnd
 } from '@fullcalendar/core'
 import { HitDragging } from '../interactions/HitDragging'
 import { __assign } from 'tslib'
@@ -155,7 +156,7 @@ export class ExternalElementDragging {
         receivingContext.emitter.trigger('eventReceive', {
           draggedEl: pev.subjectEl as HTMLElement,
           event: new EventApi(
-            receivingContext.calendar,
+            receivingContext,
             droppableEvent.def,
             droppableEvent.instance
           ),
@@ -228,7 +229,7 @@ function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, context
 
   let end = dragMeta.duration ?
     context.dateEnv.add(start, dragMeta.duration) :
-    context.calendar.getDefaultEventEnd(dateSpan.allDay, start)
+    getDefaultEventEnd(dateSpan.allDay, start, context)
 
   let instance = createEventInstance(def.defId, { start, end })
 

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

@@ -54,11 +54,11 @@ export class DateSelecting extends Interaction {
   }
 
   handleDragStart = (ev: PointerDragEvent) => {
-    this.component.context.calendar.unselect(ev) // unselect previous selections
+    this.component.context.calendarApi.unselect(ev) // unselect previous selections
   }
 
   handleHitUpdate = (hit: Hit | null, isFinal: boolean) => {
-    let { calendar, pluginHooks } = this.component.context
+    let { context } = this.component
     let dragSelection: DateSpan | null = null
     let isInvalid = false
 
@@ -66,7 +66,7 @@ export class DateSelecting extends Interaction {
       dragSelection = joinHitsIntoSelection(
         this.hitDragging.initialHit!,
         hit,
-        pluginHooks.dateSelectionTransformers
+        context.pluginHooks.dateSelectionTransformers
       )
 
       if (!dragSelection || !this.component.isDateSelectionValid(dragSelection)) {
@@ -76,9 +76,9 @@ export class DateSelecting extends Interaction {
     }
 
     if (dragSelection) {
-      calendar.dispatch({ type: 'SELECT_DATES', selection: dragSelection })
+      context.dispatch({ type: 'SELECT_DATES', selection: dragSelection })
     } else if (!isFinal) { // only unselect if moved away while dragging
-      calendar.dispatch({ type: 'UNSELECT_DATES' })
+      context.dispatch({ type: 'UNSELECT_DATES' })
     }
 
     if (!isInvalid) {

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

@@ -13,7 +13,8 @@ import {
   Interaction, InteractionSettings, interactionSettingsStore,
   EventDropTransformers,
   ReducerContext,
-  buildDatePointApiWithContext
+  buildDatePointApiWithContext,
+  Calendar
 } from '@fullcalendar/core'
 import { HitDragging, isHitsEqual } from './HitDragging'
 import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
@@ -67,14 +68,14 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
     let { component, dragging } = this
     let { mirror } = dragging
     let { options } = component.context
-    let initialCalendar = component.context.calendar
+    let initialContext = component.context
     this.subjectEl = ev.subjectEl as HTMLElement
     let subjectSeg = this.subjectSeg = getElSeg(ev.subjectEl as HTMLElement)!
     let eventRange = this.eventRange = subjectSeg.eventRange!
     let eventInstanceId = eventRange.instance!.instanceId
 
     this.relevantEvents = getRelevantEvents(
-      initialCalendar.state.eventStore,
+      initialContext.getCurrentState().eventStore,
       eventInstanceId
     )
 
@@ -85,7 +86,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
         getComponentTouchDelay(component) :
         null
 
-    mirror.parentNode = initialCalendar.el
+    mirror.parentNode = (initialContext.calendarApi as Calendar).el // BAD. will break DnD
     mirror.revertDuration = options.dragRevertDuration
 
     let isValid =
@@ -101,28 +102,27 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
   }
 
   handleDragStart = (ev: PointerDragEvent) => {
-    let { context } = this.component
-    let initialCalendar = context.calendar
+    let initialContext = this.component.context
     let eventRange = this.eventRange!
     let eventInstanceId = eventRange.instance.instanceId
 
     if (ev.isTouch) {
       // need to select a different event?
       if (eventInstanceId !== this.component.props.eventSelection) {
-        initialCalendar.dispatch({ type: 'SELECT_EVENT', eventInstanceId })
+        initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId })
       }
     } else {
       // if now using mouse, but was previous touch interaction, clear selected event
-      initialCalendar.dispatch({ type: 'UNSELECT_EVENT' })
+      initialContext.dispatch({ type: 'UNSELECT_EVENT' })
     }
 
     if (this.isDragging) {
-      initialCalendar.unselect(ev) // unselect *date* selection
-      initialCalendar.emitter.trigger('eventDragStart', {
+      initialContext.calendarApi.unselect(ev) // unselect *date* selection
+      initialContext.emitter.trigger('eventDragStart', {
         el: this.subjectEl,
-        event: new EventApi(initialCalendar, eventRange.def, eventRange.instance),
+        event: new EventApi(initialContext, eventRange.def, eventRange.instance),
         jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
-        view: context.viewApi
+        view: initialContext.viewApi
       })
     }
   }
@@ -157,10 +157,10 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
         initialContext === receivingContext ||
         receivingOptions.editable && receivingOptions.droppable
       ) {
-        mutation = computeEventMutation(initialHit, hit, receivingContext.calendar.state.pluginHooks.eventDragMutationMassagers)
+        mutation = computeEventMutation(initialHit, hit, receivingContext.getCurrentState().pluginHooks.eventDragMutationMassagers)
 
         if (mutation) {
-          mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingContext.calendar.state.eventUiBases, mutation, receivingContext)
+          mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingContext.getCurrentState().eventUiBases, mutation, receivingContext)
           interaction.mutatedEvents = mutatedRelevantEvents
 
           if (!receivingComponent.isInteractionValid(interaction)) {
@@ -222,7 +222,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
       let { receivingContext, validMutation } = this
       let eventDef = this.eventRange!.def
       let eventInstance = this.eventRange!.instance
-      let eventApi = new EventApi(initialContext.calendar, eventDef, eventInstance)
+      let eventApi = new EventApi(initialContext, eventDef, eventInstance)
       let relevantEvents = this.relevantEvents!
       let mutatedRelevantEvents = this.mutatedRelevantEvents!
       let { finalHit } = this.hitDragging
@@ -248,7 +248,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
 
           let transformed: ReturnType<EventDropTransformers> = {}
 
-          for (let transformer of initialContext.calendar.state.pluginHooks.eventDropTransformers) {
+          for (let transformer of initialContext.getCurrentState().pluginHooks.eventDropTransformers) {
             __assign(transformed, transformer(validMutation, initialContext))
           }
 
@@ -258,7 +258,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
             delta: validMutation.datesDelta!,
             oldEvent: eventApi,
             event: new EventApi( // the data AFTER the mutation
-              initialContext.calendar,
+              initialContext,
               mutatedRelevantEvents.defs[eventDef.defId],
               eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null
             ),
@@ -310,7 +310,7 @@ export class EventDragging extends Interaction { // TODO: rename to EventSelecti
           receivingContext.emitter.trigger('eventReceive', {
             draggedEl: ev.subjectEl as HTMLElement,
             event: new EventApi( // the data AFTER the mutation
-              receivingContext.calendar,
+              receivingContext,
               mutatedRelevantEvents.defs[eventDef.defId],
               mutatedRelevantEvents.instances[eventInstance.instanceId]
             ),

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

@@ -67,11 +67,11 @@ export class EventResizing extends Interaction {
   }
 
   handleDragStart = (ev: PointerDragEvent) => {
-    let { calendar, viewApi } = this.component.context
+    let { context } = this.component
     let eventRange = this.eventRange!
 
     this.relevantEvents = getRelevantEvents(
-      calendar.state.eventStore,
+      context.getCurrentState().eventStore,
       this.eventRange.instance!.instanceId
     )
 
@@ -79,12 +79,12 @@ export class EventResizing extends Interaction {
     this.draggingSegEl = segEl
     this.draggingSeg = getElSeg(segEl)
 
-    calendar.unselect()
-    calendar.emitter.trigger('eventResizeStart', {
+    context.calendarApi.unselect()
+    context.emitter.trigger('eventResizeStart', {
       el: segEl,
-      event: new EventApi(calendar, eventRange.def, eventRange.instance),
+      event: new EventApi(context, eventRange.def, eventRange.instance),
       jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
-      view: viewApi
+      view: context.viewApi
     })
   }
 
@@ -113,7 +113,7 @@ export class EventResizing extends Interaction {
     }
 
     if (mutation) {
-      mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.calendar.state.eventUiBases, mutation, context)
+      mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentState().eventUiBases, mutation, context)
       interaction.mutatedEvents = mutatedRelevantEvents
 
       if (!this.component.isInteractionValid(interaction)) {
@@ -152,48 +152,48 @@ export class EventResizing extends Interaction {
   }
 
   handleDragEnd = (ev: PointerDragEvent) => {
-    let { calendar, viewApi } = this.component.context
+    let { context } = this.component
     let eventDef = this.eventRange!.def
     let eventInstance = this.eventRange!.instance
-    let eventApi = new EventApi(calendar, eventDef, eventInstance)
+    let eventApi = new EventApi(context, eventDef, eventInstance)
     let relevantEvents = this.relevantEvents!
     let mutatedRelevantEvents = this.mutatedRelevantEvents!
 
-    calendar.emitter.trigger('eventResizeStop', {
+    context.emitter.trigger('eventResizeStop', {
       el: this.draggingSegEl,
       event: eventApi,
       jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
-      view: viewApi
+      view: context.viewApi
     })
 
     if (this.validMutation) {
-      calendar.dispatch({
+      context.dispatch({
         type: 'MERGE_EVENTS',
         eventStore: mutatedRelevantEvents
       })
 
-      calendar.emitter.trigger('eventResize', {
+      context.emitter.trigger('eventResize', {
         el: this.draggingSegEl,
         startDelta: this.validMutation.startDelta || createDuration(0),
         endDelta: this.validMutation.endDelta || createDuration(0),
         prevEvent: eventApi,
         event: new EventApi( // the data AFTER the mutation
-          calendar,
+          context,
           mutatedRelevantEvents.defs[eventDef.defId],
           eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null
         ),
         revert: function() {
-          calendar.dispatch({
+          context.dispatch({
             type: 'MERGE_EVENTS',
             eventStore: relevantEvents
           })
         },
         jsEvent: ev.origEvent,
-        view: viewApi
+        view: context.viewApi
       })
 
     } else {
-      calendar.emitter.trigger('_noEventResize')
+      context.emitter.trigger('_noEventResize')
     }
 
     // reset all internal state

+ 5 - 4
packages/interaction/src/interactions/UnselectAuto.ts

@@ -38,27 +38,28 @@ export class UnselectAuto {
   onDocumentPointerUp = (pev: PointerDragEvent) => {
     let { context } = this
     let { documentPointer } = this
+    let calendarState = context.getCurrentState()
 
     // touch-scrolling should never unfocus any type of selection
     if (!documentPointer.wasTouchScroll) {
 
       if (
-        context.calendar.state.dateSelection && // an existing date selection?
+        calendarState.dateSelection && // an existing date selection?
         !this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp?
       ) {
         let unselectAuto = context.options.unselectAuto
         let unselectCancel = context.options.unselectCancel
 
         if (unselectAuto && (!unselectAuto || !elementClosest(documentPointer.downEl, unselectCancel))) {
-          context.calendar.unselect(pev)
+          context.calendarApi.unselect(pev)
         }
       }
 
       if (
-        context.calendar.state.eventSelection && // an existing event selected?
+        calendarState.eventSelection && // an existing event selected?
         !elementClosest(documentPointer.downEl, EventDragging.SELECTOR) // interaction DIDN'T start on an event
       ) {
-        context.calendar.dispatch({ type: 'UNSELECT_EVENT' })
+        context.dispatch({ type: 'UNSELECT_EVENT' })
       }
 
     }

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

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