Adam Shaw 5 лет назад
Родитель
Сommit
afc48635cd
86 измененных файлов с 1340 добавлено и 872 удалено
  1. 1 1
      packages-premium
  2. 3 3
      packages/__tests__/src/datelib/luxon.js
  3. 2 2
      packages/__tests__/src/datelib/moment.js
  4. 4 4
      packages/__tests__/src/lib/wrappers/interaction-util.ts
  5. 1 1
      packages/bootstrap/src/main.ts
  6. 10 9
      packages/common/src/CalendarApi.tsx
  7. 4 5
      packages/common/src/CalendarContent.tsx
  8. 2 3
      packages/common/src/CalendarContext.ts
  9. 0 26
      packages/common/src/ComputedOptions.ts
  10. 38 39
      packages/common/src/DateProfileGenerator.ts
  11. 1 1
      packages/common/src/NowTimer.ts
  12. 3 3
      packages/common/src/ViewContext.ts
  13. 3 5
      packages/common/src/api/EventApi.ts
  14. 3 3
      packages/common/src/calendar-utils.ts
  15. 42 30
      packages/common/src/common/DayCellRoot.tsx
  16. 3 6
      packages/common/src/common/DayHeader.tsx
  17. 3 3
      packages/common/src/common/Emitter.ts
  18. 5 1
      packages/common/src/common/EventRoot.tsx
  19. 16 2
      packages/common/src/common/NowIndicatorRoot.tsx
  20. 3 11
      packages/common/src/common/StandardEvent.tsx
  21. 21 14
      packages/common/src/common/TableDateCell.tsx
  22. 16 5
      packages/common/src/common/ViewRoot.tsx
  23. 21 7
      packages/common/src/common/WeekNumberRoot.tsx
  24. 54 58
      packages/common/src/common/render-hook.tsx
  25. 4 3
      packages/common/src/common/table-utils.ts
  26. 44 51
      packages/common/src/component/event-ui.ts
  27. 2 1
      packages/common/src/datelib/DateFormatter.ts
  28. 2 2
      packages/common/src/datelib/duration.ts
  29. 9 1
      packages/common/src/datelib/env.ts
  30. 2 2
      packages/common/src/datelib/formatting-native.ts
  31. 5 6
      packages/common/src/datelib/locale.ts
  32. 3 3
      packages/common/src/formatting-api.ts
  33. 4 0
      packages/common/src/global-config.ts
  34. 1 1
      packages/common/src/global-plugins.ts
  35. 16 7
      packages/common/src/main.ts
  36. 432 41
      packages/common/src/options.ts
  37. 3 0
      packages/common/src/plugin-system-struct.ts
  38. 6 3
      packages/common/src/plugin-system.ts
  39. 1 1
      packages/common/src/reducers/Action.ts
  40. 200 87
      packages/common/src/reducers/CalendarDataManager.ts
  41. 9 9
      packages/common/src/reducers/current-date.ts
  42. 6 7
      packages/common/src/reducers/data-types.ts
  43. 1 1
      packages/common/src/reducers/options.ts
  44. 4 3
      packages/common/src/reducers/title-formatting.ts
  45. 3 2
      packages/common/src/scrollgrid/util.tsx
  46. 5 5
      packages/common/src/structs/event-parse.ts
  47. 2 2
      packages/common/src/structs/event-source-parse.ts
  48. 3 3
      packages/common/src/structs/recurring-event.ts
  49. 31 28
      packages/common/src/structs/view-config.tsx
  50. 5 4
      packages/common/src/structs/view-def.ts
  51. 8 8
      packages/common/src/structs/view-spec.ts
  52. 1 1
      packages/common/src/theme/StandardTheme.ts
  53. 2 1
      packages/common/src/theme/Theme.ts
  54. 20 9
      packages/common/src/toolbar-parse.ts
  55. 0 217
      packages/common/src/types/input-types.ts
  56. 1 1
      packages/common/src/util/html.ts
  57. 2 6
      packages/common/src/util/misc.ts
  58. 8 7
      packages/common/src/validation.ts
  59. 2 1
      packages/common/src/vdom-util.tsx
  60. 2 2
      packages/core/src/Calendar.tsx
  61. 2 2
      packages/daygrid/src/DayTableView.tsx
  62. 2 2
      packages/daygrid/src/MorePopover.tsx
  63. 1 1
      packages/daygrid/src/Table.tsx
  64. 21 3
      packages/daygrid/src/TableCell.tsx
  65. 2 9
      packages/daygrid/src/TableListItemEvent.tsx
  66. 1 1
      packages/daygrid/src/TableRow.tsx
  67. 3 3
      packages/daygrid/src/event-rendering.ts
  68. 2 0
      packages/daygrid/src/main.ts
  69. 35 0
      packages/daygrid/src/options.ts
  70. 4 8
      packages/google-calendar/src/main.ts
  71. 10 0
      packages/google-calendar/src/options.ts
  72. 3 3
      packages/interaction/src/interactions-external/ExternalDraggable.ts
  73. 1 0
      packages/interaction/src/interactions-external/ExternalElementDragging.ts
  74. 24 10
      packages/list/src/ListView.tsx
  75. 15 12
      packages/list/src/ListViewEventRow.tsx
  76. 13 8
      packages/list/src/ListViewHeaderRow.tsx
  77. 2 0
      packages/list/src/main.ts
  78. 25 0
      packages/list/src/options.ts
  79. 5 5
      packages/timegrid/src/DayTimeColsView.tsx
  80. 1 1
      packages/timegrid/src/TimeCol.tsx
  81. 3 3
      packages/timegrid/src/TimeColEvent.tsx
  82. 2 2
      packages/timegrid/src/TimeCols.tsx
  83. 27 24
      packages/timegrid/src/TimeColsSlats.tsx
  84. 16 7
      packages/timegrid/src/TimeColsView.tsx
  85. 2 0
      packages/timegrid/src/main.ts
  86. 10 0
      packages/timegrid/src/options.ts

+ 1 - 1
packages-premium

@@ -1 +1 @@
-Subproject commit 9bc55cc974b53147bd515a133484ae5b621e6534
+Subproject commit 3a27852b4057804601eafebc8336879434ed7599

+ 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.getCurrentData().computedOptions.defaultTimedEventDuration, calendar)
-      let allDayDuration = toLuxonDuration(calendar.getCurrentData().computedOptions.defaultAllDayEventDuration, calendar)
+      let timedDuration = toLuxonDuration(calendar.getCurrentData().options.defaultTimedEventDuration, calendar)
+      let allDayDuration = toLuxonDuration(calendar.getCurrentData().options.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.getCurrentData().computedOptions.defaultTimedEventDuration, calendar)
+      let timedDuration = toLuxonDuration(calendar.getCurrentData().options.defaultTimedEventDuration, calendar)
 
       expect(timedDuration.locale).toBe('es')
     })

+ 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.getCurrentData().computedOptions.defaultTimedEventDuration)
-      let allDayDuration = toMomentDuration(calendar.getCurrentData().computedOptions.defaultAllDayEventDuration)
+      let timedDuration = toMomentDuration(calendar.getCurrentData().options.defaultTimedEventDuration)
+      let allDayDuration = toMomentDuration(calendar.getCurrentData().options.defaultAllDayEventDuration)
 
       expect(timedDuration.asHours()).toBe(5)
       expect(allDayDuration.asDays()).toBe(3)

+ 4 - 4
packages/__tests__/src/lib/wrappers/interaction-util.ts

@@ -3,7 +3,7 @@ import { Calendar } from '@fullcalendar/core'
 
 export function waitEventDrag(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
-    let modifiedEvent = false
+    let modifiedEvent: any = false
 
     calendar.on('eventDrop', function(arg) {
       modifiedEvent = arg.event
@@ -24,7 +24,7 @@ export function waitEventDrag(calendar: Calendar, dragging: Promise<any>) {
 
 export function waitEventDrag2(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
-    let theArg = false
+    let theArg: any = false
 
     calendar.on('eventDrop', function(arg) {
       theArg = arg
@@ -45,7 +45,7 @@ export function waitEventDrag2(calendar: Calendar, dragging: Promise<any>) {
 
 export function waitEventResize(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
-    let modifiedEvent = false
+    let modifiedEvent: any = false
 
     calendar.on('eventResize', function(arg) {
       modifiedEvent = arg.event
@@ -62,7 +62,7 @@ export function waitEventResize(calendar: Calendar, dragging: Promise<any>) {
 
 export function waitEventResize2(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
-    let theArg = false
+    let theArg: any = false
 
     calendar.on('eventResize', function(arg) {
       theArg = arg

+ 1 - 1
packages/bootstrap/src/main.ts

@@ -32,7 +32,7 @@ BootstrapTheme.prototype.rtlIconClasses = {
   nextYear: 'fa-angle-double-left'
 }
 
-BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome'
+BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome' // TODO: make TS-friendly. move the option-processing into this plugin
 BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome'
 BootstrapTheme.prototype.iconOverridePrefix = 'fa-'
 

+ 10 - 9
packages/common/src/CalendarApi.tsx

@@ -18,6 +18,7 @@ import { triggerDateSelect, triggerDateUnselect } from './calendar-utils'
 import { CalendarDataManager } from './reducers/CalendarDataManager'
 import { Action } from './reducers/Action'
 import { EventSource } from './structs/event-source'
+import { RawCalendarOptions, CalendarListeners } from './options'
 
 
 export class CalendarApi {
@@ -49,17 +50,17 @@ export class CalendarApi {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  setOption(name: string, val) {
+  setOption<OptionName extends keyof RawCalendarOptions>(name: OptionName, val: RawCalendarOptions[OptionName]) {
     this.dispatch({
       type: 'SET_OPTION',
       optionName: name,
-      optionValue: val
+      rawOptionValue: val
     })
   }
 
 
-  getOption(name: string) { // getter, used externally
-    return this.getCurrentData().calendarOptions[name]
+  getOption(name: keyof RawCalendarOptions) { // getter, used externally
+    return this.currentDataManager!.currentRawCalendarOptions[name]
   }
 
 
@@ -72,17 +73,17 @@ export class CalendarApi {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  on(handlerName: string, handler) {
+  on<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]) {
     this.currentDataManager!.emitter.on(handlerName, handler)
   }
 
 
-  off(handlerName: string, handler) {
+  off<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]) {
     this.currentDataManager!.emitter.off(handlerName, handler)
   }
 
 
-  protected trigger(handlerName, ...args) {
+  protected trigger<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, ...args: Parameters<CalendarListeners[ListenerName]>) {
     this.currentDataManager!.emitter.trigger(handlerName, ...args)
   }
 
@@ -105,7 +106,7 @@ export class CalendarApi {
           this.dispatch({ // not very efficient to do two dispatches
             type: 'SET_OPTION',
             optionName: 'visibleRange',
-            optionValue: dateOrRange
+            rawOptionValue: dateOrRange
           })
 
         } else {
@@ -222,7 +223,7 @@ export class CalendarApi {
     this.unselect()
     this.dispatch({
       type: 'CHANGE_DATE',
-      dateMarker: getNow(state.calendarOptions, state.dateEnv)
+      dateMarker: getNow(state.calendarOptions.now, state.dateEnv)
     })
   }
 

+ 4 - 5
packages/common/src/CalendarContent.tsx

@@ -11,7 +11,6 @@ import { ViewPropsTransformerClass } from './plugin-system-struct'
 import { __assign } from 'tslib'
 import { h, createRef, Component, VUIEvent } from './vdom'
 import { buildDelegationHandler } from './util/dom-event'
-import { capitaliseFirstLetter } from './util/misc'
 import { ViewContainer } from './ViewContainer'
 import { CssDimValue } from './scrollgrid/util'
 import { getCanVGrowWithinCell } from './util/table-styling'
@@ -61,7 +60,7 @@ export class CalendarContent extends Component<CalendarContentProps, CalendarCon
       props.dateProfile,
       props.dateProfileGenerator,
       props.currentDate,
-      getNow(props.options, props.dateEnv), // TODO: use NowTimer????
+      getNow(props.options.now, props.dateEnv), // TODO: use NowTimer????
       props.viewTitle
     )
 
@@ -86,7 +85,6 @@ export class CalendarContent extends Component<CalendarContentProps, CalendarCon
       props.viewSpec,
       props.viewApi,
       props.options,
-      props.computedOptions,
       props.dateProfileGenerator,
       props.dateEnv,
       props.theme,
@@ -185,8 +183,9 @@ export class CalendarContent extends Component<CalendarContentProps, CalendarCon
     let dateMarker = dateEnv.createMarker(navLinkOptions.date)
     let viewType = navLinkOptions.type
 
-    // property like "navLinkDayClick". might be a string or a function
-    let customAction = options['navLink' + capitaliseFirstLetter(viewType) + 'Click']
+    let customAction =
+      viewType === 'day' ? options.navLinkDayClick :
+      viewType === 'week' ? options.navLinkWeekClick : null
 
     if (typeof customAction === 'function') {
       customAction(dateEnv.toDate(dateMarker), ev)

+ 2 - 3
packages/common/src/CalendarContext.ts

@@ -1,5 +1,5 @@
 import { DateEnv } from './datelib/env'
-import { ComputedOptions } from './ComputedOptions'
+import { RefinedBaseOptions } from './options'
 import { PluginHooks } from './plugin-system-struct'
 import { Emitter } from './common/Emitter'
 import { Action } from './reducers/Action'
@@ -8,8 +8,7 @@ import { CalendarData } from './reducers/data-types'
 
 export interface CalendarContext {
   dateEnv: DateEnv
-  options: any
-  computedOptions: ComputedOptions
+  options: RefinedBaseOptions // does not have calendar-specific properties. aims to be compatible with RefinedViewOptions
   pluginHooks: PluginHooks
   emitter: Emitter
   dispatch(action: Action): void

+ 0 - 26
packages/common/src/ComputedOptions.ts

@@ -1,26 +0,0 @@
-import { Duration, createDuration } from './datelib/duration'
-import { parseFieldSpecs } from './util/misc'
-
-export interface ComputedOptions {
-  eventOrderSpecs: any
-  nextDayThreshold: Duration
-  defaultAllDayEventDuration: Duration
-  defaultTimedEventDuration: Duration
-  slotDuration: Duration | null
-  snapDuration: Duration | null
-  slotMinTime: Duration
-  slotMaxTime: Duration
-}
-
-export function buildComputedOptions(options: any): ComputedOptions {
-  return {
-    eventOrderSpecs: parseFieldSpecs(options.eventOrder),
-    nextDayThreshold: createDuration(options.nextDayThreshold),
-    defaultAllDayEventDuration: createDuration(options.defaultAllDayEventDuration),
-    defaultTimedEventDuration: createDuration(options.defaultTimedEventDuration),
-    slotDuration: options.slotDuration ? createDuration(options.slotDuration) : null,
-    snapDuration: options.snapDuration ? createDuration(options.snapDuration) : null,
-    slotMinTime: createDuration(options.slotMinTime),
-    slotMaxTime: createDuration(options.slotMaxTime)
-  }
-}

+ 38 - 39
packages/common/src/DateProfileGenerator.ts

@@ -1,6 +1,6 @@
 import { DateMarker, startOfDay, addDays } from './datelib/marker'
-import { Duration, createDuration, getWeeksFromInput, asRoughDays, asRoughMs, greatestDurationDenominator, DurationInput } from './datelib/duration'
-import { DateRange, OpenDateRange, constrainMarkerToRange, intersectRanges, rangesIntersect, parseRange } from './datelib/date-range'
+import { Duration, createDuration, getWeeksFromInput, asRoughDays, asRoughMs, greatestDurationDenominator } from './datelib/duration'
+import { DateRange, OpenDateRange, constrainMarkerToRange, intersectRanges, rangesIntersect, parseRange, DateRangeInput } from './datelib/date-range'
 import { ViewSpec } from './structs/view-spec'
 import { DateEnv, DateInput } from './datelib/env'
 import { computeVisibleDayRange } from './util/date'
@@ -26,34 +26,30 @@ export interface DateProfileGeneratorProps extends DateProfileOptions {
 }
 
 export interface DateProfileOptions {
-  slotMinTime: DurationInput
-  slotMaxTime: DurationInput
+  slotMinTime: Duration
+  slotMaxTime: Duration
   showNonCurrentDates?: boolean
   dayCount?: number
   dateAlignment?: string
-  dateIncrement?: DurationInput
+  dateIncrement?: Duration
   hiddenDays?: number[]
   weekends?: boolean
-  now?: DateInput // for getNow
-  validRange?: OpenDateRange // for getRangeOption
-  visibleRange?: OpenDateRange // for getRangeOption
+  nowInput?: DateInput | (() => DateInput)
+  validRangeInput?: DateRangeInput | ((nowDate: Date) => DateRangeInput)
+  visibleRangeInput?: DateRangeInput | ((nowDate: Date) => DateRangeInput)
   monthMode?: boolean
-  fixedWeekCount?: number
+  fixedWeekCount?: boolean
 }
 
 
 export class DateProfileGenerator { // only publicly used for isHiddenDay :(
 
-  slotMinTime: Duration
-  slotMaxTime: Duration
   nowDate: DateMarker
   isHiddenDayHash: boolean[]
 
 
   constructor(protected props: DateProfileGeneratorProps) {
-    this.slotMinTime = createDuration(props.slotMinTime) // TODO: use parsed. but need better options system
-    this.slotMaxTime = createDuration(props.slotMaxTime)
-    this.nowDate = getNow(props, props.dateEnv) // uses props.now. bad system
+    this.nowDate = getNow(props.nowInput, props.dateEnv)
     this.initHiddenDays()
   }
 
@@ -92,6 +88,7 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
   // Optional direction param indicates whether the date is being incremented/decremented
   // from its previous value. decremented = -1, incremented = 1 (default).
   build(currentDate: DateMarker, direction?, forceToValid = true): DateProfile {
+    let { props } = this
     let validRange: DateRange
     let currentInfo
     let isRangeAllDay
@@ -116,7 +113,7 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
     renderRange = this.trimHiddenDays(renderRange)
     activeRange = renderRange
 
-    if (!this.props.showNonCurrentDates) {
+    if (!props.showNonCurrentDates) {
       activeRange = intersectRanges(activeRange, currentInfo.range)
     }
 
@@ -150,10 +147,10 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
       renderRange,
 
       // Duration object that denotes the first visible time of any given day
-      slotMinTime: this.slotMinTime,
+      slotMinTime: props.slotMinTime,
 
       // Duration object that denotes the exclusive visible end time of any given day
-      slotMaxTime: this.slotMaxTime,
+      slotMaxTime: props.slotMaxTime,
 
       isValid,
 
@@ -168,7 +165,12 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
   // Indicates the minimum/maximum dates to display.
   // not responsible for trimming hidden days.
   buildValidRange(): OpenDateRange {
-    return this.getRangeOption('validRange', this.nowDate) ||
+    let input = this.props.validRangeInput
+    let simpleInput = typeof input === 'function'
+      ? input(this.nowDate)
+      : input
+
+    return this.refineRange(simpleInput) ||
       { start: null, end: null } // completely open-ended
   }
 
@@ -211,8 +213,7 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
   // Returns a new activeRange to have time values (un-ambiguate)
   // slotMinTime or slotMaxTime causes the range to expand.
   adjustActiveRange(range: DateRange) {
-    let { dateEnv, viewSpec } = this.props
-    let { slotMinTime, slotMaxTime } = this
+    let { dateEnv, viewSpec, slotMinTime, slotMaxTime } = this.props
     let start = range.start
     let end = range.end
 
@@ -322,14 +323,19 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
   // Builds a normalized range object for the "visible" range,
   // which is a way to define the currentRange and activeRange at the same time.
   buildCustomVisibleRange(date: DateMarker) {
-    let { dateEnv } = this.props
-    let visibleRange = this.getRangeOption('visibleRange', dateEnv.toDate(date))
+    let { props } = this
+    let input = props.visibleRangeInput
+    let simpleInput = typeof input === 'function'
+      ? input(props.dateEnv.toDate(date))
+      : input
 
-    if (visibleRange && (visibleRange.start == null || visibleRange.end == null)) {
+    let range = this.refineRange(simpleInput)
+
+    if (range && (range.start == null || range.end == null)) {
       return null
     }
 
-    return visibleRange
+    return range
   }
 
 
@@ -359,25 +365,18 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
   }
 
 
-  // Arguments after name will be forwarded to a hypothetical function value
-  // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
-  // Always clone your objects if you fear mutation.
-  getRangeOption(name, ...otherArgs): OpenDateRange {
-    let val = this.props[name]
-
-    if (typeof val === 'function') {
-      val = val.apply(null, otherArgs)
-    }
+  refineRange(rangeInput: DateRangeInput | undefined): DateRange | null {
+    if (rangeInput) {
+      let range = parseRange(rangeInput, this.props.dateEnv)
 
-    if (val) {
-      val = parseRange(val, this.props.dateEnv)
-    }
+      if (range) {
+        range = computeVisibleDayRange(range)
+      }
 
-    if (val) {
-      val = computeVisibleDayRange(val)
+      return range
     }
 
-    return val
+    return null
   }
 
 

+ 1 - 1
packages/common/src/NowTimer.ts

@@ -30,7 +30,7 @@ export class NowTimer extends Component<NowTimerProps, NowTimerState> {
   constructor(props: NowTimerProps, context: ViewContext) {
     super(props, context)
 
-    this.initialNowDate = getNow(context.options, context.dateEnv)
+    this.initialNowDate = getNow(context.options.now, context.dateEnv)
     this.initialNowQueriedMs = new Date().valueOf()
 
     this.state = this.computeTiming().currentState

+ 3 - 3
packages/common/src/ViewContext.ts

@@ -14,6 +14,7 @@ import { InteractionSettingsInput } from './interactions/interaction'
 import { DateComponent } from './component/DateComponent'
 import { CalendarContext } from './CalendarContext'
 import { createDuration } from './datelib/duration'
+import { RefinedViewOptions } from './options'
 
 export const ViewContextType = createContext<ViewContext>({} as any) // for Components
 export type ResizeHandler = (force: boolean) => void
@@ -23,6 +24,7 @@ it's important that ViewContext extends CalendarContext so that components that
 can pass in their ViewContext to util functions that accept CalendarContext.
 */
 export interface ViewContext extends CalendarContext {
+  options: RefinedViewOptions // more specific than RefinedBaseOptions
   theme: Theme
   isRtl: boolean
   dateProfileGenerator: DateProfileGenerator
@@ -38,8 +40,7 @@ export interface ViewContext extends CalendarContext {
 export function buildViewContext(
   viewSpec: ViewSpec,
   viewApi: ViewApi,
-  viewOptions: any,
-  computedViewOptions: any,
+  viewOptions: RefinedViewOptions,
   dateProfileGenerator: DateProfileGenerator,
   dateEnv: DateEnv,
   theme: Theme,
@@ -54,7 +55,6 @@ export function buildViewContext(
   return {
     dateEnv,
     options: viewOptions,
-    computedOptions: computedViewOptions,
     pluginHooks,
     emitter,
     dispatch,

+ 3 - 5
packages/common/src/api/EventApi.ts

@@ -1,6 +1,6 @@
 import { EventDef, NON_DATE_PROPS, DATE_PROPS } from '../structs/event-def'
 import { EventInstance } from '../structs/event-instance'
-import { UNSCOPED_EVENT_UI_PROPS } from '../component/event-ui'
+import { UI_PROPS_REFINERS } from '../component/event-ui'
 import { EventMutation } from '../structs/event-mutation'
 import { DateInput } from '../datelib/env'
 import { diffDates, computeAlignedDayRange } from '../util/date'
@@ -38,12 +38,10 @@ export class EventApi {
         standardProps: { [name]: val }
       })
 
-    } else if (name in UNSCOPED_EVENT_UI_PROPS) {
+    } else if (name in UI_PROPS_REFINERS) {
       let ui
 
-      if (typeof UNSCOPED_EVENT_UI_PROPS[name] === 'function') {
-        val = UNSCOPED_EVENT_UI_PROPS[name](val)
-      }
+      val = UI_PROPS_REFINERS[name](val)
 
       if (name === 'color') {
         ui = { backgroundColor: val, borderColor: val }

+ 3 - 3
packages/common/src/calendar-utils.ts

@@ -88,14 +88,14 @@ export function buildDateSpanApiWithContext(dateSpan: DateSpan, context: Calenda
 // 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: CalendarContext): DateMarker {
-  let { dateEnv, computedOptions } = context
+  let { dateEnv, options } = context
   let end = marker
 
   if (allDay) {
     end = startOfDay(end)
-    end = dateEnv.add(end, computedOptions.defaultAllDayEventDuration)
+    end = dateEnv.add(end, options.defaultAllDayEventDuration)
   } else {
-    end = dateEnv.add(end, computedOptions.defaultTimedEventDuration)
+    end = dateEnv.add(end, options.defaultTimedEventDuration)
   }
 
   return end

+ 42 - 30
packages/common/src/common/DayCellRoot.tsx

@@ -1,23 +1,27 @@
 import { Ref, ComponentChildren, h } from '../vdom'
 import { DateMarker } from '../datelib/marker'
 import { DateRange } from '../datelib/date-range'
-import { ViewContext } from '../ViewContext'
 import { getDateMeta, getDayClassNames, DateMeta } from '../component/date-rendering'
 import { createFormatter } from '../datelib/formatting'
 import { formatDayString } from '../datelib/formatting-utils'
-import { buildHookClassNameGenerator, MountHook, ContentHook } from './render-hook'
+import { buildClassNameNormalizer, MountHook, ContentHook } from './render-hook'
 import { ViewApi } from '../ViewApi'
 import { BaseComponent } from '../vdom-util'
 import { DateProfile } from '../DateProfileGenerator'
+import { memoizeObjArg } from '../util/memoize'
+import { DateEnv } from '../datelib/env'
 
 
 const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
 
-interface DayCellHookPropOrigin {
+interface RawDayCellHookProps {
   date: DateMarker // generic
   dateProfile: DateProfile
   todayRange: DateRange
+  dateEnv: DateEnv
+  viewApi: ViewApi
   showDayNumber?: boolean // defaults to false
+  extraProps?: object // so can include a resource
 }
 
 export interface DayCellHookProps extends DateMeta {
@@ -45,27 +49,27 @@ export interface DayCellRootProps {
 
 export class DayCellRoot extends BaseComponent<DayCellRootProps> {
 
-  buildClassNames = buildHookClassNameGenerator<DayCellHookProps>('dayCell')
+  refineHookProps = memoizeObjArg(refineHookProps)
+  normalizeClassNames = buildClassNameNormalizer<DayCellHookProps>()
 
 
   render() {
     let { props, context } = this
-
-    let hookPropsOrigin: DayCellHookPropOrigin = {
+    let { options } = context
+    let hookProps = this.refineHookProps({
       date: props.date,
       dateProfile: props.dateProfile,
       todayRange: props.todayRange,
-      showDayNumber: props.showDayNumber
-    }
-    let hookProps = { // it's weird to rely on this internally so much (isDisabled)
-      ...massageHooksProps(hookPropsOrigin, context),
-      ...props.extraHookProps
-    }
+      showDayNumber: props.showDayNumber,
+      extraProps: props.extraHookProps,
+      viewApi: context.viewApi,
+      dateEnv: context.dateEnv
+    })
 
     let classNames = getDayClassNames(hookProps, context.theme).concat(
       hookProps.isDisabled
-        ? [] // don't use custom classNames if disalbed
-        : this.buildClassNames(hookProps, context, null, hookPropsOrigin) // cacheBuster=hookPropsOrigin
+        ? [] // don't use custom classNames if disabled
+        : this.normalizeClassNames(options.dayCellClassNames, hookProps)
     )
 
     let dataAttrs = hookProps.isDisabled ? {} : {
@@ -73,7 +77,12 @@ export class DayCellRoot extends BaseComponent<DayCellRootProps> {
     }
 
     return (
-      <MountHook name='dayCell' hookProps={hookProps} elRef={props.elRef}>
+      <MountHook
+        hookProps={hookProps}
+        didMount={options.dayCellDidMount}
+        willUnmount={options.dayCellWillUnmount}
+        elRef={props.elRef}
+      >
         {(rootElRef) => props.children(rootElRef, classNames, dataAttrs, hookProps.isDisabled)}
       </MountHook>
     )
@@ -99,20 +108,23 @@ export class DayCellContent extends BaseComponent<DayCellContentProps> {
 
   render() {
     let { props, context } = this
-
-    let hookPropsOrigin: DayCellHookPropOrigin = {
+    let { options } = context
+    let hookProps = refineHookProps({
       date: props.date,
       dateProfile: props.dateProfile,
       todayRange: props.todayRange,
-      showDayNumber: props.showDayNumber
-    }
-    let hookProps = {
-      ...massageHooksProps(hookPropsOrigin, context),
-      ...props.extraHookProps
-    }
+      showDayNumber: props.showDayNumber,
+      extraProps: props.extraHookProps,
+      viewApi: context.viewApi,
+      dateEnv: context.dateEnv
+    })
 
     return (
-      <ContentHook name='dayCell' hookProps={hookProps} defaultContent={props.defaultContent}>
+      <ContentHook
+        hookProps={hookProps}
+        content={options.dayCellContent}
+        defaultContent={props.defaultContent}
+      >
         {props.children}
       </ContentHook>
     )
@@ -121,15 +133,15 @@ export class DayCellContent extends BaseComponent<DayCellContentProps> {
 }
 
 
-function massageHooksProps(input: DayCellHookPropOrigin, context: ViewContext): DayCellHookProps {
-  let { dateEnv } = context
-  let { date } = input
-  let dayMeta = getDateMeta(date, input.todayRange, null, input.dateProfile)
+function refineHookProps(raw: RawDayCellHookProps): DayCellHookProps {
+  let { date, dateEnv } = raw
+  let dayMeta = getDateMeta(date, raw.todayRange, null, raw.dateProfile)
 
   return {
     date: dateEnv.toDate(date),
-    view: context.viewApi,
+    view: raw.viewApi,
     ...dayMeta,
-    dayNumberText: input.showDayNumber ? dateEnv.format(date, DAY_NUM_FORMAT) : ''
+    dayNumberText: raw.showDayNumber ? dateEnv.format(date, DAY_NUM_FORMAT) : '',
+    ...raw.extraProps
   }
 }

+ 3 - 6
packages/common/src/common/DayHeader.tsx

@@ -1,6 +1,5 @@
 import { BaseComponent } from '../vdom-util'
 import { DateMarker } from '../datelib/marker'
-import { createFormatter } from '../datelib/formatting'
 import { computeFallbackHeaderFormat } from './table-utils'
 import { VNode, h } from '../vdom'
 import { TableDateCell, TableDowCell } from './TableDateCell'
@@ -8,6 +7,7 @@ import { NowTimer } from '../NowTimer'
 import { DateRange } from '../datelib/date-range'
 import { memoize } from '../util/memoize'
 import { DateProfile } from '../DateProfileGenerator'
+import { DateFormatter } from '../datelib/DateFormatter'
 
 
 export interface DayHeaderProps {
@@ -61,9 +61,6 @@ export class DayHeader extends BaseComponent<DayHeaderProps> { // TODO: rename t
 }
 
 
-function createDayHeaderFormatter(input, datesRepDistinctDays, dateCnt) {
-  return createFormatter(
-    input ||
-    computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt)
-  )
+function createDayHeaderFormatter(explicitFormat: DateFormatter, datesRepDistinctDays, dateCnt) {
+  return explicitFormat || computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt)
 }

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

@@ -1,10 +1,10 @@
 import { applyAll } from '../util/misc'
 
 
-export class Emitter {
+export class Emitter<Options = {}> {
 
   private handlers: any = {}
-  private options: any
+  private options: Options
   private thisContext: any = null
 
 
@@ -13,7 +13,7 @@ export class Emitter {
   }
 
 
-  setOptions(options) {
+  setOptions(options: Options) {
     this.options = options
   }
 

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

@@ -41,6 +41,7 @@ export class EventRoot extends BaseComponent<EventRootProps> {
 
   render() {
     let { props, context } = this
+    let { options } = context
     let { seg } = props
     let { eventRange } = seg
     let { ui } = eventRange
@@ -71,9 +72,12 @@ export class EventRoot extends BaseComponent<EventRootProps> {
 
     return (
       <RenderHook
-        name='event'
         hookProps={hookProps}
+        classNames={options.eventClassNames}
+        content={options.eventContent}
         defaultContent={props.defaultContent}
+        didMount={options.eventDidMount}
+        willUnmount={options.eventWillUnmount}
         elRef={this.elRef}
       >
         {(rootElRef, customClassNames, innerElRef, innerContent) => props.children(

+ 16 - 2
packages/common/src/common/NowIndicatorRoot.tsx

@@ -2,6 +2,7 @@ import { RenderHook, RenderHookPropsChildren } from './render-hook'
 import { DateMarker } from '../datelib/marker'
 import { ViewContext, ViewContextType } from '../ViewContext'
 import { h } from '../vdom'
+import { ViewApi } from '../ViewApi'
 
 
 export interface NowIndicatorRootProps {
@@ -10,18 +11,31 @@ export interface NowIndicatorRootProps {
   children: RenderHookPropsChildren
 }
 
+export interface NowIndicatorHookProps {
+  isAxis: boolean
+  date: Date
+  view: ViewApi
+}
+
 
 export const NowIndicatorRoot = (props: NowIndicatorRootProps) => (
   <ViewContextType.Consumer>
     {(context: ViewContext) => {
-      let hookProps = {
+      let { options } = context
+      let hookProps: NowIndicatorHookProps = {
         isAxis: props.isAxis,
         date: context.dateEnv.toDate(props.date),
         view: context.viewApi
       }
 
       return (
-        <RenderHook name='nowIndicator' hookProps={hookProps}>
+        <RenderHook
+          hookProps={hookProps}
+          classNames={options.nowIndicatorClassNames}
+          content={options.nowIndicatorContent}
+          didMount={options.nowIndicatorDidMount}
+          willUnmount={options.nowIndicatorWillUnmount}
+        >
           {props.children}
         </RenderHook>
       )

+ 3 - 11
packages/common/src/common/StandardEvent.tsx

@@ -1,15 +1,15 @@
 
 import { ComponentChildren, h, Fragment } from '../vdom'
 import { BaseComponent } from '../vdom-util'
-import { createFormatter } from '../datelib/formatting'
 import { buildSegTimeText, EventMeta } from '../component/event-rendering'
 import { EventRoot, MinimalEventProps } from './EventRoot'
 import { Seg } from '../component/DateComponent'
+import { DateFormatter } from '../datelib/DateFormatter'
 
 
 export interface StandardEventProps extends MinimalEventProps {
   extraClassNames: string[]
-  defaultTimeFormat: any // date-formatter INPUT
+  defaultTimeFormat: DateFormatter
   defaultDisplayEventTime?: boolean // default true
   defaultDisplayEventEnd?: boolean // default true
   disableDragging?: boolean // default false
@@ -24,15 +24,7 @@ export class StandardEvent extends BaseComponent<StandardEventProps> {
   render() {
     let { props, context } = this
 
-    // TODO: avoid createFormatter, cache!!!
-    // SOLUTION: require that props.defaultTimeFormat is a real formatter, a top-level const,
-    // which will require that defaultRangeSeparator be part of the DateEnv (possible already?),
-    // and have options.eventTimeFormat be preprocessed.
-    let timeFormat = createFormatter(
-      context.options.eventTimeFormat || props.defaultTimeFormat,
-      context.options.defaultRangeSeparator
-    )
-
+    let timeFormat = context.options.eventTimeFormat || props.defaultTimeFormat
     let timeText = buildSegTimeText(props.seg, timeFormat, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd)
 
     return (

+ 21 - 14
packages/common/src/common/TableDateCell.tsx

@@ -8,8 +8,8 @@ import { formatDayString } from '../datelib/formatting-utils'
 import { BaseComponent } from '../vdom-util'
 import { RenderHook } from './render-hook'
 import { buildNavLinkData } from './nav-link'
-import { ViewApi } from '../ViewApi'
 import { DateProfile } from '../DateProfileGenerator'
+import { DayHeaderHookProps } from '../options'
 
 
 export interface TableDateCellProps {
@@ -24,13 +24,6 @@ export interface TableDateCellProps {
   extraHookProps?: object
 }
 
-export interface DateHeaderCellHookProps extends DateMeta { // is used publicly as the standard header cell. TODO: move
-  date: Date
-  view: ViewApi
-  text: string
-  [otherProp: string]: any
-}
-
 const CLASS_NAME = 'fc-col-header-cell' // do the cushion too? no
 
 
@@ -52,7 +45,7 @@ export class TableDateCell extends BaseComponent<TableDateCellProps> { // BAD na
       ? buildNavLinkData(date)
       : null
 
-    let hookProps: DateHeaderCellHookProps = {
+    let hookProps: DayHeaderHookProps = {
       date: dateEnv.toDate(date),
       view: viewApi,
       ...props.extraHookProps,
@@ -61,7 +54,14 @@ export class TableDateCell extends BaseComponent<TableDateCellProps> { // BAD na
     }
 
     return (
-      <RenderHook name='dayHeader' hookProps={hookProps} defaultContent={renderInner}>
+      <RenderHook
+        hookProps={hookProps}
+        classNames={options.dayHeaderClassNames}
+        content={options.dayHeaderContent}
+        defaultContent={renderInner}
+        didMount={options.dayHeaderDidMount}
+        willUnmount={options.dayHeaderWillUnmount}
+      >
         {(rootElRef, customClassNames, innerElRef, innerContent) => (
           <th
             ref={rootElRef}
@@ -105,7 +105,7 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
 
   render() {
     let { props } = this
-    let { dateEnv, theme, viewApi } = this.context
+    let { dateEnv, theme, viewApi, options } = this.context
 
     let date = addDays(new Date(259200000), props.dow) // start with Sun, 04 Jan 1970 00:00:00 GMT
 
@@ -125,7 +125,7 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
 
     let text = dateEnv.format(date, props.dayHeaderFormat)
 
-    let hookProps: DateHeaderCellHookProps = {
+    let hookProps: DayHeaderHookProps = {
       date,
       ...dateMeta,
       view: viewApi,
@@ -134,7 +134,14 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
     }
 
     return (
-      <RenderHook name='dayHeader' hookProps={hookProps} defaultContent={renderInner}>
+      <RenderHook
+        hookProps={hookProps}
+        classNames={options.dayHeaderClassNames}
+        content={options.dayHeaderContent}
+        defaultContent={renderInner}
+        didMount={options.dayHeaderDidMount}
+        willUnmount={options.dayHeaderWillUnmount}
+      >
         {(rootElRef, customClassNames, innerElRef, innerContent) => (
           <th
             ref={rootElRef}
@@ -160,6 +167,6 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
 }
 
 
-function renderInner(hookProps: DateHeaderCellHookProps) {
+function renderInner(hookProps: DayHeaderHookProps) {
   return hookProps.text
 }

+ 16 - 5
packages/common/src/common/ViewRoot.tsx

@@ -1,7 +1,8 @@
 import { ViewSpec } from '../structs/view-spec'
-import { MountHook, buildHookClassNameGenerator } from './render-hook'
+import { MountHook, buildClassNameNormalizer } from './render-hook'
 import { ComponentChildren, h, Ref } from '../vdom'
 import { BaseComponent } from '../vdom-util'
+import { ViewApi } from '../ViewApi'
 
 
 export interface ViewRootProps {
@@ -10,19 +11,29 @@ export interface ViewRootProps {
   elRef?: Ref<any>
 }
 
+export interface ViewRootHookProps {
+  view: ViewApi
+}
+
 
 export class ViewRoot extends BaseComponent<ViewRootProps> {
 
-  buildClassNames = buildHookClassNameGenerator('view')
+  normalizeClassNames = buildClassNameNormalizer<ViewRootHookProps>()
 
 
   render() {
     let { props, context } = this
-    let hookProps = { view: context.viewApi }
-    let customClassNames = this.buildClassNames(hookProps, context)
+    let { options } = context
+    let hookProps: ViewRootHookProps = { view: context.viewApi }
+    let customClassNames = this.normalizeClassNames(options.viewClassNames, hookProps)
 
     return (
-      <MountHook name='view' hookProps={hookProps} elRef={props.elRef}>
+      <MountHook
+        hookProps={hookProps}
+        didMount={options.viewDidMount}
+        willUnmount={options.viewWillUnmount}
+        elRef={props.elRef}
+      >
         {(rootElRef) => props.children(
           rootElRef,
           [ `fc-${props.viewSpec.type}-view`, 'fc-view' ].concat(customClassNames)

+ 21 - 7
packages/common/src/common/WeekNumberRoot.tsx

@@ -1,28 +1,42 @@
-import { createFormatter, FormatterInput } from '../datelib/formatting'
 import { ViewContext, ViewContextType } from '../ViewContext'
 import { DateMarker } from '../datelib/marker'
 import { RenderHook, RenderHookPropsChildren } from './render-hook'
 import { h } from '../vdom'
+import { DateFormatter } from '../datelib/DateFormatter'
 
 
 export interface WeekNumberRootProps {
   date: DateMarker
-  defaultFormat: FormatterInput
+  defaultFormat: DateFormatter
   children: RenderHookPropsChildren
 }
 
+export interface WeekNumberHookProps {
+  num: number
+  text: string
+  date: Date
+}
+
 
 export const WeekNumberRoot = (props: WeekNumberRootProps) => (
   <ViewContextType.Consumer>
     {(context: ViewContext) => {
+      let { dateEnv, options } = context
       let { date } = props
-      let format = createFormatter(context.options.weekNumberFormat || props.defaultFormat) // TODO: precompute
-      let num = context.dateEnv.computeWeekNumber(date) // TODO: somehow use for formatting as well?
-      let text = context.dateEnv.format(date, format)
-      let hookProps = { num, text, date }
+      let format = options.weekNumberFormat || props.defaultFormat
+      let num = dateEnv.computeWeekNumber(date) // TODO: somehow use for formatting as well?
+      let text = dateEnv.format(date, format)
+      let hookProps: WeekNumberHookProps = { num, text, date }
 
       return (
-        <RenderHook name='weekNumber' hookProps={hookProps} defaultContent={renderInner}>
+        <RenderHook<WeekNumberHookProps> // why isn't WeekNumberHookProps being auto-detected?
+          hookProps={hookProps}
+          classNames={options.weekNumberClassNames}
+          content={options.weekNumberContent}
+          defaultContent={renderInner}
+          didMount={options.weekNumberDidMount}
+          willUnmount={options.weekNumberWillUnmount}
+        >
           {props.children}
         </RenderHook>
       )

+ 54 - 58
packages/common/src/common/render-hook.tsx

@@ -1,14 +1,15 @@
 import { Ref, createRef, ComponentChildren, h, RefObject, createContext } from '../vdom'
-import { ViewContext } from '../ViewContext'
 import { setRef, BaseComponent } from '../vdom-util'
 import { isPropsEqual } from '../util/object'
 
 
 export interface RenderHookProps<HookProps> {
-  name: string
   hookProps: HookProps
-  defaultContent?: (hookProps: HookProps) => ComponentChildren
-  options?: object // for using another root object for the options. RENAME
+  classNames: ClassNameGenerator<HookProps>
+  content: CustomContentGenerator<HookProps>
+  defaultContent?: DefaultContentGenerator<HookProps>
+  didMount: DidMountHandler<HookProps>
+  willUnmount: WillUnmountHandler<HookProps>
   children: RenderHookPropsChildren
   elRef?: Ref<any>
 }
@@ -24,27 +25,24 @@ export interface ContentTypeHandlers {
   [contentKey: string]: () => (el: HTMLElement, contentVal: any) => void
 }
 
-// TODO: use capitalizeFirstLetter util
-
 
+// NOTE: in JSX, you should always use this class with <HookProps> arg. otherwise, will default to any???
 export class RenderHook<HookProps> extends BaseComponent<RenderHookProps<HookProps>> {
 
   private rootElRef = createRef()
 
 
   render() {
-    let { name, hookProps, options, defaultContent, children } = this.props
+    let { props } = this
+    let { hookProps } = props
 
     return (
-      <MountHook name={name} hookProps={hookProps} options={options} elRef={this.handleRootEl}>
+      <MountHook hookProps={hookProps} didMount={props.didMount} willUnmount={props.willUnmount} elRef={this.handleRootEl}>
         {(rootElRef) => (
-          <ContentHook name={name} hookProps={hookProps} options={options} defaultContent={defaultContent} backupElRef={this.rootElRef}>
-            {(innerElRef, innerContent) => children(
+          <ContentHook hookProps={hookProps} content={props.content} defaultContent={props.defaultContent} backupElRef={this.rootElRef}>
+            {(innerElRef, innerContent) => props.children(
               rootElRef,
-              normalizeClassNames(
-                (options || this.context.options)[name ? name + 'ClassNames' : 'classNames'],
-                hookProps
-              ),
+              normalizeClassNames(props.classNames, hookProps),
               innerElRef,
               innerContent
             )}
@@ -66,22 +64,32 @@ export class RenderHook<HookProps> extends BaseComponent<RenderHookProps<HookPro
 }
 
 
+
+export interface ObjCustomContent {
+  html: string
+  domNodes: any[]
+  [custom: string]: any // TODO: expose hook for plugins to add!
+}
+
+export type CustomContent = ComponentChildren | ObjCustomContent
+export type CustomContentGenerator<HookProps> = CustomContent | ((hookProps: HookProps) => CustomContent)
+export type DefaultContentGenerator<HookProps> = (hookProps: HookProps) => ComponentChildren
+
 // for forcing rerender of components that use the ContentHook
 export const CustomContentRenderContext = createContext<number>(0)
 
 export interface ContentHookProps<HookProps> {
-  name: string
   hookProps: HookProps
-  options?: object // will use instead of context. RENAME
-  backupElRef?: RefObject<any>
-  defaultContent?: (hookProps: HookProps) => ComponentChildren
+  content: CustomContentGenerator<HookProps>
+  defaultContent?: DefaultContentGenerator<HookProps>
   children: (
     innerElRef: Ref<any>,
     innerContent: ComponentChildren // if falsy, means it wasn't specified
   ) => ComponentChildren
+  backupElRef?: RefObject<any>
 }
 
-export class ContentHook<HookProps> extends BaseComponent<ContentHookProps<HookProps>> {
+export class ContentHook<HookProps> extends BaseComponent<ContentHookProps<HookProps>> { // TODO: rename to CustomContentHook?
 
   private innerElRef = createRef()
   private customContentInfo: {
@@ -115,7 +123,7 @@ export class ContentHook<HookProps> extends BaseComponent<ContentHookProps<HookP
   private renderInnerContent() {
     let { contentTypeHandlers } = this.context.pluginHooks
     let { props, customContentInfo } = this
-    let rawVal = (this.props.options || this.context.options)[props.name ? props.name + 'Content' : 'content']
+    let rawVal = props.content
     let innerContent = normalizeContent(rawVal, props.hookProps)
     let innerContentVDom: ComponentChildren = null
 
@@ -166,12 +174,18 @@ export class ContentHook<HookProps> extends BaseComponent<ContentHookProps<HookP
 }
 
 
+
+
+export type HookPropsWithEl<HookProps> = HookProps & { el: HTMLElement }
+export type DidMountHandler<HookProps> = (hookProps: HookPropsWithEl<HookProps>) => void
+export type WillUnmountHandler<HookProps> = (hookProps: HookPropsWithEl<HookProps>) => void
+
 export interface MountHookProps<HookProps> {
-  name: string
-  elRef?: Ref<any> // maybe get rid of once we have better API for caller to combine refs
   hookProps: HookProps
-  options?: object // will use instead of context
+  didMount: DidMountHandler<HookProps>
+  willUnmount: WillUnmountHandler<HookProps>
   children: (rootElRef: Ref<any>) => ComponentChildren
+  elRef?: Ref<any> // maybe get rid of once we have better API for caller to combine refs
 }
 
 export class MountHook<HookProps> extends BaseComponent<MountHookProps<HookProps>> {
@@ -185,12 +199,12 @@ export class MountHook<HookProps> extends BaseComponent<MountHookProps<HookProps
 
 
   componentDidMount() {
-    this.triggerMountHandler('DidMount', 'didMount')
+    this.props.didMount({ ...this.props.hookProps, el: this.rootEl })
   }
 
 
   componentWillUnmount() {
-    this.triggerMountHandler('WillUnmount', 'willUnmount')
+    this.props.willUnmount({ ...this.props.hookProps, el: this.rootEl })
   }
 
 
@@ -202,41 +216,19 @@ export class MountHook<HookProps> extends BaseComponent<MountHookProps<HookProps
     }
   }
 
-
-  private triggerMountHandler(postfix: string, simplePostfix: string) {
-    let { name } = this.props
-    let handler = (this.props.options || this.context.options)[name ? name + postfix : simplePostfix]
-
-    if (handler) {
-      handler({ // TODO: make a better type for this
-        ...this.props.hookProps,
-        el: this.rootEl
-      })
-    }
-  }
-
 }
 
 
-export function buildHookClassNameGenerator<HookProps>(hookName: string) {
-  let currentRawGenerator
-  let currentContext: object
-  let currentCacheBuster
-  let currentClassNames: string[]
-
-  return function(hookProps: HookProps, context: ViewContext, optionsOverride?: object, cacheBusterOverride?: object) {
-    let rawGenerator = (optionsOverride || context.options)[hookName ? hookName + 'ClassNames' : 'classNames']
-    let cacheBuster = cacheBusterOverride || hookProps
-
-    if (
-      currentRawGenerator !== rawGenerator ||
-      currentContext !== context ||
-      (!currentCacheBuster || !isPropsEqual(currentCacheBuster, cacheBuster))
-    ) {
-      currentClassNames = normalizeClassNames(rawGenerator, hookProps)
-      currentRawGenerator = rawGenerator
-      currentContext = context
-      currentCacheBuster = cacheBuster
+export function buildClassNameNormalizer<HookProps>() { // TODO: general deep-memoizer?
+  let currentGenerator: ClassNameGenerator<HookProps>
+  let currentHookProps: HookProps
+  let currentClassNames: string[] = []
+
+  return function(generator: ClassNameGenerator<HookProps>, hookProps: HookProps) {
+    if (!currentHookProps || !isPropsEqual(currentHookProps, hookProps) || generator !== currentGenerator) {
+      currentGenerator = generator
+      currentHookProps = hookProps
+      currentClassNames = normalizeClassNames(generator, hookProps)
     }
 
     return currentClassNames
@@ -244,7 +236,11 @@ export function buildHookClassNameGenerator<HookProps>(hookName: string) {
 }
 
 
-function normalizeClassNames(classNames, hookProps) {
+export type RawClassNames = string | string[] // also somewhere else? a util for parsing classname string/array?
+export type ClassNameGenerator<HookProps> = RawClassNames | ((hookProps: HookProps) => RawClassNames)
+
+
+function normalizeClassNames<HookProps>(classNames: ClassNameGenerator<HookProps>, hookProps: HookProps): string[] {
 
   if (typeof classNames === 'function') {
     classNames = classNames(hookProps)

+ 4 - 3
packages/common/src/common/table-utils.ts

@@ -1,13 +1,14 @@
+import { createFormatter } from '../datelib/formatting'
 
 // Computes a default column header formatting string if `colFormat` is not explicitly defined
 export function computeFallbackHeaderFormat(datesRepDistinctDays: boolean, dayCnt: number) {
   // if more than one week row, or if there are a lot of columns with not much space,
   // put just the day numbers will be in each cell
   if (!datesRepDistinctDays || dayCnt > 10) {
-    return { weekday: 'short' } // "Sat"
+    return createFormatter({ weekday: 'short' }) // "Sat"
   } else if (dayCnt > 1) {
-    return { weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true } // "Sat 11/12"
+    return createFormatter({ weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }) // "Sat 11/12"
   } else {
-    return { weekday: 'long' } // "Saturday"
+    return createFormatter({ weekday: 'long' }) // "Saturday"
   }
 }

+ 44 - 51
packages/common/src/component/event-ui.ts

@@ -1,13 +1,15 @@
 import { Constraint, AllowFunc, normalizeConstraint, ConstraintInput } from '../structs/constraint'
-import { parseClassName } from '../util/html'
-import { refineProps, capitaliseFirstLetter } from '../util/misc'
+import { parseClassNames } from '../util/html'
+import { refineProps } from '../util/misc'
 import { CalendarContext } from '../CalendarContext'
+import { identity } from '../options'
 
 // TODO: better called "EventSettings" or "EventConfig"
 // TODO: move this file into structs
 // TODO: separate constraint/overlap/allow, because selection uses only that, not other props
 
-export interface UnscopedEventUiInput {
+export interface RawEventUi {
+  display?: string
   editable?: boolean
   startEditable?: boolean
   durationEditable?: boolean
@@ -28,7 +30,7 @@ export interface EventUi {
   durationEditable: boolean | null
   constraints: Constraint[]
   overlap: boolean | null
-  allows: AllowFunc[]
+  allows: AllowFunc[] // crappy name to indicate plural
   backgroundColor: string
   borderColor: string
   textColor: string,
@@ -37,24 +39,51 @@ export interface EventUi {
 
 export type EventUiHash = { [defId: string]: EventUi }
 
-export const UNSCOPED_EVENT_UI_PROPS = {
-  display: null, // TODO: string?
+export const UI_PROPS_REFINERS = {
+  display: identity, // TODO: string?
   editable: Boolean,
   startEditable: Boolean,
   durationEditable: Boolean,
-  constraint: null,
-  overlap: null,
-  allow: null,
-  className: parseClassName,
-  classNames: parseClassName,
+  constraint: identity,
+  overlap: identity,
+  allow: identity,
+  classNames: parseClassNames,
   color: String,
   backgroundColor: String,
   borderColor: String,
   textColor: String
 }
 
-export function processUnscopedUiProps(rawProps: UnscopedEventUiInput, context: CalendarContext, leftovers?): EventUi {
-  let props = refineProps(rawProps, UNSCOPED_EVENT_UI_PROPS, {}, leftovers)
+export const EVENT_SCOPED_RAW_UI_PROPS = {
+  eventDisplay: true,
+  editable: true,
+  eventStartEditable: true,
+  eventDurationEditable: true,
+  eventConstraint: true,
+  eventOverlap: true,
+  eventAllow: true,
+  eventBackgroundColor: true,
+  eventBorderColor: true,
+  eventTextColor: true,
+  eventClassNames: true
+}
+
+const EMPTY_EVENT_UI: EventUi = {
+  display: null,
+  startEditable: null,
+  durationEditable: null,
+  constraints: [],
+  overlap: null,
+  allows: [],
+  backgroundColor: '',
+  borderColor: '',
+  textColor: '',
+  classNames: []
+}
+
+
+export function processUiProps(rawProps: RawEventUi, context: CalendarContext, leftovers?): EventUi {
+  let props = refineProps(rawProps, UI_PROPS_REFINERS, {}, leftovers)
   let constraint = normalizeConstraint(props.constraint, context)
 
   return {
@@ -67,53 +96,17 @@ export function processUnscopedUiProps(rawProps: UnscopedEventUiInput, context:
     backgroundColor: props.backgroundColor || props.color,
     borderColor: props.borderColor || props.color,
     textColor: props.textColor,
-    classNames: props.classNames.concat(props.className)
+    classNames: props.classNames
   }
 }
 
-export function processScopedUiProps(prefix: string, rawScoped: any, context: CalendarContext, leftovers?): EventUi {
-  let rawUnscoped = {} as any
-  let wasFound = {} as any
-
-  for (let key in UNSCOPED_EVENT_UI_PROPS) {
-    let scopedKey = prefix + capitaliseFirstLetter(key)
-    rawUnscoped[key] = rawScoped[scopedKey]
-    wasFound[scopedKey] = true
-  }
-
-  if (prefix === 'event') {
-    rawUnscoped.editable = rawScoped.editable // special case. there is no 'eventEditable', just 'editable'
-  }
-
-  if (leftovers) {
-    for (let key in rawScoped) {
-      if (!wasFound[key]) {
-        leftovers[key] = rawScoped[key]
-      }
-    }
-  }
-
-  return processUnscopedUiProps(rawUnscoped, context)
-}
-
-const EMPTY_EVENT_UI: EventUi = {
-  display: null,
-  startEditable: null,
-  durationEditable: null,
-  constraints: [],
-  overlap: null,
-  allows: [],
-  backgroundColor: '',
-  borderColor: '',
-  textColor: '',
-  classNames: []
-}
 
 // prevent against problems with <2 args!
 export function combineEventUis(uis: EventUi[]): EventUi {
   return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI)
 }
 
+
 function combineTwoEventUis(item0: EventUi, item1: EventUi): EventUi { // hash1 has higher precedence
   return {
     display: item1.display != null ? item1.display : item0.display,

+ 2 - 1
packages/common/src/datelib/DateFormatter.ts

@@ -35,9 +35,10 @@ export interface DateFormattingContext {
   computeWeekNumber: (d: DateMarker) => number
   weekText: string
   cmdFormatter?: CmdFormatterFunc
+  defaultSeparator: string
 }
 
 export interface DateFormatter {
   format(date: ZonedMarker, context: DateFormattingContext): string
-  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext): string
+  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext, separatorOverride?: string): string
 }

+ 2 - 2
packages/common/src/datelib/duration.ts

@@ -81,8 +81,8 @@ function normalizeObject(obj: DurationObjectInput): Duration {
   }
 }
 
-export function getWeeksFromInput(obj: DurationObjectInput) {
-  return obj.weeks || obj.week || 0
+export function getWeeksFromInput(input: DurationInput) {
+  return typeof input === 'object' && (input.weeks || input.week || 0)
 }
 
 

+ 9 - 1
packages/common/src/datelib/env.ts

@@ -22,6 +22,7 @@ export interface DateEnvSettings {
   firstDay?: any,
   weekText?: string,
   cmdFormatter?: CmdFormatterFunc
+  defaultSeparator?: string
 }
 
 export type DateInput = Date | string | number | number[]
@@ -46,6 +47,7 @@ export class DateEnv {
   weekNumberFunc: any
   weekText: string // DON'T LIKE how options are confused with local
   cmdFormatter?: CmdFormatterFunc
+  defaultSeparator: string
 
 
   constructor(settings: DateEnvSettings) {
@@ -79,6 +81,7 @@ export class DateEnv {
     this.weekText = settings.weekText != null ? settings.weekText : settings.locale.options.weekText
 
     this.cmdFormatter = settings.cmdFormatter
+    this.defaultSeparator = settings.defaultSeparator
   }
 
 
@@ -367,7 +370,12 @@ export class DateEnv {
     )
   }
 
-  formatRange(start: DateMarker, end: DateMarker, formatter: DateFormatter, dateOptions: { forcedStartTzo?: number, forcedEndTzo?: number, isEndExclusive?: boolean } = {}) {
+  formatRange(
+    start: DateMarker,
+    end: DateMarker,
+    formatter: DateFormatter,
+    dateOptions: { forcedStartTzo?: number, forcedEndTzo?: number, isEndExclusive?: boolean } = {}
+  ) {
 
     if (dateOptions.isEndExclusive) {
       end = addMs(end, -1)

+ 2 - 2
packages/common/src/datelib/formatting-native.ts

@@ -99,7 +99,7 @@ export class NativeFormatter implements DateFormatter {
     let partial1 = partialFormattingFunc(end)
 
     let insertion = findCommonInsertion(full0, partial0, full1, partial1)
-    let separator = extendedSettings.separator || ''
+    let separator = extendedSettings.separator || context.defaultSeparator || ''
 
     if (insertion) {
       return insertion.before + partial0 + separator + partial1 + insertion.after
@@ -289,7 +289,7 @@ function formatWeekNumber(num: number, weekText: string, locale: Locale, display
 
   parts.push(locale.simpleNumberFormat.format(num))
 
-  if (locale.options.isRtl) { // TODO: use control characters instead?
+  if (locale.options.direction === 'rtl') { // TODO: use control characters instead?
     parts.reverse()
   }
 

+ 5 - 6
packages/common/src/datelib/locale.ts

@@ -1,6 +1,7 @@
 import { mergeProps } from '../util/object'
 import { getGlobalRawLocales } from '../global-locales' // weird to be importing this
 import { __assign } from 'tslib'
+import { RawCalendarOptions, RefinedCalendarOptions } from '../options'
 
 export type LocaleCodeArg = string | string[]
 export type LocaleSingularArg = LocaleCodeArg | RawLocale
@@ -10,12 +11,11 @@ export interface Locale {
   codes: string[]
   week: { dow: number, doy: number }
   simpleNumberFormat: Intl.NumberFormat
-  options: any
+  options: RefinedCalendarOptions
 }
 
-export interface RawLocale {
+export interface RawLocale extends RawCalendarOptions {
   code: string
-  [otherProp: string]: any
 }
 
 export type RawLocaleMap = { [code: string]: RawLocale }
@@ -31,7 +31,7 @@ const RAW_EN_LOCALE = {
     dow: 0, // Sunday is the first day of the week
     doy: 4 // 4 days need to be within the year to be considered the first week
   },
-  direction: 'ltr',
+  direction: 'ltr' as ('ltr' | 'rtl'), // TODO: make a real type for this
   buttonText: {
     prev: 'prev',
     next: 'next',
@@ -55,7 +55,7 @@ export function organizeRawLocales(explicitRawLocales: RawLocale[]): RawLocaleIn
   let defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en'
   let globalRawLocales = getGlobalRawLocales()
   let allRawLocales = globalRawLocales.concat(explicitRawLocales)
-  let rawLocaleMap = {
+  let rawLocaleMap: RawLocaleMap = {
     en: RAW_EN_LOCALE // necessary?
   }
 
@@ -111,7 +111,6 @@ function parseLocale(codeArg: LocaleCodeArg, codes: string[], raw: RawLocale): L
   let merged = mergeProps([ RAW_EN_LOCALE, raw ], [ 'buttonText' ])
 
   delete merged.code // don't want this part of the options
-
   let week = merged.week
   delete merged.week
 

+ 3 - 3
packages/common/src/formatting-api.ts

@@ -1,7 +1,7 @@
 import { DateEnv, DateInput } from './datelib/env'
 import { createFormatter } from './datelib/formatting'
 import { organizeRawLocales, buildLocale } from './datelib/locale'
-import { globalDefaults } from './options'
+import { RAW_BASE_DEFAULTS } from './options'
 
 export function formatDate(dateInput: DateInput, settings = {}) {
   let dateEnv = buildDateEnv(settings)
@@ -23,7 +23,7 @@ export function formatRange(
   settings // mixture of env and formatter settings
 ) {
   let dateEnv = buildDateEnv(typeof settings === 'object' && settings ? settings : {}) // pass in if non-null object
-  let formatter = createFormatter(settings, globalDefaults.defaultRangeSeparator)
+  let formatter = createFormatter(settings, RAW_BASE_DEFAULTS.defaultRangeSeparator)
   let startMeta = dateEnv.createMarkerMeta(startInput)
   let endMeta = dateEnv.createMarkerMeta(endInput)
 
@@ -44,7 +44,7 @@ function buildDateEnv(settings) {
 
   // ensure required settings
   settings = {
-    timeZone: globalDefaults.timeZone,
+    timeZone: RAW_BASE_DEFAULTS.timeZone,
     calendarSystem: 'gregory',
     ...settings,
     locale

+ 4 - 0
packages/common/src/global-config.ts

@@ -0,0 +1,4 @@
+
+// TODO: get rid of this in favor of options system,
+// tho it's really easy to access this globally rather than pass thru options.
+export const config = {} as any

+ 1 - 1
packages/common/src/global-plugins.ts

@@ -11,7 +11,7 @@ import { injectHtml, injectDomNodes } from './util/dom-manip'
 this array is exposed on the root namespace so that UMD plugins can add to it.
 see the rollup-bundles script.
 */
-export let globalPlugins: PluginDef[] = [
+export let globalPlugins: PluginDef[] = [ // TODO: make a const?
   arrayEventSourcePlugin,
   funcEventSourcePlugin,
   jsonFeedEventSourcePlugin,

+ 16 - 7
packages/common/src/main.ts

@@ -6,7 +6,6 @@ import './main.scss'
 export const version: string = '<%= version %>' // important to type it, so .d.ts has generic string
 
 // types
-export { OptionsInput } from './types/input-types'
 export { EventDef, EventDefHash } from './structs/event-def'
 export { EventInstance, EventInstanceHash, createEventInstance } from './structs/event-instance'
 export { EventInput, parseEventDef, EventTuple } from './structs/event-parse'
@@ -16,7 +15,6 @@ export {
   applyAll,
   padStart,
   isInt,
-  capitaliseFirstLetter,
   parseFieldSpecs,
   compareByFieldSpecs,
   compareByFieldSpec,
@@ -40,7 +38,7 @@ export {
   isArraysEqual
 } from './util/array'
 
-export { memoize, memoizeArraylike, memoizeHashlike } from './util/memoize'
+export { memoize, memoizeObjArg, memoizeArraylike, memoizeHashlike } from './util/memoize'
 
 export {
   intersectRects,
@@ -64,7 +62,7 @@ export {
 } from './util/dom-manip'
 
 export { EventStore, filterEventStoreDefs, createEmptyEventStore, mergeEventStores, getRelevantEvents, eventTupleToStore } from './structs/event-store'
-export { EventUiHash, EventUi, processScopedUiProps, combineEventUis } from './component/event-ui'
+export { EventUiHash, EventUi, EVENT_SCOPED_RAW_UI_PROPS, processUiProps, combineEventUis } from './component/event-ui'
 export { Splitter, SplittableProps } from './component/event-splitting'
 export { getDayClassNames, getDateMeta, DateMeta, getSlotClassNames } from './component/date-rendering'
 export { buildNavLinkData } from './common/nav-link'
@@ -115,6 +113,7 @@ export { DateEnv, DateMarkerMeta } from './datelib/env'
 
 export {
   createFormatter,
+  FormatterInput
 } from './datelib/formatting'
 export {
   DateFormatter,
@@ -140,7 +139,14 @@ export { ElementDragging } from './interactions/ElementDragging'
 
 export { formatDate, formatRange } from './formatting-api'
 
-export { globalDefaults, config } from './options'
+export {
+  RAW_BASE_DEFAULTS, identity, Identity, DayHeaderHookProps,
+  SlotLaneHookProps, SlotLabelHookProps, AllDayHookProps,
+  BaseOptionRefiners, RawBaseOptions, RefinedBaseOptions,
+  CalendarOptionRefiners, RawCalendarOptions, RefinedCalendarOptions,
+  ViewOptionRefiners, RawViewOptions, RefinedViewOptions
+} from './options'
+export { config } from './global-config'
 
 export { RecurringType, ParsedRecurring } from './structs/recurring-event'
 
@@ -154,7 +160,7 @@ export { CalendarContentProps, CalendarContent, computeCalendarClassNames, compu
 
 export { DayHeader } from './common/DayHeader'
 export { computeFallbackHeaderFormat } from './common/table-utils'
-export { TableDateCell, TableDowCell, DateHeaderCellHookProps } from './common/TableDateCell'
+export { TableDateCell, TableDowCell } from './common/TableDateCell'
 
 export { DaySeriesModel } from './common/DaySeriesModel'
 
@@ -203,7 +209,10 @@ export { getIsRtlScrollbarOnLeft } from './util/scrollbar-side'
 export { NowTimer } from './NowTimer'
 export { ScrollResponder, ScrollRequest } from './ScrollResponder'
 export { globalPlugins } from './global-plugins'
-export { RenderHook, RenderHookProps, RenderHookPropsChildren, MountHook, MountHookProps, buildHookClassNameGenerator, ContentHook, CustomContentRenderContext } from './common/render-hook'
+export {
+  RenderHook, RenderHookProps, RenderHookPropsChildren, MountHook, MountHookProps, buildClassNameNormalizer, ContentHook, CustomContentRenderContext,
+  ClassNameGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler
+} from './common/render-hook'
 export { StandardEvent, StandardEventProps } from './common/StandardEvent'
 export { NowIndicatorRoot, NowIndicatorRootProps } from './common/NowIndicatorRoot'
 

+ 432 - 41
packages/common/src/options.ts

@@ -1,18 +1,219 @@
+import { createDuration, Duration } from './datelib/duration'
 import { mergeProps } from './util/object'
+import { ToolbarInput } from './toolbar-parse'
+import { createFormatter, FormatterInput } from './datelib/formatting'
+import { parseFieldSpecs } from './util/misc'
+import { CssDimValue } from './scrollgrid/util'
+import { DateInput } from './datelib/env'
+import { DateRangeInput } from './datelib/date-range'
+import { BusinessHoursInput } from './structs/business-hours'
+import { ViewApi } from './ViewApi'
+import { LocaleSingularArg, RawLocale } from './datelib/locale'
+import { OverlapFunc, ConstraintInput, AllowFunc } from './structs/constraint'
+import { EventApi } from './api/EventApi'
+import { EventInputTransformer } from './structs/event-parse'
+import { PluginDef } from './plugin-system-struct'
+import { EventSourceInput } from './structs/event-source-parse'
+import { ViewComponentType, ViewHookProps } from './structs/view-config'
+import { EventMeta } from './component/event-rendering'
+import { ClassNameGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler } from './common/render-hook'
+import { NowIndicatorHookProps } from './common/NowIndicatorRoot'
+import { WeekNumberHookProps } from './common/WeekNumberRoot'
+import { DateMeta } from './component/date-rendering'
+import { DayCellHookProps } from './common/DayCellRoot'
+import { ViewRootHookProps } from './common/ViewRoot'
 
-export const config = {} as any // TODO: make these options
 
-export const globalDefaults = {
+// base options
+// ------------
 
+const BASE_OPTION_REFINERS = {
+  navLinkDayClick: identity as Identity<string | ((date: Date, jsEvent: Event) => void)>,
+  navLinkWeekClick: identity as Identity<string | ((weekStart: Date, jsEvent: Event) => void)>,
+  duration: createDuration,
+  bootstrapFontAwesome: identity as Identity<ButtonIconsInput | false>, // TODO: move to bootstrap plugin
+  buttonIcons: identity as Identity<ButtonIconsInput | false>,
+  customButtons: identity as Identity<{ [name: string]: CustomButtonInput }>,
+  defaultAllDayEventDuration: createDuration,
+  defaultTimedEventDuration: createDuration,
+  nextDayThreshold: createDuration,
+  scrollTime: createDuration,
+  slotMinTime: createDuration,
+  slotMaxTime: createDuration,
+  dayPopoverFormat: createFormatter,
+  eventOrderSpecs: parseFieldSpecs,
+  slotDuration: createDuration,
+  snapDuration: createDuration,
+  headerToolbar: identity as Identity<ToolbarInput | false>,
+  footerToolbar: identity as Identity<ToolbarInput | false>,
+  defaultRangeSeparator: String,
+  titleRangeSeparator: String,
+  forceEventDuration: Boolean,
+
+  dayHeaders: Boolean,
+  dayHeaderFormat: createFormatter,
+  dayHeaderClassNames: identity as Identity<ClassNameGenerator<DayHeaderHookProps>>,
+  dayHeaderContent: identity as Identity<CustomContentGenerator<DayHeaderHookProps>>,
+  dayHeaderDidMount: identity as Identity<DidMountHandler<DayHeaderHookProps>>,
+  dayHeaderWillUnmount: identity as Identity<WillUnmountHandler<DayHeaderHookProps>>,
+
+  dayCellClassNames: identity as Identity<ClassNameGenerator<DayCellHookProps>>,
+  dayCellContent: identity as Identity<CustomContentGenerator<DayCellHookProps>>,
+  dayCellDidMount: identity as Identity<DidMountHandler<DayCellHookProps>>,
+  dayCellWillUnmount: identity as Identity<WillUnmountHandler<DayCellHookProps>>,
+
+  initialView: String,
+  aspectRatio: Number,
+  weekends: Boolean,
+
+  weekNumberCalculation: identity as Identity<WeekNumberCalculation>,
+  weekNumbers: Boolean,
+  weekNumberClassNames: identity as Identity<ClassNameGenerator<WeekNumberHookProps>>,
+  weekNumberContent: identity as Identity<CustomContentGenerator<WeekNumberHookProps>>,
+  weekNumberDidMount: identity as Identity<DidMountHandler<WeekNumberHookProps>>,
+  weekNumberWillUnmount: identity as Identity<WillUnmountHandler<WeekNumberHookProps>>,
+
+  editable: Boolean,
+
+  viewClassNames: identity as Identity<ClassNameGenerator<ViewRootHookProps>>,
+  viewDidMount: identity as Identity<DidMountHandler<ViewRootHookProps>>,
+  viewWillUnmount: identity as Identity<WillUnmountHandler<ViewRootHookProps>>,
+
+  nowIndicator: Boolean,
+  nowIndicatorClassNames: identity as Identity<ClassNameGenerator<NowIndicatorHookProps>>,
+  nowIndicatorContent: identity as Identity<CustomContentGenerator<NowIndicatorHookProps>>,
+  nowIndicatorDidMount: identity as Identity<DidMountHandler<NowIndicatorHookProps>>,
+  nowIndicatorWillUnmount: identity as Identity<WillUnmountHandler<NowIndicatorHookProps>>,
+
+  showNonCurrentDates: Boolean,
+  lazyFetching: Boolean,
+  startParam: String,
+  endParam: String,
+  timeZoneParam: String,
+  timeZone: String,
+  locales: identity as Identity<RawLocale[]>,
+  locale: identity as Identity<LocaleSingularArg>,
+  themeSystem: String as Identity<'standard' | string>,
+  dragRevertDuration: Number,
+  dragScroll: Boolean,
+  allDayMaintainDuration: Boolean,
+  unselectAuto: Boolean,
+  dropAccept: identity as Identity<string | ((draggable: any) => boolean)>, // TODO: type draggable
+  eventOrder: identity as Identity<string | Array<((a: EventApi, b: EventApi) => number) | (string | ((a: EventApi, b: EventApi) => number))>>,
+
+  handleWindowResize: Boolean,
+  windowResizeDelay: Number,
+  longPressDelay: Number,
+  eventDragMinDistance: Number,
+  expandRows: Boolean,
+  height: identity as Identity<CssDimValue>,
+  contentHeight: identity as Identity<CssDimValue>,
+  direction: String as Identity<'ltr' | 'rtl'>,
+  weekNumberFormat: createFormatter,
+  eventResizableFromStart: Boolean,
+  displayEventTime: Boolean,
+  displayEventEnd: Boolean,
+  weekText: String,
+  progressiveEventRendering: Boolean,
+  businessHours: identity as Identity<BusinessHoursInput>, // ???
+  initialDate: identity as Identity<DateInput>,
+  now: identity as Identity<DateInput | (() => DateInput)>,
+  eventDataTransform: identity as Identity<EventInputTransformer>,
+  stickyHeaderDates: identity as Identity<boolean | 'auto'>,
+  stickyFooterScrollbar: identity as Identity<boolean | 'auto'>,
+  viewHeight: identity as Identity<CssDimValue>,
+  defaultAllDay: Boolean,
+  eventSourceFailure: identity as Identity<any>, // TODO: should be Listeners?
+  eventSourceSuccess: identity as Identity<any>, //
+
+  eventDisplay: String, // TODO: give more specific
+  eventStartEditable: Boolean,
+  eventDurationEditable: Boolean,
+  eventOverlap: identity as Identity<boolean | OverlapFunc>,
+  eventConstraint: identity as Identity<ConstraintInput>,
+  eventAllow: identity as Identity<AllowFunc>,
+  eventBackgroundColor: String,
+  eventBorderColor: String,
+  eventTextColor: String,
+  eventColor: String,
+  eventClassNames: identity as Identity<ClassNameGenerator<EventMeta>>,
+  eventContent: identity as Identity<CustomContentGenerator<EventMeta>>,
+  eventDidMount: identity as Identity<DidMountHandler<EventMeta>>,
+  eventWillUnmount: identity as Identity<WillUnmountHandler<EventMeta>>,
+
+  selectConstraint: identity as Identity<ConstraintInput>,
+  selectOverlap: identity as Identity<boolean | OverlapFunc>,
+  selectAllow: identity as Identity<AllowFunc>,
+
+  droppable: Boolean,
+  unselectCancel: String,
+
+  slotLabelFormat: createFormatter,
+
+  slotLaneClassNames: identity as Identity<ClassNameGenerator<SlotLaneHookProps>>,
+  slotLaneContent: identity as Identity<CustomContentGenerator<SlotLaneHookProps>>,
+  slotLaneDidMount: identity as Identity<DidMountHandler<SlotLaneHookProps>>,
+  slotLaneWillUnmount: identity as Identity<WillUnmountHandler<SlotLaneHookProps>>,
+
+  slotLabelClassNames: identity as Identity<ClassNameGenerator<SlotLabelHookProps>>,
+  slotLabelContent: identity as Identity<CustomContentGenerator<SlotLabelHookProps>>,
+  slotLabelDidMount: identity as Identity<DidMountHandler<SlotLabelHookProps>>,
+  slotLabelWillUnmount: identity as Identity<WillUnmountHandler<SlotLabelHookProps>>,
+
+  dayMaxEvents: identity as Identity<boolean | number>,
+  dayMaxEventRows: identity as Identity<boolean | number>,
+  dayMinWidth: Number,
+  slotLabelInterval: createDuration,
+
+  allDayText: String,
+  allDayClassNames: identity as Identity<ClassNameGenerator<AllDayHookProps>>,
+  allDayContent: identity as Identity<CustomContentGenerator<AllDayHookProps>>,
+  allDayDidMount: identity as Identity<DidMountHandler<AllDayHookProps>>,
+  allDayWillUnmount: identity as Identity<WillUnmountHandler<AllDayHookProps>>,
+
+  slotMinWidth: Number, // move to timeline?
+  navLinks: Boolean,
+  eventTimeFormat: createFormatter,
+  rerenderDelay: Number, // TODO: move to @fullcalendar/core right? nah keep here
+  moreLinkText: identity as Identity<string | ((num: number) => string)>,
+  selectMinDistance: Number,
+  selectable: Boolean,
+  selectLongPressDelay: Number,
+  eventLongPressDelay: Number,
+
+  selectMirror: Boolean,
+  eventMinHeight: Number, // TODO: kill this setting
+  slotEventOverlap: Boolean,
+  plugins: identity as Identity<PluginDef[]>,
+  firstDay: Number,
+  dayCount: Number,
+  dateAlignment: String,
+  dateIncrement: createDuration,
+  hiddenDays: identity as Identity<number[]>,
+  monthMode: Boolean,
+  fixedWeekCount: Boolean,
+  validRange: identity as Identity<DateRangeInput | ((nowDate: Date) => DateRangeInput)>,
+  visibleRange: identity as Identity<DateRangeInput | ((currentDate: Date) => DateRangeInput)>,
+  titleFormat: identity as Identity<FormatterInput>, // DONT parse just yet. we need to inject titleSeparator
+}
+
+type BuiltInBaseOptionRefiners = typeof BASE_OPTION_REFINERS
+
+export interface BaseOptionRefiners extends BuiltInBaseOptionRefiners {
+  // for ambient-extending
+}
+
+export type RawBaseOptions = RawOptionsFromRefiners< // as RawOptions
+  Required<BaseOptionRefiners> // Required is a hack for "Index signature is missing"
+>
+
+export const RAW_BASE_DEFAULTS = { // do NOT give a type here. need `typeof RAW_BASE_DEFAULTS` to give real results
   defaultRangeSeparator: ' - ',
   titleRangeSeparator: ' \u2013 ', // en dash
-
   defaultTimedEventDuration: '01:00:00',
   defaultAllDayEventDuration: { day: 1 },
   forceEventDuration: false,
   nextDayThreshold: '00:00:00',
-
-  // display
   dayHeaders: true,
   initialView: '',
   aspectRatio: 1.35,
@@ -23,68 +224,127 @@ export const globalDefaults = {
   },
   weekends: true,
   weekNumbers: false,
-  weekNumberCalculation: 'local',
-
+  weekNumberCalculation: 'local' as WeekNumberCalculation,
   editable: false,
-
-  // nowIndicator: false,
-
+  nowIndicator: false,
   scrollTime: '06:00:00',
   slotMinTime: '00:00:00',
   slotMaxTime: '24:00:00',
   showNonCurrentDates: true,
-
-  // event ajax
   lazyFetching: true,
   startParam: 'start',
   endParam: 'end',
   timeZoneParam: 'timeZone',
-
   timeZone: 'local', // TODO: throw error if given falsy value?
-
-  // defaultAllDay: undefined,
-
-  // locale
   locales: [],
   locale: '', // blank values means it will compute based off locales[]
-  // direction: will get this from the default locale
-  // buttonIcons: null,
-
   themeSystem: 'standard',
-
-  // eventResizableFromStart: false,
   dragRevertDuration: 500,
   dragScroll: true,
-
   allDayMaintainDuration: false,
-
-  // selectable: false,
   unselectAuto: true,
-  // selectMinDistance: 0,
-
   dropAccept: '*',
-
   eventOrder: 'start,-duration,allDay,title',
-  // ^ if start tie, longer events go before shorter. final tie-breaker is title text
-
-  // rerenderDelay: null,
-
-  moreLinkClick: 'popover',
   dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' },
-
   handleWindowResize: true,
   windowResizeDelay: 100, // milliseconds before an updateSize happens
-
   longPressDelay: 1000,
   eventDragMinDistance: 5, // only applies to mouse
+  expandRows: false,
+  navLinks: false,
+  selectable: false,
+  firstDay: 0
+}
 
-  expandRows: false
+export type RefinedBaseOptions = DefaultedRefinedOptions<
+  RefinedOptionsFromRefiners<Required<BaseOptionRefiners>>, // Required is a hack for "Index signature is missing"
+  keyof typeof RAW_BASE_DEFAULTS
+>
 
-  // dayMinWidth: null
+
+// calendar-specific options
+// -------------------------
+
+export const CALENDAR_OPTION_REFINERS = { // does not include base
+  buttonText: identity as Identity<ButtonTextCompoundInput>,
+  views: identity as Identity<{ [viewId: string]: RawViewOptions }>,
+  plugins: identity as Identity<PluginDef[]>,
+  events: identity as Identity<EventSourceInput>,
+  eventSources: identity as Identity<EventSourceInput[]>,
+
+  windowResize: identity as Identity<
+    (view: ViewApi) => void
+  >,
+
+  _destroy: identity as Identity<() => void>,
+  _init: identity as Identity<() => void>,
+  _noEventDrop: identity as Identity<() => void>,
+  _noEventResize: identity as Identity<() => void>,
+  _resize: identity as Identity<(forced: boolean) => void>,
+  _scrollRequest: identity as Identity<(arg: any) => void>,
+
+  // TODO: move a lot of these to interaction plugin?
+  dateClick: identity as Identity< // resource for Scheduler
+    (arg: { date: Date, dateStr: string, allDay: boolean, resource?: any, dayEl: HTMLElement, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  eventClick: identity as Identity<
+    (arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }) => boolean | void
+  >,
+  eventMouseEnter: identity as Identity<
+    (arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  eventMouseLeave: identity as Identity<
+    (arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  select: identity as Identity< // resource for Scheduler
+    (arg: { start: Date, end: Date, startStr: string, endStr: string, allDay: boolean, resource?: any, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  unselect: identity as Identity<
+    (arg: { view: ViewApi, jsEvent: Event }) => void
+  >,
+  loading: identity as Identity<
+    (isLoading: boolean) => void
+  >,
+  eventDragStart: identity as Identity<
+    (arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  eventDragStop: identity as Identity<
+    (arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  eventDrop: identity as Identity<
+    (arg: { el: HTMLElement, event: EventApi, oldEvent: EventApi, delta: Duration, revert: () => void, jsEvent: Event, view: ViewApi }) => void
+  >,
+  eventResizeStart: identity as Identity<
+    (arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  eventResizeStop: identity as Identity<
+    (arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  eventResize: identity as Identity<
+    (arg: { el: HTMLElement, startDelta: Duration, endDelta: Duration, prevEvent: EventApi, event: EventApi, revert: () => void, jsEvent: Event, view: ViewApi }) => void
+  >,
+  drop: identity as Identity<
+    (arg: { date: Date, dateStr: string, allDay: boolean, draggedEl: HTMLElement, jsEvent: MouseEvent, view: ViewApi }) => void
+  >,
+  eventReceive: identity as Identity<
+    (arg: { event: EventApi, draggedEl: HTMLElement, view: ViewApi }) => void
+  >,
+  eventLeave: identity as Identity<
+    (arg: { draggedEl: HTMLElement, event: EventApi, view: ViewApi }) => void
+  >
 }
 
+type BuiltInCalendarOptionRefiners = typeof CALENDAR_OPTION_REFINERS
 
-let complexOptions = [ // names of options that are objects whose properties should be combined
+export interface CalendarOptionRefiners extends BuiltInCalendarOptionRefiners {
+  // for ambient-extending
+}
+
+export type RawCalendarOptions = RawBaseOptions & RawOptionsFromRefiners<Required<CalendarOptionRefiners>> // aaaaa https://github.com/microsoft/TypeScript/issues/15300
+export type RefinedCalendarOptions = RefinedBaseOptions & RefinedOptionsFromRefiners<Required<CalendarOptionRefiners>> // aaaaaa
+export type CalendarListeners = FilteredPropValues<RefinedCalendarOptions, (...args: any[]) => void>
+
+const COMPLEX_CALENDAR_OPTIONS: (keyof RawCalendarOptions)[] = [
   'headerToolbar',
   'footerToolbar',
   'buttonText',
@@ -92,7 +352,138 @@ let complexOptions = [ // names of options that are objects whose properties sho
 ]
 
 
-// Merges an array of option objects into a single object
-export function mergeOptions(optionObjs) {
-  return mergeProps(optionObjs, complexOptions)
+
+// view-specific options
+// ---------------------
+
+export const VIEW_OPTION_REFINERS = {
+  type: String,
+  component: identity as Identity<ViewComponentType>,
+  buttonText: String,
+  buttonTextKey: String, // internal only
+  dateProfileGeneratorClass: identity as Identity<any>, // internal only
+  usesMinMaxTime: Boolean, // internal only
+  classNames: identity as Identity<ClassNameGenerator<ViewHookProps>>,
+  content: identity as Identity<CustomContentGenerator<ViewHookProps>>,
+  didMount: identity as Identity<DidMountHandler<ViewHookProps>>,
+  willUnmount: identity as Identity<WillUnmountHandler<ViewHookProps>>
+}
+
+type BuiltInViewOptionRefiners = typeof VIEW_OPTION_REFINERS
+
+export interface ViewOptionRefiners extends BuiltInViewOptionRefiners {
+  // for ambient-extending
+}
+
+export type RawViewOptions = RawBaseOptions & RawOptionsFromRefiners<typeof VIEW_OPTION_REFINERS>
+export type RefinedViewOptions = RefinedBaseOptions & RefinedOptionsFromRefiners<typeof VIEW_OPTION_REFINERS>
+
+
+
+// util funcs
+// ----------------------------------------------------------------------------------------------------
+
+
+export function mergeRawOptions(optionSets: GenericObject[]) {
+  return mergeProps(optionSets, COMPLEX_CALENDAR_OPTIONS)
+}
+
+
+
+// definition utils
+// ----------------------------------------------------------------------------------------------------
+
+
+export type GenericRefiners = {
+  [propName: string]: (input: any) => any
+}
+
+type RawOptionsFromRefiners<Refiners extends GenericRefiners> = {
+  [Prop in keyof Refiners]?: // all optional
+    Refiners[Prop] extends ((input: infer RawType) => infer RefinedType)
+      ? (any extends RawType ? RefinedType : RawType) // if input type `any`, use output (for Boolean/Number/String)
+      : never
+}
+
+type RefinedOptionsFromRefiners<Refiners extends GenericRefiners> = {
+  [Prop in keyof Refiners]?: // all optional
+    Refiners[Prop] extends ((input: any) => infer RefinedType) ? RefinedType : never
+}
+
+type DefaultedRefinedOptions<RefinedOptions extends GenericObject, DefaultKey extends keyof RefinedOptions> =
+  Required<Pick<RefinedOptions, DefaultKey>> &
+  Partial<Omit<RefinedOptions, DefaultKey>>
+
+
+
+type GenericObject = { [prop: string]: any } // TODO: Partial<{}>
+
+// https://stackoverflow.com/a/49397693/96342
+type FilteredPropKeys<T, Match> = ({ [P in keyof T]: T[P] extends Match ? P : never })[keyof T]
+type FilteredPropValues<T, Match> = Pick<T, FilteredPropKeys<T, Match>>
+
+export type Identity<T = any> = (raw: T) => T
+
+export function identity<T>(raw: T): T {
+  return raw
+}
+
+
+
+// random crap we need to put into other files
+// -------------------------------------------
+
+export interface SlotLaneHookProps extends Partial<DateMeta> { // TODO: move?
+  time?: Duration
+  date?: Date
+  view: ViewApi
+  // this interface is for date-specific slots AND time-general slots. make an OR?
+}
+
+export interface SlotLabelHookProps { // TODO: move?
+  time: Duration
+  date: Date
+  view: ViewApi
+  text: string
+}
+
+export interface AllDayHookProps {
+  text: string
+  view: ViewApi
+}
+
+export interface CustomButtonInput {
+  text: string
+  icon?: string
+  themeIcon?: string
+  bootstrapFontAwesome?: string,
+  click(element: HTMLElement): void
+}
+
+export interface ButtonIconsInput {
+  prev?: string
+  next?: string
+  prevYear?: string
+  nextYear?: string
+}
+
+export interface ButtonTextCompoundInput {
+  prev?: string
+  next?: string
+  prevYear?: string // derive these somehow?
+  nextYear?: string
+  today?: string
+  month?: string
+  week?: string
+  day?: string
+  [viewId: string]: string | undefined // needed b/c of other optional types ... make extendable???
+}
+
+export type WeekNumberCalculation = 'local' | 'ISO' | ((m: Date) => number)
+
+export interface DayHeaderHookProps extends DateMeta {
+  date: Date
+  view: ViewApi
+  text: string
+  [otherProp: string]: any
 }

+ 3 - 0
packages/common/src/plugin-system-struct.ts

@@ -22,6 +22,7 @@ import { ElementDraggingClass } from './interactions/ElementDragging'
 import { ComponentChildren } from './vdom'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
 import { ContentTypeHandlers } from './common/render-hook'
+import { GenericRefiners } from './options'
 
 // TODO: easier way to add new hooks? need to update a million things
 
@@ -54,6 +55,7 @@ export interface PluginDefInput {
   optionChangeHandlers?: OptionChangeHandlerMap
   scrollGridImpl?: ScrollGridImpl
   contentTypeHandlers?: ContentTypeHandlers
+  optionRefiners?: GenericRefiners
 }
 
 export interface PluginHooks {
@@ -84,6 +86,7 @@ export interface PluginHooks {
   optionChangeHandlers: OptionChangeHandlerMap
   scrollGridImpl: ScrollGridImpl | null
   contentTypeHandlers: ContentTypeHandlers
+  optionRefiners: GenericRefiners
 }
 
 export interface PluginDef extends PluginHooks {

+ 6 - 3
packages/common/src/plugin-system.ts

@@ -35,7 +35,8 @@ export function createPlugin(input: PluginDefInput): PluginDef {
     elementDraggingImpl: input.elementDraggingImpl,
     optionChangeHandlers: input.optionChangeHandlers || {},
     scrollGridImpl: input.scrollGridImpl || null,
-    contentTypeHandlers: input.contentTypeHandlers || {}
+    contentTypeHandlers: input.contentTypeHandlers || {},
+    optionRefiners: input.optionRefiners || {}
   }
 }
 
@@ -69,7 +70,8 @@ export function buildPluginHooks(pluginDefs: PluginDef[] | null, globalDefs: Plu
     elementDraggingImpl: null,
     optionChangeHandlers: {},
     scrollGridImpl: null,
-    contentTypeHandlers: {}
+    contentTypeHandlers: {},
+    optionRefiners: {}
   }
 
   function addDefs(defs: PluginDef[]) {
@@ -120,6 +122,7 @@ function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
     elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
     optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers },
     scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl,
-    contentTypeHandlers: { ...hooks0.contentTypeHandlers, ...hooks1.contentTypeHandlers }
+    contentTypeHandlers: { ...hooks0.contentTypeHandlers, ...hooks1.contentTypeHandlers },
+    optionRefiners: { ...hooks0.optionRefiners, ...hooks1.optionRefiners },
   }
 }

+ 1 - 1
packages/common/src/reducers/Action.ts

@@ -9,7 +9,7 @@ import { DateSpan } from '../structs/date-span'
 import { DateMarker } from '../datelib/marker'
 
 export type Action =
-  { type: 'SET_OPTION', optionName: string, optionValue: any } |
+  { type: 'SET_OPTION', optionName: string, rawOptionValue: any } | // TODO: how to link this to RawCalendarOptions?
 
   { type: 'PREV' } |
   { type: 'NEXT' } |

+ 200 - 87
packages/common/src/reducers/CalendarDataManager.ts

@@ -1,4 +1,4 @@
-import { buildLocale, RawLocaleInfo, organizeRawLocales } from '../datelib/locale'
+import { buildLocale, RawLocaleInfo, organizeRawLocales, LocaleSingularArg } from '../datelib/locale'
 import { memoize, memoizeObjArg } from '../util/memoize'
 import { Action } from './Action'
 import { buildPluginHooks } from '../plugin-system'
@@ -7,7 +7,7 @@ import { DateEnv } from '../datelib/env'
 import { CalendarApi } from '../CalendarApi'
 import { StandardTheme } from '../theme/StandardTheme'
 import { EventSourceHash } from '../structs/event-source'
-import { buildViewSpecs } from '../structs/view-spec'
+import { buildViewSpecs, ViewSpec } from '../structs/view-spec'
 import { mapHash, isPropsEqual } from '../util/object'
 import { DateProfileGenerator, DateProfileGeneratorProps } from '../DateProfileGenerator'
 import { reduceViewType } from './view-type'
@@ -21,18 +21,16 @@ import { reduceSelectedEvent } from './selected-event'
 import { reduceEventDrag } from './event-drag'
 import { reduceEventResize } from './event-resize'
 import { Emitter } from '../common/Emitter'
-import { processScopedUiProps, EventUiHash, EventUi } from '../component/event-ui'
+import { EventUiHash, EventUi, processUiProps } from '../component/event-ui'
 import { EventDefHash } from '../structs/event-def'
 import { parseToolbars } from '../toolbar-parse'
-import { firstDefined } from '../util/misc'
-import { globalDefaults, mergeOptions } from '../options'
+import { RefinedCalendarOptions, RefinedBaseOptions, RawCalendarOptions, CALENDAR_OPTION_REFINERS, RawViewOptions, RefinedViewOptions, RAW_BASE_DEFAULTS, mergeRawOptions } from '../options'
 import { rangeContainsMarker } from '../datelib/date-range'
 import { ViewApi } from '../ViewApi'
 import { parseBusinessHours } from '../structs/business-hours'
 import { globalPlugins } from '../global-plugins'
 import { createEmptyEventStore } from '../structs/event-store'
 import { CalendarContext } from '../CalendarContext'
-import { buildComputedOptions } from '../ComputedOptions'
 import { CalendarDataManagerState, CalendarOptionsData, CalendarCurrentViewData, CalendarData } from './data-types'
 import { __assign } from 'tslib'
 import { TaskRunner } from '../util/runner'
@@ -40,7 +38,7 @@ import { buildTitle } from './title-formatting'
 
 
 export interface CalendarDataManagerProps {
-  optionOverrides: any
+  optionOverrides: RawCalendarOptions
   calendarApi: CalendarApi
   onAction?: (action: Action) => void
   onData?: (data: CalendarData) => void
@@ -62,16 +60,12 @@ export class CalendarDataManager {
   private computeOptionsData = memoize(this._computeOptionsData)
   private computeCurrentViewData = memoize(this._computeCurrentViewData)
   private organizeRawLocales = memoize(organizeRawLocales)
-  private buildCalendarOptions = memoize(mergeOptionSets)
-  private buildComputedCalendarOptions = memoize(buildComputedOptions)
   private buildLocale = memoize(buildLocale)
   private buildPluginHooks = memoize(buildPluginHooks)
   private buildDateEnv = memoize(buildDateEnv)
   private buildTheme = memoize(buildTheme)
   private parseToolbars = memoize(parseToolbars)
   private buildViewSpecs = memoize(buildViewSpecs)
-  private buildViewOptions = memoize(mergeOptionSets)
-  private buildComputedViewOptions = memoize(buildComputedOptions)
   private buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator)
   private buildViewApi = memoize(buildViewApi)
   private buildViewUiProps = memoizeObjArg(buildViewUiProps)
@@ -80,18 +74,23 @@ export class CalendarDataManager {
   private parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours)
   private buildTitle = memoize(buildTitle)
 
-  public emitter = new Emitter()
+  public emitter = new Emitter<RefinedCalendarOptions>()
   private actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this))
   private props: CalendarDataManagerProps
   private state: CalendarDataManagerState
   private data: CalendarData
 
+  public currentRawCalendarOptions: RawCalendarOptions = {}
+  private currentRefinedCalendarOptions: RefinedCalendarOptions = ({} as any)
+  private currentRawViewOptions: RawViewOptions = {}
+  private currentRefinedViewOptions: RefinedViewOptions = ({} as any)
+
 
   constructor(props: CalendarDataManagerProps) {
     this.props = props
     this.actionRunner.pause()
 
-    let dynamicOptionOverrides = {}
+    let dynamicOptionOverrides: RawCalendarOptions = {}
     let optionsData = this.computeOptionsData(
       props.optionOverrides,
       dynamicOptionOverrides,
@@ -100,8 +99,8 @@ export class CalendarDataManager {
 
     let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView
     let currentViewData = this.computeCurrentViewData(
-      optionsData,
       currentViewType,
+      optionsData,
       props.optionOverrides,
       dynamicOptionOverrides
     )
@@ -122,7 +121,6 @@ export class CalendarDataManager {
     let calendarContext: CalendarContext = {
       dateEnv: optionsData.dateEnv,
       options: optionsData.calendarOptions,
-      computedOptions: optionsData.computedCalendarOptions,
       pluginHooks: optionsData.pluginHooks,
       calendarApi: props.calendarApi,
       dispatch: this.dispatch,
@@ -176,7 +174,7 @@ export class CalendarDataManager {
   }
 
 
-  resetOptions(optionOverrides, append?: boolean) {
+  resetOptions(optionOverrides: RawCalendarOptions, append?: boolean) {
     let { props } = this
 
     props.optionOverrides = append
@@ -201,8 +199,8 @@ export class CalendarDataManager {
 
     let currentViewType = reduceViewType(state.currentViewType, action)
     let currentViewData = this.computeCurrentViewData(
-      optionsData,
       currentViewType,
+      optionsData,
       props.optionOverrides,
       dynamicOptionOverrides
     )
@@ -216,7 +214,6 @@ export class CalendarDataManager {
     let calendarContext: CalendarContext = {
       dateEnv: optionsData.dateEnv,
       options: optionsData.calendarOptions,
-      computedOptions: optionsData.computedCalendarOptions,
       pluginHooks: optionsData.pluginHooks,
       calendarApi: props.calendarApi,
       dispatch: this.dispatch,
@@ -302,8 +299,8 @@ export class CalendarDataManager {
     )
 
     let currentViewData = this.computeCurrentViewData(
-      optionsData,
       state.currentViewType,
+      optionsData,
       props.optionOverrides,
       state.dynamicOptionOverrides
     )
@@ -343,117 +340,203 @@ export class CalendarDataManager {
   }
 
 
-  _computeOptionsData(optionOverrides, dynamicOptionOverrides, calendarApi: CalendarApi): CalendarOptionsData {
+  _computeOptionsData(optionOverrides: Partial<RefinedCalendarOptions>, dynamicOptionOverrides: Partial<RefinedCalendarOptions>, calendarApi: CalendarApi): CalendarOptionsData {
     // TODO: blacklist options that are handled by optionChangeHandlers
 
-    let locales = firstDefined( // explicit locale option given?
-      dynamicOptionOverrides.locales,
-      optionOverrides.locales,
-      globalDefaults.locales
-    )
-    let locale = firstDefined( // explicit locales option given?
-      dynamicOptionOverrides.locale,
-      optionOverrides.locale,
-      globalDefaults.locale
-    )
+    let {
+      refinedOptions, pluginHooks, localeDefaults, availableLocaleData, refiners, extra
+    } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides)
 
-    let availableLocaleData = this.organizeRawLocales(locales)
-    let availableRawLocales = availableLocaleData.map
-    let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options
-    let calendarOptions = this.buildCalendarOptions( // NOTE: use viewOptions mostly instead
-      globalDefaults,
-      localeDefaults,
-      optionOverrides,
-      dynamicOptionOverrides
-    )
+    warnUnknownOptions(extra)
 
-    let pluginHooks = this.buildPluginHooks(calendarOptions.plugins, globalPlugins)
     let dateEnv = this.buildDateEnv(
-      calendarOptions.timeZone,
-      calendarOptions.locale,
-      calendarOptions.weekNumberCalculation,
-      calendarOptions.firstDay,
-      calendarOptions.weekText,
+      refinedOptions.timeZone,
+      refinedOptions.locale,
+      refinedOptions.weekNumberCalculation,
+      refinedOptions.firstDay,
+      refinedOptions.weekText,
       pluginHooks,
-      availableLocaleData
+      availableLocaleData,
+      refinedOptions.defaultRangeSeparator
     )
 
     let viewSpecs = this.buildViewSpecs(pluginHooks.views, optionOverrides, dynamicOptionOverrides, localeDefaults)
-    let theme = this.buildTheme(calendarOptions, pluginHooks)
-    let toolbarConfig = this.parseToolbars(calendarOptions, optionOverrides, theme, viewSpecs, calendarApi)
-    let computedCalendarOptions = this.buildComputedCalendarOptions(calendarOptions)
+    let theme = this.buildTheme(refinedOptions, pluginHooks)
+    let toolbarConfig = this.parseToolbars(refinedOptions, optionOverrides, theme, viewSpecs, calendarApi)
 
     return {
-      calendarOptions,
-      computedCalendarOptions,
-      availableRawLocales,
+      calendarOptions: refinedOptions,
       pluginHooks,
       dateEnv,
       viewSpecs,
       theme,
       toolbarConfig,
-      localeDefaults
+      localeDefaults,
+      refiners,
+      availableRawLocales: availableLocaleData.map
     }
   }
 
 
-  _computeCurrentViewData(optionsData: CalendarOptionsData, currentViewType: string, optionOverrides, dynamicOptionOverrides): CalendarCurrentViewData {
-    let viewSpec = optionsData.viewSpecs[currentViewType]
+  // always called from behind a memoizer
+  processRawCalendarOptions(optionOverrides: RawCalendarOptions, dynamicOptionOverrides: RawCalendarOptions) {
+    let { locales, locale } = mergeRawOptions([
+      RAW_BASE_DEFAULTS,
+      optionOverrides,
+      dynamicOptionOverrides
+    ])
+    let availableLocaleData = this.organizeRawLocales(locales)
+    let availableRawLocales = availableLocaleData.map
+    let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options
+    let pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins)
+    let refiners = { ...CALENDAR_OPTION_REFINERS, ...pluginHooks.optionRefiners }
+    let extra = {}
+
+    let raw = mergeRawOptions([
+      RAW_BASE_DEFAULTS,
+      localeDefaults,
+      optionOverrides,
+      dynamicOptionOverrides
+    ])
+    let refined: Partial<RefinedCalendarOptions> = {}
+    let currentRaw = this.currentRawCalendarOptions
+    let currentRefined = this.currentRefinedCalendarOptions
+    let anyChanges = false
+
+    for (let optionName in raw) {
+
+      if (raw[optionName] === currentRaw[optionName]) {
+        refined[optionName] = currentRefined[optionName]
+
+      } else if (refiners[optionName]) {
+        refined[optionName] = refiners[optionName](raw[optionName])
+        anyChanges = true
+
+      } else {
+        extra[optionName] = currentRaw[optionName]
+      }
+    }
+
+    if (anyChanges) {
+      this.currentRawCalendarOptions = raw
+      this.currentRefinedCalendarOptions = refined as RefinedCalendarOptions
+    }
+
+    return {
+      rawOptions: this.currentRawCalendarOptions,
+      refinedOptions: this.currentRefinedCalendarOptions,
+      refiners,
+      pluginHooks,
+      availableLocaleData,
+      localeDefaults,
+      extra
+    }
+  }
+
+
+  _computeCurrentViewData(viewType: string, optionsData: CalendarOptionsData, optionOverrides: Partial<RefinedBaseOptions>, dynamicOptionOverrides: Partial<RefinedBaseOptions>): CalendarCurrentViewData {
+    let viewSpec = optionsData.viewSpecs[viewType]
 
     if (!viewSpec) {
-      throw new Error(`viewType "${currentViewType}" is not available. Please make sure you've loaded all neccessary plugins`)
+      throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`)
     }
 
-    let options = this.buildViewOptions( // merge defaults and overrides. lowest to highest precedence
-      globalDefaults,
-      viewSpec.optionDefaults,
+    let { refinedOptions, extra } = this.processRawViewOptions(
+      viewSpec,
+      optionsData.refiners,
       optionsData.localeDefaults,
       optionOverrides,
-      viewSpec.optionOverrides,
       dynamicOptionOverrides
     )
 
-    let computedOptions = this.buildComputedViewOptions(options)
+    warnUnknownOptions(extra)
 
     let dateProfileGenerator = this.buildDateProfileGenerator({
       viewSpec,
       dateEnv: optionsData.dateEnv,
-      slotMinTime: options.slotMinTime,
-      slotMaxTime: options.slotMaxTime,
-      showNonCurrentDates: options.showNonCurrentDates,
-      dayCount: options.dayCount,
-      dateAlignment: options.dateAlignment,
-      dateIncrement: options.dateIncrement,
-      hiddenDays: options.hiddenDays,
-      weekends: options.weekends,
-      now: options.now,
-      validRange: options.validRange,
-      visibleRange: options.visibleRange,
-      monthMode: options.monthMode,
-      fixedWeekCount: options.fixedWeekCount
+      slotMinTime: refinedOptions.slotMinTime,
+      slotMaxTime: refinedOptions.slotMaxTime,
+      showNonCurrentDates: refinedOptions.showNonCurrentDates,
+      dayCount: refinedOptions.dayCount,
+      dateAlignment: refinedOptions.dateAlignment,
+      dateIncrement: refinedOptions.dateIncrement,
+      hiddenDays: refinedOptions.hiddenDays,
+      weekends: refinedOptions.weekends,
+      nowInput: refinedOptions.now,
+      validRangeInput: refinedOptions.validRange,
+      visibleRangeInput: refinedOptions.visibleRange,
+      monthMode: refinedOptions.monthMode,
+      fixedWeekCount: refinedOptions.fixedWeekCount
     })
 
-    let viewApi = this.buildViewApi(currentViewType, this.getCurrentData, optionsData.dateEnv)
+    let viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv)
 
-    return { viewSpec, options, computedOptions, dateProfileGenerator, viewApi }
+    return { viewSpec, options: refinedOptions, dateProfileGenerator, viewApi }
   }
 
-}
 
+  processRawViewOptions(viewSpec: ViewSpec, refiners, localeDefaults: RawCalendarOptions, optionOverrides: RawCalendarOptions, dynamicOptionOverrides: RawCalendarOptions) {
+    let raw = mergeRawOptions([
+      RAW_BASE_DEFAULTS,
+      viewSpec.optionDefaults,
+      localeDefaults,
+      optionOverrides,
+      viewSpec.optionOverrides,
+      dynamicOptionOverrides
+    ])
+    let refined: Partial<RefinedViewOptions> = {}
+    let currentRaw = this.currentRawViewOptions
+    let currentRefined = this.currentRefinedViewOptions
+    let anyChanges = false
+    let extra = {}
+
+    for (let optionName in raw) {
+
+      if (raw[optionName] === currentRaw[optionName]) {
+        refined[optionName] = currentRefined[optionName]
+
+      } else {
+        if (raw[optionName] === this.currentRawCalendarOptions[optionName]) {
+
+          if (optionName in this.currentRefinedCalendarOptions) {  // might be an "extra" prop
+            refined[optionName] = this.currentRefinedCalendarOptions[optionName]
+          }
+
+        } else if (refiners[optionName]) {
+          refined[optionName] = refiners[optionName](raw[optionName])
+
+        } else {
+          extra[optionName] = raw[optionName]
+        }
+
+        anyChanges = true
+      }
+    }
+
+    if (anyChanges) {
+      this.currentRawViewOptions = raw
+      this.currentRefinedViewOptions = refined as RefinedViewOptions
+    }
+
+    return {
+      rawOptions: this.currentRawViewOptions,
+      refinedOptions: this.currentRefinedViewOptions,
+      extra
+    }
+  }
 
-function mergeOptionSets(...optionSets: any[]) {
-  return mergeOptions(optionSets)
 }
 
 
 function buildDateEnv(
   timeZone: string,
-  explicitLocale: string,
+  explicitLocale: LocaleSingularArg,
   weekNumberCalculation,
   firstDay,
   weekText,
   pluginHooks: PluginHooks,
-  availableLocaleData: RawLocaleInfo
+  availableLocaleData: RawLocaleInfo,
+  defaultSeparator: string
 ) {
   let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map)
 
@@ -465,15 +548,16 @@ function buildDateEnv(
     weekNumberCalculation,
     firstDay,
     weekText,
-    cmdFormatter: pluginHooks.cmdFormatter
+    cmdFormatter: pluginHooks.cmdFormatter,
+    defaultSeparator
   })
 }
 
 
-function buildTheme(optionOverrides, pluginHooks: PluginHooks) {
-  let ThemeClass = pluginHooks.themeClasses[optionOverrides.themeSystem] || StandardTheme
+function buildTheme(options: RefinedCalendarOptions, pluginHooks: PluginHooks) {
+  let ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme
 
-  return new ThemeClass(optionOverrides)
+  return new ThemeClass(options)
 }
 
 
@@ -512,9 +596,28 @@ function buildEventUiBases(eventDefs: EventDefHash, eventUiSingleBase: EventUi,
 
 
 function buildViewUiProps(calendarContext: CalendarContext) {
+  let { options } = calendarContext
+
   return {
-    eventUiSingleBase: processScopedUiProps('event', calendarContext.options, calendarContext),
-    selectionConfig: processScopedUiProps('select', calendarContext.options, calendarContext)
+    eventUiSingleBase: processUiProps({
+      display: options.eventDisplay,
+      editable: options.editable, // without "event" at start
+      startEditable: options.eventStartEditable,
+      durationEditable: options.eventDurationEditable,
+      constraint: options.eventConstraint,
+      // overlap: options.eventOverlap, // validation system uses this directly, b/c might be a func
+      allow: options.eventAllow,
+      backgroundColor: options.eventBackgroundColor,
+      borderColor: options.eventBorderColor,
+      textColor: options.eventTextColor,
+      // classNames: options.eventClassNames // render hook will handle this
+    }, calendarContext),
+
+    selectionConfig: processUiProps({
+      constraint: options.selectConstraint,
+      // overlap: options.selectOverlap, // validation system uses this directly, b/c might be a func
+      allow: options.selectAllow
+    }, calendarContext)
   }
 }
 
@@ -522,3 +625,13 @@ function buildViewUiProps(calendarContext: CalendarContext) {
 function parseContextBusinessHours(calendarContext: CalendarContext) {
   return parseBusinessHours(calendarContext.options.businessHours, calendarContext)
 }
+
+
+function warnUnknownOptions(options: any, viewName?: string) {
+  for (let optionName in options) {
+    console.warn(
+      `Unknown option '${optionName}'` +
+      (viewName ? ` (in '${viewName}' view)` : '')
+    )
+  }
+}

+ 9 - 9
packages/common/src/reducers/current-date.ts

@@ -1,6 +1,7 @@
-import { DateEnv } from '../datelib/env'
+import { DateEnv, DateInput } from '../datelib/env'
 import { DateMarker } from '../datelib/marker'
 import { Action } from './Action'
+import { RefinedBaseOptions } from '../options'
 
 
 export function reduceCurrentDate(currentDate: DateMarker, action: Action) {
@@ -13,28 +14,27 @@ export function reduceCurrentDate(currentDate: DateMarker, action: Action) {
 }
 
 
-export function getInitialDate(options, dateEnv: DateEnv) {
+export function getInitialDate(options: RefinedBaseOptions, dateEnv: DateEnv) {
   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
+    return getNow(options.now, dateEnv) // getNow already returns unzoned
   }
 }
 
 
-export function getNow(options, dateEnv: DateEnv) {
-  let now = options.now
+export function getNow(nowInput: DateInput | (() => DateInput), dateEnv: DateEnv) {
 
-  if (typeof now === 'function') {
-    now = now()
+  if (typeof nowInput === 'function') {
+    nowInput = nowInput()
   }
 
-  if (now == null) {
+  if (nowInput == null) {
     return dateEnv.createNowMarker()
   }
 
-  return dateEnv.createMarker(now)
+  return dateEnv.createMarker(nowInput)
 }

+ 6 - 7
packages/common/src/reducers/data-types.ts

@@ -13,11 +13,11 @@ import { Theme } from '../theme/Theme'
 import { EventStore } from '../structs/event-store'
 import { DateSpan } from '../structs/date-span'
 import { EventInteractionState } from '../interactions/event-interaction-state'
-import { ComputedOptions } from '../ComputedOptions'
+import { RefinedCalendarOptions, RefinedViewOptions, GenericRefiners, RawCalendarOptions } from '../options'
 
 
 export interface CalendarDataManagerState {
-  dynamicOptionOverrides: object // raw
+  dynamicOptionOverrides: RawCalendarOptions
   currentViewType: string
   currentDate: DateMarker
   dateProfile: DateProfile
@@ -35,11 +35,11 @@ export interface CalendarDataManagerState {
 }
 
 export interface CalendarOptionsData {
-  localeDefaults: any
-  calendarOptions: any
-  computedCalendarOptions: ComputedOptions
+  localeDefaults: RawCalendarOptions
+  calendarOptions: RefinedCalendarOptions
   toolbarConfig: any
   availableRawLocales: any
+  refiners: GenericRefiners
   dateEnv: DateEnv
   theme: Theme
   pluginHooks: PluginHooks
@@ -48,8 +48,7 @@ export interface CalendarOptionsData {
 
 export interface CalendarCurrentViewData {
   viewSpec: ViewSpec
-  options: any // is VIEW-SPECIFIC
-  computedOptions: ComputedOptions // is VIEW-SPECIFIC
+  options: RefinedViewOptions
   viewApi: ViewApi
   dateProfileGenerator: DateProfileGenerator
 }

+ 1 - 1
packages/common/src/reducers/options.ts

@@ -3,7 +3,7 @@ import { Action } from './Action'
 export function reduceDynamicOptionOverrides(dynamicOptionOverrides, action: Action) {
   switch (action.type) {
     case 'SET_OPTION':
-      return { ...dynamicOptionOverrides, [action.optionName]: action.optionValue }
+      return { ...dynamicOptionOverrides, [action.optionName]: action.rawOptionValue }
     default:
       return dynamicOptionOverrides
   }

+ 4 - 3
packages/common/src/reducers/title-formatting.ts

@@ -1,12 +1,13 @@
 import { DateProfile } from '../DateProfileGenerator'
 import { diffWholeDays } from '../datelib/marker'
-import { createFormatter } from '../datelib/formatting'
+import { createFormatter, FormatterInput } from '../datelib/formatting'
 import { DateRange } from '../datelib/date-range'
 import { DateEnv } from '../datelib/env'
+import { RefinedCalendarOptions } from '../options'
 
 
 // Computes what the title at the top of the calendarApi should be for this view
-export function buildTitle(dateProfile: DateProfile, viewOptions, dateEnv: DateEnv) {
+export function buildTitle(dateProfile: DateProfile, viewOptions: RefinedCalendarOptions, dateEnv: DateEnv) {
   let range: DateRange
 
   // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
@@ -30,7 +31,7 @@ export function buildTitle(dateProfile: DateProfile, viewOptions, dateEnv: DateE
 
 // 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 buildTitleFormat(dateProfile: DateProfile) {
+function buildTitleFormat(dateProfile: DateProfile): FormatterInput {
   let currentRangeUnit = dateProfile.currentRangeUnit
 
   if (currentRangeUnit === 'year') {

+ 3 - 2
packages/common/src/scrollgrid/util.tsx

@@ -4,6 +4,7 @@ import { ViewContext } from '../ViewContext'
 import { computeSmallestCellWidth } from '../util/misc'
 import { isPropsEqual } from '../util/object'
 import { isArraysEqual } from '../util/array'
+import { RefinedCalendarOptions } from '../options'
 
 
 export type CssDimValue = string | number // TODO: move to more general file
@@ -200,7 +201,7 @@ export function renderScrollShim(arg: ChunkContentCallbackArgs) {
 }
 
 
-export function getStickyHeaderDates(options) {
+export function getStickyHeaderDates(options: RefinedCalendarOptions) {
   let { stickyHeaderDates } = options
 
   if (stickyHeaderDates == null || stickyHeaderDates === 'auto') {
@@ -211,7 +212,7 @@ export function getStickyHeaderDates(options) {
 }
 
 
-export function getStickyFooterScrollbar(options) {
+export function getStickyFooterScrollbar(options: RefinedCalendarOptions) {
   let { stickyFooterScrollbar } = options
 
   if (stickyFooterScrollbar == null || stickyFooterScrollbar === 'auto') {

+ 5 - 5
packages/common/src/structs/event-parse.ts

@@ -2,7 +2,7 @@ import { refineProps, guid } from '../util/misc'
 import { DateInput } from '../datelib/env'
 import { startOfDay } from '../datelib/marker'
 import { parseRecurring } from './recurring-event'
-import { UnscopedEventUiInput, processUnscopedUiProps } from '../component/event-ui'
+import { RawEventUi, processUiProps } from '../component/event-ui'
 import { __assign } from 'tslib'
 import { CalendarContext } from '../CalendarContext'
 import { EventDef, DATE_PROPS, NON_DATE_PROPS } from './event-def'
@@ -14,7 +14,7 @@ Utils for parsing event-input data. Each util parses a subset of the event-input
 It's up to the caller to stitch them together into an aggregate object like an EventStore.
 */
 
-export interface EventNonDateInput extends UnscopedEventUiInput {
+export interface EventNonDateInput extends RawEventUi {
   id?: string | number
   groupId?: string | number
   title?: string
@@ -164,8 +164,8 @@ function parseSingle(raw: EventInput, defaultAllDay: boolean | null, context: Ca
     endMarker = context.dateEnv.add(
       startMarker,
       allDay ?
-        context.computedOptions.defaultAllDayEventDuration :
-        context.computedOptions.defaultTimedEventDuration
+        context.options.defaultAllDayEventDuration :
+        context.options.defaultTimedEventDuration
     )
   }
 
@@ -192,7 +192,7 @@ function pluckDateProps(raw: EventInput, leftovers: any) {
 function pluckNonDateProps(raw: EventInput, context: CalendarContext, leftovers?) {
   let preLeftovers = {}
   let props = refineProps(raw, NON_DATE_PROPS, {}, preLeftovers)
-  let ui = processUnscopedUiProps(preLeftovers, context, leftovers)
+  let ui = processUiProps(preLeftovers, context, leftovers)
 
   props.publicId = props.id
   delete props.id

+ 2 - 2
packages/common/src/structs/event-source-parse.ts

@@ -4,7 +4,7 @@ import { ConstraintInput, AllowFunc } from './constraint'
 import { EventSource, EventSourceSuccessResponseHandler, EventSourceErrorResponseHandler } from './event-source'
 import { CalendarContext } from '../CalendarContext'
 import { refineProps, guid } from '../util/misc'
-import { processUnscopedUiProps } from '../component/event-ui'
+import { processUiProps } from '../component/event-ui'
 
 
 export interface ExtendedEventSourceInput {
@@ -87,7 +87,7 @@ function parseEventSourceProps(raw: ExtendedEventSourceInput, meta: object, sour
   let leftovers0 = {}
   let props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0)
   let leftovers1 = {}
-  let ui = processUnscopedUiProps(leftovers0, context, leftovers1)
+  let ui = processUiProps(leftovers0, context, leftovers1)
 
   props.isFetching = false
   props.latestFetchId = ''

+ 3 - 3
packages/common/src/structs/recurring-event.ts

@@ -66,7 +66,7 @@ export function parseRecurring(
 
 
 export function expandRecurring(eventStore: EventStore, framingRange: DateRange, context: CalendarContext): EventStore {
-  let { dateEnv, pluginHooks, computedOptions } = context
+  let { dateEnv, pluginHooks, options } = context
   let { defs, instances } = eventStore
 
   // remove existing recurring instances
@@ -83,8 +83,8 @@ export function expandRecurring(eventStore: EventStore, framingRange: DateRange,
 
       if (!duration) {
         duration = def.allDay ?
-          computedOptions.defaultAllDayEventDuration :
-          computedOptions.defaultTimedEventDuration
+          options.defaultAllDayEventDuration :
+          options.defaultTimedEventDuration
       }
 
       let starts = expandRecurringRanges(def, duration, framingRange, dateEnv, pluginHooks.recurringTypes)

+ 31 - 28
packages/common/src/structs/view-config.tsx

@@ -1,10 +1,11 @@
 import { ViewProps } from '../View'
-import { refineProps } from '../util/misc'
 import { mapHash } from '../util/object'
 import { ComponentType, Component, h } from '../vdom'
 import { ViewRoot } from '../common/ViewRoot'
 import { RenderHook } from '../common/render-hook'
 import { ViewContext, ViewContextType } from '../ViewContext'
+import { RawViewOptions } from '../options'
+import { Duration } from '../datelib/duration'
 
 /*
 A view-config represents information for either:
@@ -15,19 +16,13 @@ B) options to customize an existing view, in which case only provides options.
 export type ViewComponent = Component<ViewProps> // an instance
 export type ViewComponentType = ComponentType<ViewProps>
 
-export interface ViewConfigObjectInput { // not strict enough. will basically allow for anything :(
-  type?: string
-  component?: ViewComponentType
-  [optionName: string]: any
-}
-
-export type ViewConfigInput = ViewComponentType | ViewConfigObjectInput
+export type ViewConfigInput = ViewComponentType | RawViewOptions
 export type ViewConfigInputHash = { [viewType: string]: ViewConfigInput }
 
 export interface ViewConfig {
   superType: string
   component: ViewComponentType | null
-  options: any
+  rawOptions: RawViewOptions
 }
 
 export type ViewConfigHash = { [viewType: string]: ViewConfig }
@@ -38,43 +33,51 @@ export function parseViewConfigs(inputs: ViewConfigInputHash): ViewConfigHash {
 }
 
 
-const VIEW_DEF_PROPS = {
-  type: String,
-  component: null
-}
-
 function parseViewConfig(input: ViewConfigInput): ViewConfig {
-  if (typeof input === 'function') {
-    input = { component: input }
-  }
+  let rawOptions: RawViewOptions = typeof input === 'function' ?
+    { component: input } :
+    input
+  let component = rawOptions.component
 
-  let options = {} as any
-  let props = refineProps(input, VIEW_DEF_PROPS, {}, options)
-  let component = props.component
-
-  if (options.content) {
-    component = createViewHookComponent(options)
+  if (rawOptions.content) {
+    component = createViewHookComponent(rawOptions)
     // TODO: remove content/classNames/didMount/etc from options?
   }
 
   return {
-    superType: props.type,
+    superType: rawOptions.type,
     component,
-    options
+    rawOptions // includes type and component too :(
   }
 }
 
 
-function createViewHookComponent(options) {
+export interface ViewHookProps extends ViewProps {
+  nextDayThreshold: Duration
+}
+
+
+function createViewHookComponent(options: RawViewOptions) {
   return function(viewProps: ViewProps) {
     return (
       <ViewContextType.Consumer>
         {(context: ViewContext) => (
           <ViewRoot viewSpec={context.viewSpec}>
             {(rootElRef, viewClassNames) => {
-              let hookProps = { ...viewProps, nextDayThreshold: context.computedOptions.nextDayThreshold }
+              let hookProps: ViewHookProps = {
+                ...viewProps,
+                nextDayThreshold: context.options.nextDayThreshold
+              }
+
               return (
-                <RenderHook name='' options={options} hookProps={hookProps} elRef={rootElRef}>
+                <RenderHook
+                  hookProps={hookProps}
+                  classNames={options.classNames}
+                  content={options.content}
+                  didMount={options.didMount}
+                  willUnmount={options.willUnmount}
+                  elRef={rootElRef}
+                >
                   {(rootElRef, customClassNames, innerElRef, innerContent) => (
                     <div className={viewClassNames.concat(customClassNames).join(' ')} ref={rootElRef}>
                       {innerContent}

+ 5 - 4
packages/common/src/structs/view-def.ts

@@ -1,4 +1,5 @@
 import { ViewConfigHash, ViewComponentType } from './view-config'
+import { RawViewOptions } from '../options'
 
 /*
 Represents information for an instantiatable View class along with settings
@@ -7,8 +8,8 @@ that are specific to that view. No other settings, like calendar-wide settings,
 export interface ViewDef {
   type: string
   component: ViewComponentType
-  overrides: any
-  defaults: any
+  overrides: RawViewOptions
+  defaults: RawViewOptions
 }
 
 export type ViewDefHash = { [viewType: string]: ViewDef }
@@ -77,11 +78,11 @@ function buildViewDef(viewType: string, hash: ViewDefHash, defaultConfigs: ViewC
     component: theComponent,
     defaults: {
       ...(superDef ? superDef.defaults : {}),
-      ...(defaultConfig ? defaultConfig.options : {})
+      ...(defaultConfig ? defaultConfig.rawOptions : {})
     },
     overrides: {
       ...(superDef ? superDef.overrides : {}),
-      ...(overrideConfig ? overrideConfig.options : {})
+      ...(overrideConfig ? overrideConfig.rawOptions : {})
     }
   }
 }

+ 8 - 8
packages/common/src/structs/view-spec.ts

@@ -1,7 +1,7 @@
 import { ViewDef, compileViewDefs } from './view-def'
 import { Duration, createDuration, greatestDurationDenominator, getWeeksFromInput } from '../datelib/duration'
 import { mapHash } from '../util/object'
-import { globalDefaults } from '../options'
+import { RawViewOptions, RawCalendarOptions, RAW_BASE_DEFAULTS } from '../options'
 import { ViewConfigInputHash, parseViewConfigs, ViewConfigHash, ViewComponentType } from './view-config'
 
 /*
@@ -18,8 +18,8 @@ export interface ViewSpec {
   duration: Duration
   durationUnit: string
   singleUnit: string
-  optionDefaults: any
-  optionOverrides: any
+  optionDefaults: RawViewOptions
+  optionOverrides: RawViewOptions
   buttonTextOverride: string
   buttonTextDefault: string
 }
@@ -27,7 +27,7 @@ export interface ViewSpec {
 export type ViewSpecHash = { [viewType: string]: ViewSpec }
 
 
-export function buildViewSpecs(defaultInputs: ViewConfigInputHash, optionOverrides, dynamicOptionOverrides, localeDefaults): ViewSpecHash {
+export function buildViewSpecs(defaultInputs: ViewConfigInputHash, optionOverrides: RawCalendarOptions, dynamicOptionOverrides: RawCalendarOptions, localeDefaults): ViewSpecHash {
   let defaultConfigs = parseViewConfigs(defaultInputs)
   let overrideConfigs = parseViewConfigs(optionOverrides.views)
   let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs)
@@ -38,7 +38,7 @@ export function buildViewSpecs(defaultInputs: ViewConfigInputHash, optionOverrid
 }
 
 
-function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, optionOverrides, dynamicOptionOverrides, localeDefaults): ViewSpec {
+function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, optionOverrides: RawCalendarOptions, dynamicOptionOverrides: RawCalendarOptions, localeDefaults): ViewSpec {
   let durationInput =
     viewDef.overrides.duration ||
     viewDef.defaults.duration ||
@@ -48,7 +48,7 @@ function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, option
   let duration = null
   let durationUnit = ''
   let singleUnit = ''
-  let singleUnitOverrides = {}
+  let singleUnitOverrides: RawViewOptions = {}
 
   if (durationInput) {
     duration = createDuration(durationInput)
@@ -63,7 +63,7 @@ function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, option
 
       if (denom.value === 1) {
         singleUnit = durationUnit
-        singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].options : {}
+        singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {}
       }
     }
   }
@@ -102,7 +102,7 @@ function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, option
     buttonTextDefault:
       queryButtonText(localeDefaults) ||
       viewDef.defaults.buttonText ||
-      queryButtonText(globalDefaults) ||
+      queryButtonText(RAW_BASE_DEFAULTS) ||
       viewDef.type // fall back to given view name
   }
 }

+ 1 - 1
packages/common/src/theme/StandardTheme.ts

@@ -25,6 +25,6 @@ StandardTheme.prototype.rtlIconClasses = {
   nextYear: 'fc-icon-chevrons-left'
 }
 
-StandardTheme.prototype.iconOverrideOption = 'buttonIcons'
+StandardTheme.prototype.iconOverrideOption = 'buttonIcons' // TODO: make TS-friendly
 StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'
 StandardTheme.prototype.iconOverridePrefix = 'fc-icon-'

+ 2 - 1
packages/common/src/theme/Theme.ts

@@ -1,3 +1,4 @@
+import { RefinedCalendarOptions } from '../options'
 
 export class Theme {
 
@@ -11,7 +12,7 @@ export class Theme {
   iconOverridePrefix: string
 
 
-  constructor(calendarOptions) {
+  constructor(calendarOptions: RefinedCalendarOptions) {
     if (this.iconOverrideOption) {
       this.setIconOverride(
         calendarOptions[this.iconOverrideOption]

+ 20 - 9
packages/common/src/toolbar-parse.ts

@@ -2,6 +2,7 @@ import { ViewSpec, ViewSpecHash } from './structs/view-spec'
 import { Theme } from './theme/Theme'
 import { mapHash } from './util/object'
 import { CalendarApi } from './CalendarApi'
+import { RefinedCalendarOptions } from './options'
 
 export interface ToolbarModel {
   [sectionName: string]: ToolbarWidget[][]
@@ -14,11 +15,18 @@ export interface ToolbarWidget {
   buttonText?: any
 }
 
+export interface ToolbarInput {
+  left?: string
+  center?: string
+  right?: string
+  start?: string
+  end?: string
+}
+
 
-// TODO: make separate parsing of headerToolbar/footerToolbar part of options-processing system
 export function parseToolbars(
-  calendarOptions: any,
-  calendarOptionOverrides: any,
+  calendarOptions: RefinedCalendarOptions,
+  calendarOptionOverrides: Partial<RefinedCalendarOptions>,
   theme: Theme,
   viewSpecs: ViewSpecHash,
   calendarApi: CalendarApi
@@ -32,15 +40,18 @@ export function parseToolbars(
 
 
 function parseToolbar(
-  sectionStrHash: { [sectionName: string]: string },
-  calendarOptions: any,
-  calendarOptionOverrides: any,
+  sectionStrHash: ToolbarInput,
+  calendarOptions: RefinedCalendarOptions,
+  calendarOptionOverrides: Partial<RefinedCalendarOptions>,
   theme: Theme,
   viewSpecs: ViewSpecHash,
   calendarApi: CalendarApi,
   viewsWithButtons: string[] // dump side effects
 ) : ToolbarModel {
-  return mapHash(sectionStrHash, (sectionStr) => parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons))
+  return mapHash(
+    sectionStrHash as { [section: string]: string },
+    (sectionStr) => parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons)
+  )
 }
 
 
@@ -49,8 +60,8 @@ BAD: querying icons and text here. should be done at render time
 */
 function parseSection(
   sectionStr: string,
-  calendarOptions: any,
-  calendarOptionOverrides: any,
+  calendarOptions: RefinedCalendarOptions,
+  calendarOptionOverrides: Partial<RefinedCalendarOptions>,
   theme: Theme,
   viewSpecs: ViewSpecHash,
   calendarApi: CalendarApi,

+ 0 - 217
packages/common/src/types/input-types.ts

@@ -1,217 +0,0 @@
-/*
-Huge thanks to these people:
-https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/fullcalendar/index.d.ts
-*/
-
-import { ViewApi } from '../ViewApi'
-import { EventSourceInput } from '../structs/event-source-parse'
-import { EventInputTransformer } from '../structs/event-parse'
-import { Duration, DurationInput } from '../datelib/duration'
-import { DateInput } from '../datelib/env'
-import { FormatterInput } from '../datelib/formatting'
-import { DateRangeInput } from '../datelib/date-range'
-import { BusinessHoursInput } from '../structs/business-hours'
-import { EventApi } from '../api/EventApi'
-import { AllowFunc, ConstraintInput, OverlapFunc } from '../structs/constraint'
-import { PluginDef } from '../plugin-system-struct'
-import { LocaleSingularArg, RawLocale } from '../datelib/locale'
-
-
-export interface ToolbarInput {
-  left?: string
-  center?: string
-  right?: string
-}
-
-export interface CustomButtonInput {
-  text: string
-  icon?: string
-  themeIcon?: string
-  bootstrapFontAwesome?: string,
-  click(element: HTMLElement): void
-}
-
-export interface ButtonIconsInput {
-  prev?: string
-  next?: string
-  prevYear?: string
-  nextYear?: string
-}
-
-export interface ButtonTextCompoundInput {
-  prev?: string
-  next?: string
-  prevYear?: string
-  nextYear?: string
-  today?: string
-  month?: string
-  week?: string
-  day?: string
-  [viewId: string]: string | undefined // needed b/c of other optional types
-}
-
-export interface EventSegment {
-  event: EventApi
-  start: Date
-  end: Date
-  isStart: boolean
-  isEnd: boolean
-}
-
-export interface CellInfo {
-  date: Date
-  allSegs: EventSegment[]
-  hiddenSegs: EventSegment[]
-}
-
-export interface DropInfo {
-  start: Date
-  end: Date
-}
-
-export interface OptionsInputBase {
-  headerToolbar?: boolean | ToolbarInput
-  footerToolbar?: boolean | ToolbarInput
-  customButtons?: { [name: string]: CustomButtonInput }
-  buttonIcons?: boolean | ButtonIconsInput
-  themeSystem?: 'standard' | string
-  bootstrapFontAwesome?: boolean | ButtonIconsInput
-  firstDay?: number
-  direction?: 'ltr' | 'rtl' | 'auto'
-  weekends?: boolean
-  hiddenDays?: number[]
-  fixedWeekCount?: boolean
-  weekNumbers?: boolean
-  weekNumberCalculation?: 'local' | 'ISO' | ((m: Date) => number)
-  businessHours?: BusinessHoursInput
-  showNonCurrentDates?: boolean
-  height?: number | 'auto' | 'parent' | (() => number)
-  contentHeight?: number | 'auto' | (() => number)
-  aspectRatio?: number
-  handleWindowResize?: boolean
-  windowResizeDelay?: number
-  dayMaxEvents?: boolean | number
-  dayMaxEventRows?: boolean | number
-  moreLinkClick?: 'popover' | 'week' | 'day' | 'timeGridWeek' | 'timeGridDay' | string |
-    ((arg: { date: Date, allDay: boolean, allSegs: any[], hiddenSegs: any[], jsEvent: MouseEvent, view: ViewApi }) => void),
-  timeZone?: string | boolean
-  now?: DateInput | (() => DateInput)
-  initialView?: string
-  allDaySlot?: boolean
-  allDayText?: string
-  slotDuration?: DurationInput
-  slotLabelFormat?: FormatterInput
-  slotLabelInterval?: DurationInput
-  snapDuration?: DurationInput
-  scrollTime?: DurationInput
-  slotMinTime?: DurationInput
-  slotMaxTime?: DurationInput
-  slotEventOverlap?: boolean
-  listDayFormat?: FormatterInput | boolean
-  listDaySideFormat?: FormatterInput | boolean
-  initialDate?: DateInput
-  nowIndicator?: boolean
-  visibleRange?: ((currentDate: Date) => DateRangeInput) | DateRangeInput
-  validRange?: DateRangeInput
-  dateIncrement?: DurationInput
-  dateAlignment?: string
-  duration?: DurationInput
-  dayCount?: number
-  locales?: RawLocale[]
-  locale?: LocaleSingularArg
-  eventTimeFormat?: FormatterInput
-  dayHeaders?: boolean
-  dayHeaderFormat?: FormatterInput
-  titleFormat?: FormatterInput
-  weekText?: string
-  displayEventTime?: boolean
-  displayEventEnd?: boolean
-  moreLinkText?: string | ((eventCnt: number) => string)
-  dayPopoverFormat?: FormatterInput
-  navLinks?: boolean
-  navLinkDayClick?: string | ((date: Date, jsEvent: Event) => void)
-  navLinkWeekClick?: string | ((weekStart: any, jsEvent: Event) => void)
-  selectable?: boolean
-  selectMirror?: boolean
-  unselectAuto?: boolean
-  unselectCancel?: string
-  defaultAllDayEventDuration?: DurationInput
-  defaultTimedEventDuration?: DurationInput
-  cmdFormatter?: string
-  defaultRangeSeparator?: string
-  selectConstraint?: ConstraintInput
-  selectOverlap?: boolean | OverlapFunc
-  selectAllow?: AllowFunc
-  editable?: boolean
-  eventStartEditable?: boolean
-  eventDurationEditable?: boolean
-  eventConstraint?: ConstraintInput
-  eventOverlap?: boolean | OverlapFunc // allows a function, unlike EventUi
-  eventAllow?: AllowFunc
-  eventClassName?: string[] | string
-  eventClassNames?: string[] | string
-  eventBackgroundColor?: string
-  eventBorderColor?: string
-  eventTextColor?: string
-  eventColor?: string
-  events?: EventSourceInput
-  eventSources?: EventSourceInput[]
-  defaultAllDay?: boolean
-  startParam?: string
-  endParam?: string
-  lazyFetching?: boolean
-  nextDayThreshold?: DurationInput
-  eventOrder?: string | Array<((a: EventApi, b: EventApi) => number) | (string | ((a: EventApi, b: EventApi) => number))>
-  rerenderDelay?: number | null
-  dragRevertDuration?: number
-  dragScroll?: boolean
-  longPressDelay?: number
-  eventLongPressDelay?: number
-  droppable?: boolean
-  dropAccept?: string | ((draggable: any) => boolean)
-  eventDataTransform?: EventInputTransformer
-  allDayMaintainDuration?: boolean
-  eventResizableFromStart?: boolean
-  eventDragMinDistance?: number
-  eventSourceFailure?: any
-  eventSourceSuccess?: any
-  forceEventDuration?: boolean
-  progressiveEventRendering?: boolean
-  selectLongPressDelay?: number
-  selectMinDistance?: number
-  timeZoneParam?: string
-  titleRangeSeparator?: string
-  windowResize?(view: ViewApi): void
-  dateClick?(arg: { date: Date, dateStr: string, allDay: boolean, resource?: any, dayEl: HTMLElement, jsEvent: MouseEvent, view: ViewApi }): void // resource for Scheduler
-  eventClick?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }): boolean | void
-  eventMouseEnter?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }): void
-  eventMouseLeave?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }): void
-  select?(arg: { start: Date, end: Date, startStr: string, endStr: string, allDay: boolean, resource?: any, jsEvent: MouseEvent, view: ViewApi }): void // resource for Scheduler
-  unselect?(arg: { view: ViewApi, jsEvent: Event }): void
-  loading?(isLoading: boolean): void
-  eventDragStart?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: ViewApi }): void
-  eventDragStop?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: ViewApi }): void
-  eventDrop?(arg: { el: HTMLElement, event: EventApi, oldEvent: EventApi, delta: Duration, revert: () => void, jsEvent: Event, view: ViewApi }): void
-  eventResizeStart?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }): void
-  eventResizeStop?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: ViewApi }): void
-  eventResize?(arg: { el: HTMLElement, startDelta: Duration, endDelta: Duration, prevEvent: EventApi, event: EventApi, revert: () => void, jsEvent: Event, view: ViewApi }): void
-  drop?(arg: { date: Date, dateStr: string, allDay: boolean, draggedEl: HTMLElement, jsEvent: MouseEvent, view: ViewApi }): void
-  eventReceive?(arg: { event: EventApi, draggedEl: HTMLElement, view: ViewApi }): void
-  eventLeave?(arg: { draggedEl: HTMLElement, event: EventApi, view: ViewApi }): void
-  _destroy?(): void
-  _init?(): void
-  _noEventDrop?(): void
-  _noEventResize?(): void
-  [otherOptions: string]: any // TEMPORARY
-}
-
-export interface ViewOptionsInput extends OptionsInputBase {
-  type?: string
-  buttonText?: string
-}
-
-export interface OptionsInput extends OptionsInputBase {
-  buttonText?: ButtonTextCompoundInput
-  views?: { [viewId: string]: ViewOptionsInput }
-  plugins?: (PluginDef | string)[]
-}

+ 1 - 1
packages/common/src/util/html.ts

@@ -1,7 +1,7 @@
 
 export type ClassNameInput = string | string[]
 
-export function parseClassName(raw: ClassNameInput) {
+export function parseClassNames(raw: ClassNameInput) {
   if (Array.isArray(raw)) {
     return raw
   } else if (typeof raw === 'string') {

+ 2 - 6
packages/common/src/util/misc.ts

@@ -141,11 +141,6 @@ export function flexibleCompare(a, b) {
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
-export function capitaliseFirstLetter(str) {
-  return str.charAt(0).toUpperCase() + str.slice(1)
-}
-
-
 export function padStart(val, len) { // doesn't work with total length more than 3
   let s = String(val)
   return '000'.substr(0, len - s.length) + s
@@ -198,10 +193,11 @@ export function firstDefined(...args) {
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
-export type GenericHash = { [key: string]: any }
+export type GenericHash = { [key: string]: any } // already did this somewhere
 
 // Number and Boolean are only types that defaults or not computed for
 // TODO: write more comments
+// TODO: will kill
 export function refineProps(rawProps: GenericHash, processors: GenericHash, defaults: GenericHash = {}, leftoverProps?: GenericHash): GenericHash {
   let refined: GenericHash = {}
 

+ 8 - 7
packages/common/src/validation.ts

@@ -98,8 +98,8 @@ function isInteractionPropsValid(state: SplittableProps, context: CalendarContex
 
     // overlap
 
-    let overlapFunc = context.options.eventOverlap
-    if (typeof overlapFunc !== 'function') { overlapFunc = null }
+    let { eventOverlap } = context.options
+    let eventOverlapFunc = typeof eventOverlap === 'function' ? eventOverlap : null
 
     for (let otherInstanceId in otherInstances) {
       let otherInstance = otherInstances[otherInstanceId]
@@ -117,7 +117,7 @@ function isInteractionPropsValid(state: SplittableProps, context: CalendarContex
           return false
         }
 
-        if (overlapFunc && !overlapFunc(
+        if (eventOverlapFunc && !eventOverlapFunc(
           new EventApi(context, otherDefs[otherInstance.defId], otherInstance), // still event
           new EventApi(context, subjectDef, subjectInstance) // moving event
         )) {
@@ -186,8 +186,8 @@ function isDateSelectionPropsValid(state: SplittableProps, context: CalendarCont
 
   // overlap
 
-  let overlapFunc = context.options.selectOverlap
-  if (typeof overlapFunc !== 'function') { overlapFunc = null }
+  let { selectOverlap } = context.options
+  let selectOverlapFunc = typeof selectOverlap === 'function' ? selectOverlap : null
 
   for (let relevantInstanceId in relevantInstances) {
     let relevantInstance = relevantInstances[relevantInstanceId]
@@ -199,8 +199,9 @@ function isDateSelectionPropsValid(state: SplittableProps, context: CalendarCont
         return false
       }
 
-      if (overlapFunc && !overlapFunc(
-        new EventApi(context, relevantDefs[relevantInstance.defId], relevantInstance)
+      if (selectOverlapFunc && !selectOverlapFunc(
+        new EventApi(context, relevantDefs[relevantInstance.defId], relevantInstance),
+        null
       )) {
         return false
       }

+ 2 - 1
packages/common/src/vdom-util.tsx

@@ -1,5 +1,5 @@
 import { Component, Ref } from './vdom'
-import { ViewContextType } from './ViewContext'
+import { ViewContextType, ViewContext } from './ViewContext'
 import { __assign } from 'tslib'
 import { compareObjs, EqualityFuncs, getUnequalProps } from './util/object'
 
@@ -11,6 +11,7 @@ export abstract class BaseComponent<Props={}, State={}> extends Component<Props,
   static addStateEquality = addStateEquality
   static contextType = ViewContextType
 
+  context: ViewContext
   propEquality: EqualityFuncs<Props>
   stateEquality: EqualityFuncs<State>
   debug: boolean

+ 2 - 2
packages/core/src/Calendar.tsx

@@ -1,6 +1,6 @@
 import { __assign } from 'tslib'
 import {
-  OptionsInput, Action, CalendarContent, render, h, DelayedRunner, CssDimValue, applyStyleProp,
+  RawCalendarOptions, Action, CalendarContent, render, h, DelayedRunner, CssDimValue, applyStyleProp,
   CalendarApi, computeCalendarClassNames, computeCalendarHeight, isArraysEqual, CalendarDataManager, CalendarData,
   CustomContentRenderContext
  } from '@fullcalendar/common'
@@ -20,7 +20,7 @@ export class Calendar extends CalendarApi {
   get view() { return this.currentData.viewApi } // for public API
 
 
-  constructor(el: HTMLElement, optionOverrides: OptionsInput = {}) {
+  constructor(el: HTMLElement, optionOverrides: RawCalendarOptions = {}) {
     super()
 
     this.el = el

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

@@ -20,7 +20,7 @@ export class DayTableView extends TableView {
 
 
   render() {
-    let { options, computedOptions, dateProfileGenerator } = this.context
+    let { options, dateProfileGenerator } = this.context
     let { props } = this
     let dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator)
 
@@ -44,7 +44,7 @@ export class DayTableView extends TableView {
         eventSelection={props.eventSelection}
         eventDrag={props.eventDrag}
         eventResize={props.eventResize}
-        nextDayThreshold={computedOptions.nextDayThreshold}
+        nextDayThreshold={options.nextDayThreshold}
         colGroupNode={contentArg.tableColGroupNode}
         tableMinWidth={contentArg.tableMinWidth}
         dayMaxEvents={options.dayMaxEvents}

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

@@ -1,4 +1,4 @@
-import { DateComponent, DateMarker, h, EventInstanceHash, createFormatter, Hit, addDays, DateRange, getSegMeta, DayCellRoot, DayCellContent, DateProfile } from '@fullcalendar/common'
+import { DateComponent, DateMarker, h, EventInstanceHash, Hit, addDays, DateRange, getSegMeta, DayCellRoot, DayCellContent, DateProfile } from '@fullcalendar/common'
 import { TableSeg } from './TableSeg'
 import { TableBlockEvent } from './TableBlockEvent'
 import { TableListItemEvent } from './TableListItemEvent'
@@ -28,7 +28,7 @@ export class MorePopover extends DateComponent<MorePopoverProps> {
     let { options, dateEnv } = this.context
     let { props } = this
     let { date, hiddenInstances, todayRange, dateProfile, selectedInstanceId } = props
-    let title = dateEnv.format(date, createFormatter(options.dayPopoverFormat)) // TODO: cache formatter
+    let title = dateEnv.format(date, options.dayPopoverFormat)
 
     return (
       <DayCellRoot date={date} dateProfile={dateProfile} todayRange={todayRange} elRef={this.handlePopoverEl}>

+ 1 - 1
packages/daygrid/src/Table.tsx

@@ -183,7 +183,7 @@ export class Table extends DateComponent<TableProps, TableState> {
   handleMoreLinkClick = (arg: MoreLinkArg) => { // TODO: bad names "more link click" versus "more click"
     let { context } = this
     let { dateEnv } = context
-    let clickOption = context.options.moreLinkClick
+    let clickOption = context.options.moreLinkClick || 'popover' // TODO: define the default elsewhere
 
     function segForPublic(seg: TableSeg) {
       let { def, instance, range } = seg.eventRange

+ 21 - 3
packages/daygrid/src/TableCell.tsx

@@ -19,6 +19,8 @@ import {
   DateProfile,
   VUIEvent,
   setRef,
+  createFormatter,
+  ViewApi,
 } from '@fullcalendar/common'
 import { TableSeg } from './TableSeg'
 
@@ -71,7 +73,13 @@ export interface HookProps {
   isToday: boolean
 }
 
-const DEFAULT_WEEK_NUM_FORMAT = { week: 'narrow' }
+export interface MoreLinkHookProps {
+  num: number
+  text: string
+  view: ViewApi
+}
+
+const DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'narrow' })
 
 
 export class TableCell extends DateComponent<TableCellProps> {
@@ -84,6 +92,12 @@ export class TableCell extends DateComponent<TableCellProps> {
     let { props } = this
     let { date, dateProfile } = props
 
+    let hookProps: MoreLinkHookProps = {
+      num: props.moreCnt,
+      text: props.buildMoreLinkText(props.moreCnt),
+      view: viewApi
+    }
+
     return (
       <DayCellRoot
         date={date}
@@ -131,9 +145,13 @@ export class TableCell extends DateComponent<TableCellProps> {
                 {props.fgContent}
                 {Boolean(props.moreCnt) &&
                   <div className='fc-daygrid-day-bottom' style={{ marginTop: props.moreMarginTop }}>
-                    <RenderHook name='moreLink'
-                      hookProps={{ num: props.moreCnt, text: props.buildMoreLinkText(props.moreCnt), view: viewApi }}
+                    <RenderHook<MoreLinkHookProps> // needed?
+                      hookProps={hookProps}
+                      classNames={options.moreLinkClassNames}
+                      content={options.moreLinkContent}
                       defaultContent={renderMoreLinkInner}
+                      didMount={options.moreLinkDidMount}
+                      willUnmount={options.moreLinkWillUnmount}
                     >
                       {(rootElRef, classNames, innerElRef, innerContent) => (
                         <a onClick={this.handleMoreLinkClick} ref={rootElRef} className={[ 'fc-daygrid-more-link' ].concat(classNames).join(' ')}>

+ 2 - 9
packages/daygrid/src/TableListItemEvent.tsx

@@ -1,4 +1,4 @@
-import { h, BaseComponent, Seg, EventRoot, createFormatter, buildSegTimeText, EventMeta, Fragment } from '@fullcalendar/common'
+import { h, BaseComponent, Seg, EventRoot, buildSegTimeText, EventMeta, Fragment } from '@fullcalendar/common'
 import { DEFAULT_TABLE_EVENT_TIME_FORMAT } from './event-rendering'
 
 
@@ -17,15 +17,8 @@ export class TableListItemEvent extends BaseComponent<DotTableEventProps> {
   render() {
     let { props, context } = this
 
-    // TODO: avoid createFormatter, cache!!!
-    // SOLUTION: require that props.defaultTimeFormat is a real formatter, a top-level const,
-    // which will require that defaultRangeSeparator be part of the DateEnv (possible already?),
-    // and have options.eventTimeFormat be preprocessed.
-    let timeFormat = createFormatter(
-      context.options.eventTimeFormat || DEFAULT_TABLE_EVENT_TIME_FORMAT,
-      context.options.defaultRangeSeparator
-    )
 
+    let timeFormat = context.options.eventTimeFormat || DEFAULT_TABLE_EVENT_TIME_FORMAT
     let timeText = buildSegTimeText(props.seg, timeFormat, context, true, props.defaultDisplayEventEnd)
 
     return (

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

@@ -88,7 +88,7 @@ export class TableRow extends DateComponent<TableRowProps, TableRowState> {
       state.segHeights,
       state.maxContentHeight,
       colCnt,
-      context.computedOptions.eventOrderSpecs
+      context.options.eventOrderSpecs
     )
 
     let selectedInstanceHash = // TODO: messy way to compute this

+ 3 - 3
packages/daygrid/src/event-rendering.ts

@@ -1,12 +1,12 @@
-import { EventRenderRange, diffDays } from '@fullcalendar/common'
+import { EventRenderRange, diffDays, createFormatter } from '@fullcalendar/common'
 
 
-export const DEFAULT_TABLE_EVENT_TIME_FORMAT = {
+export const DEFAULT_TABLE_EVENT_TIME_FORMAT = createFormatter({
   hour: 'numeric',
   minute: '2-digit',
   omitZeroMinute: true,
   meridiem: 'narrow'
-}
+})
 
 
 export function hasListItemDisplay(eventRange: EventRenderRange) {

+ 2 - 0
packages/daygrid/src/main.ts

@@ -2,6 +2,7 @@ import { createPlugin } from '@fullcalendar/common'
 import { DayTableView } from './DayTableView'
 import './main.scss'
 import { TableDateProfileGenerator } from './TableDateProfileGenerator'
+import { OPTION_REFINERS } from './options'
 
 export { DayTable, DayTableSlicer } from './DayTable'
 export { Table } from './Table'
@@ -13,6 +14,7 @@ export { DayTableView as DayGridView } // export as old name!
 
 export default createPlugin({
   initialView: 'dayGridMonth',
+  optionRefiners: OPTION_REFINERS,
   views: {
 
     dayGrid: {

+ 35 - 0
packages/daygrid/src/options.ts

@@ -0,0 +1,35 @@
+import {
+  identity, Identity, ViewApi, EventApi,
+  ClassNameGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler
+} from '@fullcalendar/common'
+import { MoreLinkHookProps } from './TableCell'
+
+
+// TODO: move these types to their own file
+
+export interface EventSegment {
+  event: EventApi
+  start: Date
+  end: Date
+  isStart: boolean
+  isEnd: boolean
+}
+
+export type MoreLinkClickHandler = 'popover' | 'week' | 'day' | 'timeGridWeek' | 'timeGridDay' | string |
+  ((arg: { date: Date, allDay: boolean, allSegs: EventSegment[], hiddenSegs: EventSegment[], jsEvent: MouseEvent, view: ViewApi }) => void)
+
+
+export const OPTION_REFINERS = {
+  moreLinkClick: identity as Identity<MoreLinkClickHandler>,
+  moreLinkClassNames: identity as Identity<ClassNameGenerator<MoreLinkHookProps>>,
+  moreLinkContent: identity as Identity<CustomContentGenerator<MoreLinkHookProps>>,
+  moreLinkDidMount: identity as Identity<DidMountHandler<MoreLinkHookProps>>,
+  moreLinkWillUnmount: identity as Identity<WillUnmountHandler<MoreLinkHookProps>>,
+}
+
+
+// add types
+type ExtraOptionRefiners = typeof OPTION_REFINERS
+declare module '@fullcalendar/common' {
+  interface BaseOptionRefiners extends ExtraOptionRefiners {}
+}

+ 4 - 8
packages/google-calendar/src/main.ts

@@ -1,4 +1,5 @@
 import { createPlugin, EventSourceDef, refineProps, addDays, DateEnv, requestJson } from '@fullcalendar/common'
+import { OPTION_REFINERS } from './options'
 
 // TODO: expose somehow
 const API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'
@@ -11,14 +12,8 @@ const STANDARD_PROPS = { // for event source parsing
   data: null
 }
 
-
 declare module '@fullcalendar/common' {
-
-  interface OptionsInput {
-    googleCalendarApiKey?: string
-  }
-
-  interface ExtendedEventSourceInput {
+  interface ExtendedEventSourceInput { // add this to refiner system somehow
     googleCalendarApiKey?: string
     googleCalendarId?: string
     googleCalendarApiBase?: string
@@ -190,5 +185,6 @@ function injectQsComponent(url, component) {
 }
 
 export default createPlugin({
-  eventSourceDefs: [ eventSourceDef ]
+  eventSourceDefs: [ eventSourceDef ],
+  optionRefiners: OPTION_REFINERS
 })

+ 10 - 0
packages/google-calendar/src/options.ts

@@ -0,0 +1,10 @@
+
+export const OPTION_REFINERS = {
+  googleCalendarApiKey: String
+}
+
+// add types
+type ExtraOptionRefiners = typeof OPTION_REFINERS
+declare module '@fullcalendar/common' {
+  interface BaseOptionRefiners extends ExtraOptionRefiners {}
+}

+ 3 - 3
packages/interaction/src/interactions-external/ExternalDraggable.ts

@@ -1,4 +1,4 @@
-import { globalDefaults, PointerDragEvent } from '@fullcalendar/common'
+import { RAW_BASE_DEFAULTS, PointerDragEvent } from '@fullcalendar/common'
 import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
 import { ExternalElementDragging, DragMetaGenerator } from './ExternalElementDragging'
 
@@ -47,11 +47,11 @@ export class ExternalDraggable {
     dragging.minDistance =
       minDistance != null ?
         minDistance :
-        (ev.isTouch ? 0 : globalDefaults.eventDragMinDistance)
+        (ev.isTouch ? 0 : RAW_BASE_DEFAULTS.eventDragMinDistance)
 
     dragging.delay =
       ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
-        (longPressDelay != null ? longPressDelay : globalDefaults.longPressDelay) :
+        (longPressDelay != null ? longPressDelay : RAW_BASE_DEFAULTS.longPressDelay) :
         0
   }
 

+ 1 - 0
packages/interaction/src/interactions-external/ExternalElementDragging.ts

@@ -192,6 +192,7 @@ export class ExternalElementDragging {
 
     if (typeof dropAccept === 'function') {
       return dropAccept(el)
+
     } else if (typeof dropAccept === 'string' && dropAccept) {
       return Boolean(elementMatches(el, dropAccept))
     }

+ 24 - 10
packages/list/src/ListView.tsx

@@ -20,12 +20,19 @@ import {
   NowTimer,
   ViewRoot,
   RenderHook,
-  DateComponent
+  DateComponent,
+  ViewApi
 } from '@fullcalendar/common'
 import { ListViewHeaderRow } from './ListViewHeaderRow'
 import { ListViewEventRow } from './ListViewEventRow'
 
 
+export interface NoEventsHookProps {
+  text: string
+  view: ViewApi
+}
+
+
 /*
 Responsible for the scroller, and forwarding event-related actions into the "grid".
 */
@@ -80,14 +87,21 @@ export class ListView extends DateComponent<ViewProps> {
 
 
   renderEmptyMessage() {
-    let { context } = this
-    let hookProps = {
-      text: context.options.noEventsText,
-      view: context.viewApi
+    let { options, viewApi } = this.context
+    let hookProps: NoEventsHookProps = {
+      text: options.noEventsText,
+      view: viewApi
     }
 
     return (
-      <RenderHook name='noEvents' hookProps={hookProps} defaultContent={renderNoEventsInner}>
+      <RenderHook<NoEventsHookProps> // needed???
+        hookProps={hookProps}
+        classNames={options.noEventsClassNames}
+        content={options.noEventsContent}
+        defaultContent={renderNoEventsInner}
+        didMount={options.noEventsDidMount}
+        willUnmount={options.noEventsWillUnmount}
+      >
         {(rootElRef, classNames, innerElRef, innerContent) => (
           <div className={[ 'fc-list-empty' ].concat(classNames).join(' ')} ref={rootElRef}>
             <div className='fc-list-empty-cushion' ref={innerElRef}>
@@ -101,7 +115,7 @@ export class ListView extends DateComponent<ViewProps> {
 
 
   renderSegList(allSegs: Seg[], dayDates: DateMarker[]) {
-    let { theme, computedOptions } = this.context
+    let { theme, options } = this.context
     let segsByDay = groupSegsByDay(allSegs) // sparse array
 
     return (
@@ -121,7 +135,7 @@ export class ListView extends DateComponent<ViewProps> {
               />
             )
 
-            daySegs = sortEventSegs(daySegs, computedOptions.eventOrderSpecs)
+            daySegs = sortEventSegs(daySegs, options.eventOrderSpecs)
 
             for (let seg of daySegs) {
               innerNodes.push(
@@ -154,7 +168,7 @@ export class ListView extends DateComponent<ViewProps> {
         eventStore,
         eventUiBases,
         this.props.dateProfile.activeRange,
-        this.context.computedOptions.nextDayThreshold
+        this.context.options.nextDayThreshold
       ).fg,
       dayRanges
     )
@@ -174,7 +188,7 @@ export class ListView extends DateComponent<ViewProps> {
 
   eventRangeToSegs(eventRange: EventRenderRange, dayRanges: DateRange[]) {
     let { dateEnv } = this.context
-    let { nextDayThreshold } = this.context.computedOptions
+    let { nextDayThreshold } = this.context.options
     let range = eventRange.range
     let allDay = eventRange.def.allDay
     let dayIndex

+ 15 - 12
packages/list/src/ListViewEventRow.tsx

@@ -1,14 +1,14 @@
 import {
-  MinimalEventProps, BaseComponent, ViewContext, h,
+  MinimalEventProps, BaseComponent, ViewContext, h, AllDayHookProps,
   Seg, isMultiDayRange, DateFormatter, buildSegTimeText, createFormatter, EventMeta, EventRoot, ComponentChildren, RenderHook
 } from "@fullcalendar/common"
 
 
-const DEFAULT_TIME_FORMAT = {
+const DEFAULT_TIME_FORMAT = createFormatter({
   hour: 'numeric',
   minute: '2-digit',
   meridiem: 'short'
-}
+})
 
 
 export class ListViewEventRow extends BaseComponent<MinimalEventProps> {
@@ -17,11 +17,7 @@ export class ListViewEventRow extends BaseComponent<MinimalEventProps> {
     let { props, context } = this
     let { seg } = props
 
-    // TODO: avoid createFormatter, cache!!! see TODO in StandardEvent
-    let timeFormat = createFormatter(
-      context.options.eventTimeFormat || DEFAULT_TIME_FORMAT,
-      context.options.defaultRangeSeparator
-    )
+    let timeFormat = context.options.eventTimeFormat || DEFAULT_TIME_FORMAT
 
     return (
       <EventRoot
@@ -72,9 +68,9 @@ function renderEventInnerContent(props: EventMeta) {
 
 
 function buildTimeContent(seg: Seg, timeFormat: DateFormatter, context: ViewContext): ComponentChildren {
-  let { displayEventTime } = context.options
+  let { options } = context
 
-  if (displayEventTime !== false) {
+  if (options.displayEventTime !== false) {
     let eventDef = seg.eventRange.def
     let eventInstance = seg.eventRange.instance
     let doAllDay = false
@@ -120,13 +116,20 @@ function buildTimeContent(seg: Seg, timeFormat: DateFormatter, context: ViewCont
     }
 
     if (doAllDay) {
-      let hookProps = {
+      let hookProps: AllDayHookProps = {
         text: context.options.allDayText,
         view: context.viewApi
       }
 
       return (
-        <RenderHook name='allDay' hookProps={hookProps} defaultContent={renderAllDayInner}>
+        <RenderHook<AllDayHookProps> // needed?
+          hookProps={hookProps}
+          classNames={options.allDayClassNames}
+          content={options.allDayContent}
+          defaultContent={renderAllDayInner}
+          didMount={options.allDayDidMount}
+          willUnmount={options.allDayWillUnmount}
+        >
           {(rootElRef, classNames, innerElRef, innerContent) => (
             <td className={[ 'fc-list-event-time' ].concat(classNames).join(' ')} ref={rootElRef}>
               {innerContent}

+ 13 - 8
packages/list/src/ListViewHeaderRow.tsx

@@ -1,6 +1,6 @@
 import {
-  BaseComponent, DateMarker, createFormatter, h, DateRange, getDateMeta,
-  RenderHook, buildNavLinkData, DateHeaderCellHookProps, getDayClassNames, formatDayString
+  BaseComponent, DateMarker, h, DateRange, getDateMeta,
+  RenderHook, buildNavLinkData, DayHeaderHookProps, getDayClassNames, formatDayString
 } from '@fullcalendar/common'
 
 
@@ -9,7 +9,7 @@ export interface ListViewHeaderRowProps {
   todayRange: DateRange
 }
 
-interface HookProps extends DateHeaderCellHookProps { // doesn't enforce much since DayCellHookProps allow extra props
+interface HookProps extends DayHeaderHookProps { // doesn't enforce much since DayCellHookProps allow extra props
   text: string
   sideText: string
 }
@@ -23,10 +23,8 @@ export class ListViewHeaderRow extends BaseComponent<ListViewHeaderRowProps> {
     let { theme, dateEnv, options, viewApi } = this.context
 
     let dayMeta = getDateMeta(dayDate, todayRange)
-    let mainFormat = createFormatter(options.listDayFormat) // TODO: cache
-    let sideFormat = createFormatter(options.listDaySideFormat) // TODO: cache
-    let text = mainFormat ? dateEnv.format(dayDate, mainFormat) : '' // will ever be falsy?
-    let sideText = sideFormat ? dateEnv.format(dayDate, sideFormat) : '' // will ever be falsy? also, BAD NAME "alt"
+    let text = options.listDayFormat ? dateEnv.format(dayDate, options.listDayFormat) : '' // will ever be falsy?
+    let sideText = options.listDaySideFormat ? dateEnv.format(dayDate, options.listDaySideFormat) : '' // will ever be falsy? also, BAD NAME "alt"
 
     let navLinkData = options.navLinks
       ? buildNavLinkData(dayDate)
@@ -47,7 +45,14 @@ export class ListViewHeaderRow extends BaseComponent<ListViewHeaderRowProps> {
 
     // TODO: make a reusable HOC for dayHeader (used in daygrid/timegrid too)
     return (
-      <RenderHook name='dayHeader' hookProps={hookProps} defaultContent={renderInnerContent}>
+      <RenderHook<HookProps>
+        hookProps={hookProps}
+        classNames={options.dayHeaderClassNames}
+        content={options.dayHeaderContent}
+        defaultContent={renderInnerContent}
+        didMount={options.dayHeaderDidMount}
+        willUnmount={options.dayHeaderWillUnmount}
+      >
         {(rootElRef, customClassNames, innerElRef, innerContent) => (
           <tr
             ref={rootElRef}

+ 2 - 0
packages/list/src/main.ts

@@ -1,10 +1,12 @@
 import { createPlugin } from '@fullcalendar/common'
 import { ListView } from './ListView'
+import { OPTION_REFINERS } from './options'
 import './main.scss'
 
 export { ListView }
 
 export default createPlugin({
+  optionRefiners: OPTION_REFINERS,
   views: {
 
     list: {

+ 25 - 0
packages/list/src/options.ts

@@ -0,0 +1,25 @@
+import { identity, Identity, ClassNameGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler, createFormatter, FormatterInput } from '@fullcalendar/common'
+import { NoEventsHookProps } from './ListView'
+
+export const OPTION_REFINERS = {
+  noEventsText: String,
+
+  noEventsClassNames: identity as Identity<ClassNameGenerator<NoEventsHookProps>>,
+  noEventsContent: identity as Identity<CustomContentGenerator<NoEventsHookProps>>,
+  noEventsDidMount: identity as Identity<DidMountHandler<NoEventsHookProps>>,
+  noEventsWillUnmount: identity as Identity<WillUnmountHandler<NoEventsHookProps>>,
+
+  listDayFormat: createFalsableFormatter, // defaults specified in list plugins
+  listDaySideFormat: createFalsableFormatter, // "
+}
+
+function createFalsableFormatter(input: FormatterInput | false) {
+  return input === false ? null : createFormatter(input)
+}
+
+
+// add types
+type ExtraOptionRefiners = typeof OPTION_REFINERS
+declare module '@fullcalendar/common' {
+  interface BaseOptionRefiners extends ExtraOptionRefiners {}
+}

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

@@ -20,12 +20,12 @@ export class DayTimeColsView extends TimeColsView {
 
 
   render() {
-    let { options, computedOptions, dateEnv, dateProfileGenerator } = this.context
+    let { options, dateEnv, dateProfileGenerator } = this.context
     let { props } = this
     let { dateProfile } = props
     let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator)
     let splitProps = this.allDaySplitter.splitProps(props)
-    let slatMetas = this.buildSlatMetas(dateProfile.slotMinTime, dateProfile.slotMaxTime, options.slotLabelInterval, computedOptions.slotDuration, dateEnv)
+    let slatMetas = this.buildSlatMetas(dateProfile.slotMinTime, dateProfile.slotMaxTime, options.slotLabelInterval, options.slotDuration, dateEnv)
     let { dayMinWidth } = options
 
     let headerContent = options.dayHeaders &&
@@ -36,12 +36,12 @@ export class DayTimeColsView extends TimeColsView {
         renderIntro={dayMinWidth ? null : this.renderHeadAxis}
       />
 
-    let allDayContent = options.allDaySlot && ((contentArg: ChunkContentCallbackArgs) => (
+    let allDayContent = (options.allDaySlot !== false) && ((contentArg: ChunkContentCallbackArgs) => (
       <DayTable
         {...splitProps['allDay']}
         dateProfile={dateProfile}
         dayTableModel={dayTableModel}
-        nextDayThreshold={computedOptions.nextDayThreshold}
+        nextDayThreshold={options.nextDayThreshold}
         tableMinWidth={contentArg.tableMinWidth}
         colGroupNode={contentArg.tableColGroupNode}
         renderRowIntro={dayMinWidth ? null : this.renderTableRowAxis}
@@ -60,7 +60,7 @@ export class DayTimeColsView extends TimeColsView {
         dayTableModel={dayTableModel}
         dateProfile={dateProfile}
         axis={!dayMinWidth}
-        slotDuration={computedOptions.slotDuration}
+        slotDuration={options.slotDuration}
         slatMetas={slatMetas}
         forPrint={props.forPrint}
         tableColGroupNode={contentArg.tableColGroupNode}

+ 1 - 1
packages/timegrid/src/TimeCol.tsx

@@ -105,7 +105,7 @@ export class TimeCol extends BaseComponent<TimeColProps> {
 
     // assigns TO THE SEGS THEMSELVES
     // also, receives resorted array
-    segs = computeSegCoords(segs, props.date, props.slatCoords, context.options.eventMinHeight, context.computedOptions.eventOrderSpecs) as TimeColsSeg[]
+    segs = computeSegCoords(segs, props.date, props.slatCoords, context.options.eventMinHeight, context.options.eventOrderSpecs) as TimeColsSeg[]
 
     return segs.map((seg) => {
       let instanceId = seg.eventRange.instance.instanceId

+ 3 - 3
packages/timegrid/src/TimeColEvent.tsx

@@ -1,11 +1,11 @@
-import { h, StandardEvent, BaseComponent, MinimalEventProps } from '@fullcalendar/common'
+import { h, StandardEvent, BaseComponent, MinimalEventProps, createFormatter } from '@fullcalendar/common'
 
 
-const DEFAULT_TIME_FORMAT = {
+const DEFAULT_TIME_FORMAT = createFormatter({
   hour: 'numeric',
   minute: '2-digit',
   meridiem: false
-}
+})
 
 
 export class TimeColEvent extends BaseComponent<MinimalEventProps> {

+ 2 - 2
packages/timegrid/src/TimeCols.tsx

@@ -158,11 +158,11 @@ export class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
 
 
   positionToHit(positionLeft, positionTop) {
-    let { dateEnv, computedOptions } = this.context
+    let { dateEnv, options } = this.context
     let { colCoords } = this
     let { dateProfile } = this.props
     let { slatCoords } = this.state
-    let { snapDuration, snapsPerSlot } = this.processSlotOptions(this.props.slotDuration, computedOptions.snapDuration)
+    let { snapDuration, snapsPerSlot } = this.processSlotOptions(this.props.slotDuration, options.snapDuration)
 
     let colIndex = colCoords.leftToIndex(positionLeft)
     let slatIndex = slatCoords.positions.topToIndex(positionTop)

+ 27 - 24
packages/timegrid/src/TimeColsSlats.tsx

@@ -17,7 +17,9 @@ import {
   DateEnv,
   ViewContextType,
   RenderHook,
-  DateProfile
+  DateProfile,
+  SlotLabelHookProps,
+  SlotLaneHookProps
 } from '@fullcalendar/common'
 import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
 
@@ -138,16 +140,18 @@ export class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> {
 
   render() {
     let { props, context } = this
+    let { options } = context
     let { slatElRefs } = props
 
     return (
       <tbody>
         {props.slatMetas.map((slatMeta, i) => {
-          let hookProps = {
+          let hookProps: SlotLaneHookProps = {
             time: slatMeta.time,
             date: context.dateEnv.toDate(slatMeta.date),
             view: context.viewApi
           }
+
           let classNames = [
             'fc-timegrid-slot',
             'fc-timegrid-slot-lane',
@@ -162,7 +166,13 @@ export class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> {
               {props.axis &&
                 <TimeColsAxisCell {...slatMeta} />
               }
-              <RenderHook name='slotLane' hookProps={hookProps}>
+              <RenderHook
+                hookProps={hookProps}
+                classNames={options.slotLaneClassNames}
+                content={options.slotLaneContent}
+                didMount={options.slotLaneDidMount}
+                willUnmount={options.slotLaneWillUnmount}
+              >
                 {(rootElRef, customClassNames, innerElRef, innerContent) => (
                   <td
                     ref={rootElRef}
@@ -181,12 +191,12 @@ export class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> {
 }
 
 
-const DEFAULT_SLAT_LABEL_FORMAT = {
+const DEFAULT_SLAT_LABEL_FORMAT = createFormatter({
   hour: 'numeric',
   minute: '2-digit',
   omitZeroMinute: true,
   meridiem: 'short'
-}
+})
 
 export function TimeColsAxisCell(props: TimeSlatMeta) {
   let classNames = [
@@ -206,8 +216,8 @@ export function TimeColsAxisCell(props: TimeSlatMeta) {
 
         } else {
           let { dateEnv, options, viewApi } = context
-          let labelFormat = createFormatter(options.slotLabelFormat || DEFAULT_SLAT_LABEL_FORMAT) // TODO: optimize!!!
-          let hookProps = {
+          let labelFormat = options.slotLabelFormat || DEFAULT_SLAT_LABEL_FORMAT
+          let hookProps: SlotLabelHookProps = {
             time: props.time,
             date: dateEnv.toDate(props.date),
             view: viewApi,
@@ -215,7 +225,14 @@ export function TimeColsAxisCell(props: TimeSlatMeta) {
           }
 
           return (
-            <RenderHook name='slotLabel' hookProps={hookProps} defaultContent={renderInnerContent}>
+            <RenderHook<SlotLabelHookProps> // needed?
+              hookProps={hookProps}
+              classNames={options.slotLabelClassNames}
+              content={options.slotLabelContent}
+              defaultContent={renderInnerContent}
+              didMount={options.slotLabelDidMount}
+              willUnmount={options.slotLabelWillUnmount}
+            >
               {(rootElRef, customClassNames, innerElRef, innerContent) => (
                 <td ref={rootElRef} className={classNames.concat(customClassNames).join(' ')} data-time={props.isoTimeStr}>
                   <div className='fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame'>
@@ -247,11 +264,11 @@ export interface TimeSlatMeta {
   isLabeled: boolean
 }
 
-export function buildSlatMetas(slotMinTime: Duration, slotMaxTime: Duration, labelIntervalInput, slotDuration: Duration, dateEnv: DateEnv) {
+export function buildSlatMetas(slotMinTime: Duration, slotMaxTime: Duration, explicitLabelInterval: Duration | null, slotDuration: Duration, dateEnv: DateEnv) {
   let dayStart = new Date(0)
   let slatTime = slotMinTime
   let slatIterator = createDuration(0)
-  let labelInterval = getLabelInterval(labelIntervalInput, slotDuration)
+  let labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration)
   let metas: TimeSlatMeta[] = []
 
   while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) {
@@ -274,20 +291,6 @@ export function buildSlatMetas(slotMinTime: Duration, slotMaxTime: Duration, lab
 }
 
 
-function getLabelInterval(optionInput, slotDuration: Duration) {
-
-  // might be an array value (for TimelineView).
-  // if so, getting the most granular entry (the last one probably).
-  if (Array.isArray(optionInput)) {
-    optionInput = optionInput[optionInput.length - 1]
-  }
-
-  return optionInput ?
-    createDuration(optionInput) :
-    computeLabelInterval(slotDuration)
-}
-
-
 // Computes an automatic value for slotLabelInterval
 function computeLabelInterval(slotDuration) {
   let i

+ 16 - 7
packages/timegrid/src/TimeColsView.tsx

@@ -16,13 +16,15 @@ import {
   RefObject,
   renderScrollShim,
   getStickyHeaderDates,
-  getStickyFooterScrollbar
+  getStickyFooterScrollbar,
+  createFormatter,
+  AllDayHookProps
 } from '@fullcalendar/common'
 import { AllDaySplitter } from './AllDaySplitter'
 import { TimeSlatMeta, TimeColsAxisCell } from './TimeColsSlats'
 
 
-const DEFAULT_WEEK_NUM_FORMAT = { week: 'short' }
+const DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'short' })
 const AUTO_ALL_DAY_MAX_EVENT_ROWS = 5
 
 
@@ -289,15 +291,22 @@ export abstract class TimeColsView extends DateComponent<ViewProps> {
   // only a one-way height sync. we don't send the axis inner-content height to the DayGrid,
   // but DayGrid still needs to have classNames on inner elements in order to measure.
   renderTableRowAxis = (rowHeight?: number) => {
-    let { context } = this
-    let hookProps = {
-      text: context.options.allDayText,
-      view: context.viewApi
+    let { options, viewApi } = this.context
+    let hookProps: AllDayHookProps = {
+      text: options.allDayText,
+      view: viewApi
     }
 
     return (
       // TODO: make reusable hook. used in list view too
-      <RenderHook name='allDay' hookProps={hookProps} defaultContent={renderAllDayInner}>
+      <RenderHook<AllDayHookProps>
+        hookProps={hookProps}
+        classNames={options.allDayClassNames}
+        content={options.allDayContent}
+        defaultContent={renderAllDayInner}
+        didMount={options.allDayDidMount}
+        willUnmount={options.allDayWillUnmount}
+      >
         {(rootElRef, classNames, innerElRef, innerContent) => (
           <td ref={rootElRef} className={[
             'fc-timegrid-axis',

+ 2 - 0
packages/timegrid/src/main.ts

@@ -3,6 +3,7 @@ import { TimeColsView } from './TimeColsView'
 import { DayTimeColsView, buildTimeColsModel } from './DayTimeColsView'
 import { TimeColsSeg } from './TimeColsSeg'
 import { DayTimeCols, DayTimeColsSlicer, buildDayRanges } from './DayTimeCols'
+import { OPTION_REFINERS } from './options'
 import './main.scss'
 
 export { DayTimeCols, DayTimeColsView, TimeColsView, buildTimeColsModel, buildDayRanges, DayTimeColsSlicer, TimeColsSeg }
@@ -12,6 +13,7 @@ export { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
 
 export default createPlugin({
   initialView: 'timeGridWeek',
+  optionRefiners: OPTION_REFINERS,
   views: {
 
     timeGrid: {

+ 10 - 0
packages/timegrid/src/options.ts

@@ -0,0 +1,10 @@
+
+export const OPTION_REFINERS = {
+  allDaySlot: Boolean
+}
+
+// add types
+type ExtraOptionRefiners = typeof OPTION_REFINERS
+declare module '@fullcalendar/common' {
+  interface BaseOptionRefiners extends ExtraOptionRefiners {}
+}