Преглед изворни кода

adjust to new options system

Adam Shaw пре 5 година
родитељ
комит
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
       // 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(timedDuration.as('hours')).toBe(5)
       expect(allDayDuration.as('days')).toBe(3)
       expect(allDayDuration.as('days')).toBe(3)
@@ -105,7 +105,7 @@ describe('luxon plugin', function() {
       })
       })
 
 
       // hacky way to have a duration parsed
       // 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')
       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
       // 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(timedDuration.asHours()).toBe(5)
       expect(allDayDuration.asDays()).toBe(3)
       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>) {
 export function waitEventDrag(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
   return new Promise<any>((resolve) => {
-    let modifiedEvent = false
+    let modifiedEvent: any = false
 
 
     calendar.on('eventDrop', function(arg) {
     calendar.on('eventDrop', function(arg) {
       modifiedEvent = arg.event
       modifiedEvent = arg.event
@@ -24,7 +24,7 @@ export function waitEventDrag(calendar: Calendar, dragging: Promise<any>) {
 
 
 export function waitEventDrag2(calendar: Calendar, dragging: Promise<any>) {
 export function waitEventDrag2(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
   return new Promise<any>((resolve) => {
-    let theArg = false
+    let theArg: any = false
 
 
     calendar.on('eventDrop', function(arg) {
     calendar.on('eventDrop', function(arg) {
       theArg = arg
       theArg = arg
@@ -45,7 +45,7 @@ export function waitEventDrag2(calendar: Calendar, dragging: Promise<any>) {
 
 
 export function waitEventResize(calendar: Calendar, dragging: Promise<any>) {
 export function waitEventResize(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
   return new Promise<any>((resolve) => {
-    let modifiedEvent = false
+    let modifiedEvent: any = false
 
 
     calendar.on('eventResize', function(arg) {
     calendar.on('eventResize', function(arg) {
       modifiedEvent = arg.event
       modifiedEvent = arg.event
@@ -62,7 +62,7 @@ export function waitEventResize(calendar: Calendar, dragging: Promise<any>) {
 
 
 export function waitEventResize2(calendar: Calendar, dragging: Promise<any>) {
 export function waitEventResize2(calendar: Calendar, dragging: Promise<any>) {
   return new Promise<any>((resolve) => {
   return new Promise<any>((resolve) => {
-    let theArg = false
+    let theArg: any = false
 
 
     calendar.on('eventResize', function(arg) {
     calendar.on('eventResize', function(arg) {
       theArg = arg
       theArg = arg

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

@@ -32,7 +32,7 @@ BootstrapTheme.prototype.rtlIconClasses = {
   nextYear: 'fa-angle-double-left'
   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.iconOverrideCustomButtonOption = 'bootstrapFontAwesome'
 BootstrapTheme.prototype.iconOverridePrefix = 'fa-'
 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 { CalendarDataManager } from './reducers/CalendarDataManager'
 import { Action } from './reducers/Action'
 import { Action } from './reducers/Action'
 import { EventSource } from './structs/event-source'
 import { EventSource } from './structs/event-source'
+import { RawCalendarOptions, CalendarListeners } from './options'
 
 
 
 
 export class CalendarApi {
 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({
     this.dispatch({
       type: 'SET_OPTION',
       type: 'SET_OPTION',
       optionName: name,
       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)
     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)
     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)
     this.currentDataManager!.emitter.trigger(handlerName, ...args)
   }
   }
 
 
@@ -105,7 +106,7 @@ export class CalendarApi {
           this.dispatch({ // not very efficient to do two dispatches
           this.dispatch({ // not very efficient to do two dispatches
             type: 'SET_OPTION',
             type: 'SET_OPTION',
             optionName: 'visibleRange',
             optionName: 'visibleRange',
-            optionValue: dateOrRange
+            rawOptionValue: dateOrRange
           })
           })
 
 
         } else {
         } else {
@@ -222,7 +223,7 @@ export class CalendarApi {
     this.unselect()
     this.unselect()
     this.dispatch({
     this.dispatch({
       type: 'CHANGE_DATE',
       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 { __assign } from 'tslib'
 import { h, createRef, Component, VUIEvent } from './vdom'
 import { h, createRef, Component, VUIEvent } from './vdom'
 import { buildDelegationHandler } from './util/dom-event'
 import { buildDelegationHandler } from './util/dom-event'
-import { capitaliseFirstLetter } from './util/misc'
 import { ViewContainer } from './ViewContainer'
 import { ViewContainer } from './ViewContainer'
 import { CssDimValue } from './scrollgrid/util'
 import { CssDimValue } from './scrollgrid/util'
 import { getCanVGrowWithinCell } from './util/table-styling'
 import { getCanVGrowWithinCell } from './util/table-styling'
@@ -61,7 +60,7 @@ export class CalendarContent extends Component<CalendarContentProps, CalendarCon
       props.dateProfile,
       props.dateProfile,
       props.dateProfileGenerator,
       props.dateProfileGenerator,
       props.currentDate,
       props.currentDate,
-      getNow(props.options, props.dateEnv), // TODO: use NowTimer????
+      getNow(props.options.now, props.dateEnv), // TODO: use NowTimer????
       props.viewTitle
       props.viewTitle
     )
     )
 
 
@@ -86,7 +85,6 @@ export class CalendarContent extends Component<CalendarContentProps, CalendarCon
       props.viewSpec,
       props.viewSpec,
       props.viewApi,
       props.viewApi,
       props.options,
       props.options,
-      props.computedOptions,
       props.dateProfileGenerator,
       props.dateProfileGenerator,
       props.dateEnv,
       props.dateEnv,
       props.theme,
       props.theme,
@@ -185,8 +183,9 @@ export class CalendarContent extends Component<CalendarContentProps, CalendarCon
     let dateMarker = dateEnv.createMarker(navLinkOptions.date)
     let dateMarker = dateEnv.createMarker(navLinkOptions.date)
     let viewType = navLinkOptions.type
     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') {
     if (typeof customAction === 'function') {
       customAction(dateEnv.toDate(dateMarker), ev)
       customAction(dateEnv.toDate(dateMarker), ev)

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

@@ -1,5 +1,5 @@
 import { DateEnv } from './datelib/env'
 import { DateEnv } from './datelib/env'
-import { ComputedOptions } from './ComputedOptions'
+import { RefinedBaseOptions } from './options'
 import { PluginHooks } from './plugin-system-struct'
 import { PluginHooks } from './plugin-system-struct'
 import { Emitter } from './common/Emitter'
 import { Emitter } from './common/Emitter'
 import { Action } from './reducers/Action'
 import { Action } from './reducers/Action'
@@ -8,8 +8,7 @@ import { CalendarData } from './reducers/data-types'
 
 
 export interface CalendarContext {
 export interface CalendarContext {
   dateEnv: DateEnv
   dateEnv: DateEnv
-  options: any
-  computedOptions: ComputedOptions
+  options: RefinedBaseOptions // does not have calendar-specific properties. aims to be compatible with RefinedViewOptions
   pluginHooks: PluginHooks
   pluginHooks: PluginHooks
   emitter: Emitter
   emitter: Emitter
   dispatch(action: Action): void
   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 { 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 { ViewSpec } from './structs/view-spec'
 import { DateEnv, DateInput } from './datelib/env'
 import { DateEnv, DateInput } from './datelib/env'
 import { computeVisibleDayRange } from './util/date'
 import { computeVisibleDayRange } from './util/date'
@@ -26,34 +26,30 @@ export interface DateProfileGeneratorProps extends DateProfileOptions {
 }
 }
 
 
 export interface DateProfileOptions {
 export interface DateProfileOptions {
-  slotMinTime: DurationInput
-  slotMaxTime: DurationInput
+  slotMinTime: Duration
+  slotMaxTime: Duration
   showNonCurrentDates?: boolean
   showNonCurrentDates?: boolean
   dayCount?: number
   dayCount?: number
   dateAlignment?: string
   dateAlignment?: string
-  dateIncrement?: DurationInput
+  dateIncrement?: Duration
   hiddenDays?: number[]
   hiddenDays?: number[]
   weekends?: boolean
   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
   monthMode?: boolean
-  fixedWeekCount?: number
+  fixedWeekCount?: boolean
 }
 }
 
 
 
 
 export class DateProfileGenerator { // only publicly used for isHiddenDay :(
 export class DateProfileGenerator { // only publicly used for isHiddenDay :(
 
 
-  slotMinTime: Duration
-  slotMaxTime: Duration
   nowDate: DateMarker
   nowDate: DateMarker
   isHiddenDayHash: boolean[]
   isHiddenDayHash: boolean[]
 
 
 
 
   constructor(protected props: DateProfileGeneratorProps) {
   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()
     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
   // Optional direction param indicates whether the date is being incremented/decremented
   // from its previous value. decremented = -1, incremented = 1 (default).
   // from its previous value. decremented = -1, incremented = 1 (default).
   build(currentDate: DateMarker, direction?, forceToValid = true): DateProfile {
   build(currentDate: DateMarker, direction?, forceToValid = true): DateProfile {
+    let { props } = this
     let validRange: DateRange
     let validRange: DateRange
     let currentInfo
     let currentInfo
     let isRangeAllDay
     let isRangeAllDay
@@ -116,7 +113,7 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
     renderRange = this.trimHiddenDays(renderRange)
     renderRange = this.trimHiddenDays(renderRange)
     activeRange = renderRange
     activeRange = renderRange
 
 
-    if (!this.props.showNonCurrentDates) {
+    if (!props.showNonCurrentDates) {
       activeRange = intersectRanges(activeRange, currentInfo.range)
       activeRange = intersectRanges(activeRange, currentInfo.range)
     }
     }
 
 
@@ -150,10 +147,10 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
       renderRange,
       renderRange,
 
 
       // Duration object that denotes the first visible time of any given day
       // 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
       // Duration object that denotes the exclusive visible end time of any given day
-      slotMaxTime: this.slotMaxTime,
+      slotMaxTime: props.slotMaxTime,
 
 
       isValid,
       isValid,
 
 
@@ -168,7 +165,12 @@ export class DateProfileGenerator { // only publicly used for isHiddenDay :(
   // Indicates the minimum/maximum dates to display.
   // Indicates the minimum/maximum dates to display.
   // not responsible for trimming hidden days.
   // not responsible for trimming hidden days.
   buildValidRange(): OpenDateRange {
   buildValidRange(): OpenDateRange {
-    return this.getRangeOption('validRange', this.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
       { 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)
   // Returns a new activeRange to have time values (un-ambiguate)
   // slotMinTime or slotMaxTime causes the range to expand.
   // slotMinTime or slotMaxTime causes the range to expand.
   adjustActiveRange(range: DateRange) {
   adjustActiveRange(range: DateRange) {
-    let { dateEnv, viewSpec } = this.props
-    let { slotMinTime, slotMaxTime } = this
+    let { dateEnv, viewSpec, slotMinTime, slotMaxTime } = this.props
     let start = range.start
     let start = range.start
     let end = range.end
     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,
   // Builds a normalized range object for the "visible" range,
   // which is a way to define the currentRange and activeRange at the same time.
   // which is a way to define the currentRange and activeRange at the same time.
   buildCustomVisibleRange(date: DateMarker) {
   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 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) {
   constructor(props: NowTimerProps, context: ViewContext) {
     super(props, context)
     super(props, context)
 
 
-    this.initialNowDate = getNow(context.options, context.dateEnv)
+    this.initialNowDate = getNow(context.options.now, context.dateEnv)
     this.initialNowQueriedMs = new Date().valueOf()
     this.initialNowQueriedMs = new Date().valueOf()
 
 
     this.state = this.computeTiming().currentState
     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 { DateComponent } from './component/DateComponent'
 import { CalendarContext } from './CalendarContext'
 import { CalendarContext } from './CalendarContext'
 import { createDuration } from './datelib/duration'
 import { createDuration } from './datelib/duration'
+import { RefinedViewOptions } from './options'
 
 
 export const ViewContextType = createContext<ViewContext>({} as any) // for Components
 export const ViewContextType = createContext<ViewContext>({} as any) // for Components
 export type ResizeHandler = (force: boolean) => void
 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.
 can pass in their ViewContext to util functions that accept CalendarContext.
 */
 */
 export interface ViewContext extends CalendarContext {
 export interface ViewContext extends CalendarContext {
+  options: RefinedViewOptions // more specific than RefinedBaseOptions
   theme: Theme
   theme: Theme
   isRtl: boolean
   isRtl: boolean
   dateProfileGenerator: DateProfileGenerator
   dateProfileGenerator: DateProfileGenerator
@@ -38,8 +40,7 @@ export interface ViewContext extends CalendarContext {
 export function buildViewContext(
 export function buildViewContext(
   viewSpec: ViewSpec,
   viewSpec: ViewSpec,
   viewApi: ViewApi,
   viewApi: ViewApi,
-  viewOptions: any,
-  computedViewOptions: any,
+  viewOptions: RefinedViewOptions,
   dateProfileGenerator: DateProfileGenerator,
   dateProfileGenerator: DateProfileGenerator,
   dateEnv: DateEnv,
   dateEnv: DateEnv,
   theme: Theme,
   theme: Theme,
@@ -54,7 +55,6 @@ export function buildViewContext(
   return {
   return {
     dateEnv,
     dateEnv,
     options: viewOptions,
     options: viewOptions,
-    computedOptions: computedViewOptions,
     pluginHooks,
     pluginHooks,
     emitter,
     emitter,
     dispatch,
     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 { EventDef, NON_DATE_PROPS, DATE_PROPS } from '../structs/event-def'
 import { EventInstance } from '../structs/event-instance'
 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 { EventMutation } from '../structs/event-mutation'
 import { DateInput } from '../datelib/env'
 import { DateInput } from '../datelib/env'
 import { diffDates, computeAlignedDayRange } from '../util/date'
 import { diffDates, computeAlignedDayRange } from '../util/date'
@@ -38,12 +38,10 @@ export class EventApi {
         standardProps: { [name]: val }
         standardProps: { [name]: val }
       })
       })
 
 
-    } else if (name in UNSCOPED_EVENT_UI_PROPS) {
+    } else if (name in UI_PROPS_REFINERS) {
       let ui
       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') {
       if (name === 'color') {
         ui = { backgroundColor: val, borderColor: val }
         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.
 // Given an event's allDay status and start date, return what its fallback end date should be.
 // TODO: rename to computeDefaultEventEnd
 // TODO: rename to computeDefaultEventEnd
 export function getDefaultEventEnd(allDay: boolean, marker: DateMarker, context: CalendarContext): DateMarker {
 export function getDefaultEventEnd(allDay: boolean, marker: DateMarker, context: CalendarContext): DateMarker {
-  let { dateEnv, computedOptions } = context
+  let { dateEnv, options } = context
   let end = marker
   let end = marker
 
 
   if (allDay) {
   if (allDay) {
     end = startOfDay(end)
     end = startOfDay(end)
-    end = dateEnv.add(end, computedOptions.defaultAllDayEventDuration)
+    end = dateEnv.add(end, options.defaultAllDayEventDuration)
   } else {
   } else {
-    end = dateEnv.add(end, computedOptions.defaultTimedEventDuration)
+    end = dateEnv.add(end, options.defaultTimedEventDuration)
   }
   }
 
 
   return end
   return end

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

@@ -1,23 +1,27 @@
 import { Ref, ComponentChildren, h } from '../vdom'
 import { Ref, ComponentChildren, h } from '../vdom'
 import { DateMarker } from '../datelib/marker'
 import { DateMarker } from '../datelib/marker'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
-import { ViewContext } from '../ViewContext'
 import { getDateMeta, getDayClassNames, DateMeta } from '../component/date-rendering'
 import { getDateMeta, getDayClassNames, DateMeta } from '../component/date-rendering'
 import { createFormatter } from '../datelib/formatting'
 import { createFormatter } from '../datelib/formatting'
 import { formatDayString } from '../datelib/formatting-utils'
 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 { ViewApi } from '../ViewApi'
 import { BaseComponent } from '../vdom-util'
 import { BaseComponent } from '../vdom-util'
 import { DateProfile } from '../DateProfileGenerator'
 import { DateProfile } from '../DateProfileGenerator'
+import { memoizeObjArg } from '../util/memoize'
+import { DateEnv } from '../datelib/env'
 
 
 
 
 const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
 const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
 
 
-interface DayCellHookPropOrigin {
+interface RawDayCellHookProps {
   date: DateMarker // generic
   date: DateMarker // generic
   dateProfile: DateProfile
   dateProfile: DateProfile
   todayRange: DateRange
   todayRange: DateRange
+  dateEnv: DateEnv
+  viewApi: ViewApi
   showDayNumber?: boolean // defaults to false
   showDayNumber?: boolean // defaults to false
+  extraProps?: object // so can include a resource
 }
 }
 
 
 export interface DayCellHookProps extends DateMeta {
 export interface DayCellHookProps extends DateMeta {
@@ -45,27 +49,27 @@ export interface DayCellRootProps {
 
 
 export class DayCellRoot extends BaseComponent<DayCellRootProps> {
 export class DayCellRoot extends BaseComponent<DayCellRootProps> {
 
 
-  buildClassNames = buildHookClassNameGenerator<DayCellHookProps>('dayCell')
+  refineHookProps = memoizeObjArg(refineHookProps)
+  normalizeClassNames = buildClassNameNormalizer<DayCellHookProps>()
 
 
 
 
   render() {
   render() {
     let { props, context } = this
     let { props, context } = this
-
-    let hookPropsOrigin: DayCellHookPropOrigin = {
+    let { options } = context
+    let hookProps = this.refineHookProps({
       date: props.date,
       date: props.date,
       dateProfile: props.dateProfile,
       dateProfile: props.dateProfile,
       todayRange: props.todayRange,
       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(
     let classNames = getDayClassNames(hookProps, context.theme).concat(
       hookProps.isDisabled
       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 ? {} : {
     let dataAttrs = hookProps.isDisabled ? {} : {
@@ -73,7 +77,12 @@ export class DayCellRoot extends BaseComponent<DayCellRootProps> {
     }
     }
 
 
     return (
     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)}
         {(rootElRef) => props.children(rootElRef, classNames, dataAttrs, hookProps.isDisabled)}
       </MountHook>
       </MountHook>
     )
     )
@@ -99,20 +108,23 @@ export class DayCellContent extends BaseComponent<DayCellContentProps> {
 
 
   render() {
   render() {
     let { props, context } = this
     let { props, context } = this
-
-    let hookPropsOrigin: DayCellHookPropOrigin = {
+    let { options } = context
+    let hookProps = refineHookProps({
       date: props.date,
       date: props.date,
       dateProfile: props.dateProfile,
       dateProfile: props.dateProfile,
       todayRange: props.todayRange,
       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 (
     return (
-      <ContentHook name='dayCell' hookProps={hookProps} defaultContent={props.defaultContent}>
+      <ContentHook
+        hookProps={hookProps}
+        content={options.dayCellContent}
+        defaultContent={props.defaultContent}
+      >
         {props.children}
         {props.children}
       </ContentHook>
       </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 {
   return {
     date: dateEnv.toDate(date),
     date: dateEnv.toDate(date),
-    view: context.viewApi,
+    view: raw.viewApi,
     ...dayMeta,
     ...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 { BaseComponent } from '../vdom-util'
 import { DateMarker } from '../datelib/marker'
 import { DateMarker } from '../datelib/marker'
-import { createFormatter } from '../datelib/formatting'
 import { computeFallbackHeaderFormat } from './table-utils'
 import { computeFallbackHeaderFormat } from './table-utils'
 import { VNode, h } from '../vdom'
 import { VNode, h } from '../vdom'
 import { TableDateCell, TableDowCell } from './TableDateCell'
 import { TableDateCell, TableDowCell } from './TableDateCell'
@@ -8,6 +7,7 @@ import { NowTimer } from '../NowTimer'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
 import { memoize } from '../util/memoize'
 import { memoize } from '../util/memoize'
 import { DateProfile } from '../DateProfileGenerator'
 import { DateProfile } from '../DateProfileGenerator'
+import { DateFormatter } from '../datelib/DateFormatter'
 
 
 
 
 export interface DayHeaderProps {
 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'
 import { applyAll } from '../util/misc'
 
 
 
 
-export class Emitter {
+export class Emitter<Options = {}> {
 
 
   private handlers: any = {}
   private handlers: any = {}
-  private options: any
+  private options: Options
   private thisContext: any = null
   private thisContext: any = null
 
 
 
 
@@ -13,7 +13,7 @@ export class Emitter {
   }
   }
 
 
 
 
-  setOptions(options) {
+  setOptions(options: Options) {
     this.options = options
     this.options = options
   }
   }
 
 

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

@@ -41,6 +41,7 @@ export class EventRoot extends BaseComponent<EventRootProps> {
 
 
   render() {
   render() {
     let { props, context } = this
     let { props, context } = this
+    let { options } = context
     let { seg } = props
     let { seg } = props
     let { eventRange } = seg
     let { eventRange } = seg
     let { ui } = eventRange
     let { ui } = eventRange
@@ -71,9 +72,12 @@ export class EventRoot extends BaseComponent<EventRootProps> {
 
 
     return (
     return (
       <RenderHook
       <RenderHook
-        name='event'
         hookProps={hookProps}
         hookProps={hookProps}
+        classNames={options.eventClassNames}
+        content={options.eventContent}
         defaultContent={props.defaultContent}
         defaultContent={props.defaultContent}
+        didMount={options.eventDidMount}
+        willUnmount={options.eventWillUnmount}
         elRef={this.elRef}
         elRef={this.elRef}
       >
       >
         {(rootElRef, customClassNames, innerElRef, innerContent) => props.children(
         {(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 { DateMarker } from '../datelib/marker'
 import { ViewContext, ViewContextType } from '../ViewContext'
 import { ViewContext, ViewContextType } from '../ViewContext'
 import { h } from '../vdom'
 import { h } from '../vdom'
+import { ViewApi } from '../ViewApi'
 
 
 
 
 export interface NowIndicatorRootProps {
 export interface NowIndicatorRootProps {
@@ -10,18 +11,31 @@ export interface NowIndicatorRootProps {
   children: RenderHookPropsChildren
   children: RenderHookPropsChildren
 }
 }
 
 
+export interface NowIndicatorHookProps {
+  isAxis: boolean
+  date: Date
+  view: ViewApi
+}
+
 
 
 export const NowIndicatorRoot = (props: NowIndicatorRootProps) => (
 export const NowIndicatorRoot = (props: NowIndicatorRootProps) => (
   <ViewContextType.Consumer>
   <ViewContextType.Consumer>
     {(context: ViewContext) => {
     {(context: ViewContext) => {
-      let hookProps = {
+      let { options } = context
+      let hookProps: NowIndicatorHookProps = {
         isAxis: props.isAxis,
         isAxis: props.isAxis,
         date: context.dateEnv.toDate(props.date),
         date: context.dateEnv.toDate(props.date),
         view: context.viewApi
         view: context.viewApi
       }
       }
 
 
       return (
       return (
-        <RenderHook name='nowIndicator' hookProps={hookProps}>
+        <RenderHook
+          hookProps={hookProps}
+          classNames={options.nowIndicatorClassNames}
+          content={options.nowIndicatorContent}
+          didMount={options.nowIndicatorDidMount}
+          willUnmount={options.nowIndicatorWillUnmount}
+        >
           {props.children}
           {props.children}
         </RenderHook>
         </RenderHook>
       )
       )

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

@@ -1,15 +1,15 @@
 
 
 import { ComponentChildren, h, Fragment } from '../vdom'
 import { ComponentChildren, h, Fragment } from '../vdom'
 import { BaseComponent } from '../vdom-util'
 import { BaseComponent } from '../vdom-util'
-import { createFormatter } from '../datelib/formatting'
 import { buildSegTimeText, EventMeta } from '../component/event-rendering'
 import { buildSegTimeText, EventMeta } from '../component/event-rendering'
 import { EventRoot, MinimalEventProps } from './EventRoot'
 import { EventRoot, MinimalEventProps } from './EventRoot'
 import { Seg } from '../component/DateComponent'
 import { Seg } from '../component/DateComponent'
+import { DateFormatter } from '../datelib/DateFormatter'
 
 
 
 
 export interface StandardEventProps extends MinimalEventProps {
 export interface StandardEventProps extends MinimalEventProps {
   extraClassNames: string[]
   extraClassNames: string[]
-  defaultTimeFormat: any // date-formatter INPUT
+  defaultTimeFormat: DateFormatter
   defaultDisplayEventTime?: boolean // default true
   defaultDisplayEventTime?: boolean // default true
   defaultDisplayEventEnd?: boolean // default true
   defaultDisplayEventEnd?: boolean // default true
   disableDragging?: boolean // default false
   disableDragging?: boolean // default false
@@ -24,15 +24,7 @@ export class StandardEvent extends BaseComponent<StandardEventProps> {
   render() {
   render() {
     let { props, context } = this
     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)
     let timeText = buildSegTimeText(props.seg, timeFormat, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd)
 
 
     return (
     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 { BaseComponent } from '../vdom-util'
 import { RenderHook } from './render-hook'
 import { RenderHook } from './render-hook'
 import { buildNavLinkData } from './nav-link'
 import { buildNavLinkData } from './nav-link'
-import { ViewApi } from '../ViewApi'
 import { DateProfile } from '../DateProfileGenerator'
 import { DateProfile } from '../DateProfileGenerator'
+import { DayHeaderHookProps } from '../options'
 
 
 
 
 export interface TableDateCellProps {
 export interface TableDateCellProps {
@@ -24,13 +24,6 @@ export interface TableDateCellProps {
   extraHookProps?: object
   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
 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)
       ? buildNavLinkData(date)
       : null
       : null
 
 
-    let hookProps: DateHeaderCellHookProps = {
+    let hookProps: DayHeaderHookProps = {
       date: dateEnv.toDate(date),
       date: dateEnv.toDate(date),
       view: viewApi,
       view: viewApi,
       ...props.extraHookProps,
       ...props.extraHookProps,
@@ -61,7 +54,14 @@ export class TableDateCell extends BaseComponent<TableDateCellProps> { // BAD na
     }
     }
 
 
     return (
     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) => (
         {(rootElRef, customClassNames, innerElRef, innerContent) => (
           <th
           <th
             ref={rootElRef}
             ref={rootElRef}
@@ -105,7 +105,7 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
 
 
   render() {
   render() {
     let { props } = this
     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
     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 text = dateEnv.format(date, props.dayHeaderFormat)
 
 
-    let hookProps: DateHeaderCellHookProps = {
+    let hookProps: DayHeaderHookProps = {
       date,
       date,
       ...dateMeta,
       ...dateMeta,
       view: viewApi,
       view: viewApi,
@@ -134,7 +134,14 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
     }
     }
 
 
     return (
     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) => (
         {(rootElRef, customClassNames, innerElRef, innerContent) => (
           <th
           <th
             ref={rootElRef}
             ref={rootElRef}
@@ -160,6 +167,6 @@ export class TableDowCell extends BaseComponent<TableDowCellProps> {
 }
 }
 
 
 
 
-function renderInner(hookProps: DateHeaderCellHookProps) {
+function renderInner(hookProps: DayHeaderHookProps) {
   return hookProps.text
   return hookProps.text
 }
 }

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

@@ -1,7 +1,8 @@
 import { ViewSpec } from '../structs/view-spec'
 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 { ComponentChildren, h, Ref } from '../vdom'
 import { BaseComponent } from '../vdom-util'
 import { BaseComponent } from '../vdom-util'
+import { ViewApi } from '../ViewApi'
 
 
 
 
 export interface ViewRootProps {
 export interface ViewRootProps {
@@ -10,19 +11,29 @@ export interface ViewRootProps {
   elRef?: Ref<any>
   elRef?: Ref<any>
 }
 }
 
 
+export interface ViewRootHookProps {
+  view: ViewApi
+}
+
 
 
 export class ViewRoot extends BaseComponent<ViewRootProps> {
 export class ViewRoot extends BaseComponent<ViewRootProps> {
 
 
-  buildClassNames = buildHookClassNameGenerator('view')
+  normalizeClassNames = buildClassNameNormalizer<ViewRootHookProps>()
 
 
 
 
   render() {
   render() {
     let { props, context } = this
     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 (
     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) => props.children(
           rootElRef,
           rootElRef,
           [ `fc-${props.viewSpec.type}-view`, 'fc-view' ].concat(customClassNames)
           [ `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 { ViewContext, ViewContextType } from '../ViewContext'
 import { DateMarker } from '../datelib/marker'
 import { DateMarker } from '../datelib/marker'
 import { RenderHook, RenderHookPropsChildren } from './render-hook'
 import { RenderHook, RenderHookPropsChildren } from './render-hook'
 import { h } from '../vdom'
 import { h } from '../vdom'
+import { DateFormatter } from '../datelib/DateFormatter'
 
 
 
 
 export interface WeekNumberRootProps {
 export interface WeekNumberRootProps {
   date: DateMarker
   date: DateMarker
-  defaultFormat: FormatterInput
+  defaultFormat: DateFormatter
   children: RenderHookPropsChildren
   children: RenderHookPropsChildren
 }
 }
 
 
+export interface WeekNumberHookProps {
+  num: number
+  text: string
+  date: Date
+}
+
 
 
 export const WeekNumberRoot = (props: WeekNumberRootProps) => (
 export const WeekNumberRoot = (props: WeekNumberRootProps) => (
   <ViewContextType.Consumer>
   <ViewContextType.Consumer>
     {(context: ViewContext) => {
     {(context: ViewContext) => {
+      let { dateEnv, options } = context
       let { date } = props
       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 (
       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}
           {props.children}
         </RenderHook>
         </RenderHook>
       )
       )

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

@@ -1,14 +1,15 @@
 import { Ref, createRef, ComponentChildren, h, RefObject, createContext } from '../vdom'
 import { Ref, createRef, ComponentChildren, h, RefObject, createContext } from '../vdom'
-import { ViewContext } from '../ViewContext'
 import { setRef, BaseComponent } from '../vdom-util'
 import { setRef, BaseComponent } from '../vdom-util'
 import { isPropsEqual } from '../util/object'
 import { isPropsEqual } from '../util/object'
 
 
 
 
 export interface RenderHookProps<HookProps> {
 export interface RenderHookProps<HookProps> {
-  name: string
   hookProps: HookProps
   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
   children: RenderHookPropsChildren
   elRef?: Ref<any>
   elRef?: Ref<any>
 }
 }
@@ -24,27 +25,24 @@ export interface ContentTypeHandlers {
   [contentKey: string]: () => (el: HTMLElement, contentVal: any) => void
   [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>> {
 export class RenderHook<HookProps> extends BaseComponent<RenderHookProps<HookProps>> {
 
 
   private rootElRef = createRef()
   private rootElRef = createRef()
 
 
 
 
   render() {
   render() {
-    let { name, hookProps, options, defaultContent, children } = this.props
+    let { props } = this
+    let { hookProps } = props
 
 
     return (
     return (
-      <MountHook name={name} hookProps={hookProps} options={options} elRef={this.handleRootEl}>
+      <MountHook hookProps={hookProps} didMount={props.didMount} willUnmount={props.willUnmount} elRef={this.handleRootEl}>
         {(rootElRef) => (
         {(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,
               rootElRef,
-              normalizeClassNames(
-                (options || this.context.options)[name ? name + 'ClassNames' : 'classNames'],
-                hookProps
-              ),
+              normalizeClassNames(props.classNames, hookProps),
               innerElRef,
               innerElRef,
               innerContent
               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
 // for forcing rerender of components that use the ContentHook
 export const CustomContentRenderContext = createContext<number>(0)
 export const CustomContentRenderContext = createContext<number>(0)
 
 
 export interface ContentHookProps<HookProps> {
 export interface ContentHookProps<HookProps> {
-  name: string
   hookProps: HookProps
   hookProps: HookProps
-  options?: object // will use instead of context. RENAME
-  backupElRef?: RefObject<any>
-  defaultContent?: (hookProps: HookProps) => ComponentChildren
+  content: CustomContentGenerator<HookProps>
+  defaultContent?: DefaultContentGenerator<HookProps>
   children: (
   children: (
     innerElRef: Ref<any>,
     innerElRef: Ref<any>,
     innerContent: ComponentChildren // if falsy, means it wasn't specified
     innerContent: ComponentChildren // if falsy, means it wasn't specified
   ) => ComponentChildren
   ) => 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 innerElRef = createRef()
   private customContentInfo: {
   private customContentInfo: {
@@ -115,7 +123,7 @@ export class ContentHook<HookProps> extends BaseComponent<ContentHookProps<HookP
   private renderInnerContent() {
   private renderInnerContent() {
     let { contentTypeHandlers } = this.context.pluginHooks
     let { contentTypeHandlers } = this.context.pluginHooks
     let { props, customContentInfo } = this
     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 innerContent = normalizeContent(rawVal, props.hookProps)
     let innerContentVDom: ComponentChildren = null
     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> {
 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
   hookProps: HookProps
-  options?: object // will use instead of context
+  didMount: DidMountHandler<HookProps>
+  willUnmount: WillUnmountHandler<HookProps>
   children: (rootElRef: Ref<any>) => ComponentChildren
   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>> {
 export class MountHook<HookProps> extends BaseComponent<MountHookProps<HookProps>> {
@@ -185,12 +199,12 @@ export class MountHook<HookProps> extends BaseComponent<MountHookProps<HookProps
 
 
 
 
   componentDidMount() {
   componentDidMount() {
-    this.triggerMountHandler('DidMount', 'didMount')
+    this.props.didMount({ ...this.props.hookProps, el: this.rootEl })
   }
   }
 
 
 
 
   componentWillUnmount() {
   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
     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') {
   if (typeof classNames === 'function') {
     classNames = classNames(hookProps)
     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
 // Computes a default column header formatting string if `colFormat` is not explicitly defined
 export function computeFallbackHeaderFormat(datesRepDistinctDays: boolean, dayCnt: number) {
 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,
   // 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
   // put just the day numbers will be in each cell
   if (!datesRepDistinctDays || dayCnt > 10) {
   if (!datesRepDistinctDays || dayCnt > 10) {
-    return { weekday: 'short' } // "Sat"
+    return createFormatter({ weekday: 'short' }) // "Sat"
   } else if (dayCnt > 1) {
   } 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 {
   } 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 { 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 { CalendarContext } from '../CalendarContext'
+import { identity } from '../options'
 
 
 // TODO: better called "EventSettings" or "EventConfig"
 // TODO: better called "EventSettings" or "EventConfig"
 // TODO: move this file into structs
 // TODO: move this file into structs
 // TODO: separate constraint/overlap/allow, because selection uses only that, not other props
 // TODO: separate constraint/overlap/allow, because selection uses only that, not other props
 
 
-export interface UnscopedEventUiInput {
+export interface RawEventUi {
+  display?: string
   editable?: boolean
   editable?: boolean
   startEditable?: boolean
   startEditable?: boolean
   durationEditable?: boolean
   durationEditable?: boolean
@@ -28,7 +30,7 @@ export interface EventUi {
   durationEditable: boolean | null
   durationEditable: boolean | null
   constraints: Constraint[]
   constraints: Constraint[]
   overlap: boolean | null
   overlap: boolean | null
-  allows: AllowFunc[]
+  allows: AllowFunc[] // crappy name to indicate plural
   backgroundColor: string
   backgroundColor: string
   borderColor: string
   borderColor: string
   textColor: string,
   textColor: string,
@@ -37,24 +39,51 @@ export interface EventUi {
 
 
 export type EventUiHash = { [defId: string]: 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,
   editable: Boolean,
   startEditable: Boolean,
   startEditable: Boolean,
   durationEditable: Boolean,
   durationEditable: Boolean,
-  constraint: null,
-  overlap: null,
-  allow: null,
-  className: parseClassName,
-  classNames: parseClassName,
+  constraint: identity,
+  overlap: identity,
+  allow: identity,
+  classNames: parseClassNames,
   color: String,
   color: String,
   backgroundColor: String,
   backgroundColor: String,
   borderColor: String,
   borderColor: String,
   textColor: 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)
   let constraint = normalizeConstraint(props.constraint, context)
 
 
   return {
   return {
@@ -67,53 +96,17 @@ export function processUnscopedUiProps(rawProps: UnscopedEventUiInput, context:
     backgroundColor: props.backgroundColor || props.color,
     backgroundColor: props.backgroundColor || props.color,
     borderColor: props.borderColor || props.color,
     borderColor: props.borderColor || props.color,
     textColor: props.textColor,
     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!
 // prevent against problems with <2 args!
 export function combineEventUis(uis: EventUi[]): EventUi {
 export function combineEventUis(uis: EventUi[]): EventUi {
   return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI)
   return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI)
 }
 }
 
 
+
 function combineTwoEventUis(item0: EventUi, item1: EventUi): EventUi { // hash1 has higher precedence
 function combineTwoEventUis(item0: EventUi, item1: EventUi): EventUi { // hash1 has higher precedence
   return {
   return {
     display: item1.display != null ? item1.display : item0.display,
     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
   computeWeekNumber: (d: DateMarker) => number
   weekText: string
   weekText: string
   cmdFormatter?: CmdFormatterFunc
   cmdFormatter?: CmdFormatterFunc
+  defaultSeparator: string
 }
 }
 
 
 export interface DateFormatter {
 export interface DateFormatter {
   format(date: ZonedMarker, context: DateFormattingContext): string
   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,
   firstDay?: any,
   weekText?: string,
   weekText?: string,
   cmdFormatter?: CmdFormatterFunc
   cmdFormatter?: CmdFormatterFunc
+  defaultSeparator?: string
 }
 }
 
 
 export type DateInput = Date | string | number | number[]
 export type DateInput = Date | string | number | number[]
@@ -46,6 +47,7 @@ export class DateEnv {
   weekNumberFunc: any
   weekNumberFunc: any
   weekText: string // DON'T LIKE how options are confused with local
   weekText: string // DON'T LIKE how options are confused with local
   cmdFormatter?: CmdFormatterFunc
   cmdFormatter?: CmdFormatterFunc
+  defaultSeparator: string
 
 
 
 
   constructor(settings: DateEnvSettings) {
   constructor(settings: DateEnvSettings) {
@@ -79,6 +81,7 @@ export class DateEnv {
     this.weekText = settings.weekText != null ? settings.weekText : settings.locale.options.weekText
     this.weekText = settings.weekText != null ? settings.weekText : settings.locale.options.weekText
 
 
     this.cmdFormatter = settings.cmdFormatter
     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) {
     if (dateOptions.isEndExclusive) {
       end = addMs(end, -1)
       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 partial1 = partialFormattingFunc(end)
 
 
     let insertion = findCommonInsertion(full0, partial0, full1, partial1)
     let insertion = findCommonInsertion(full0, partial0, full1, partial1)
-    let separator = extendedSettings.separator || ''
+    let separator = extendedSettings.separator || context.defaultSeparator || ''
 
 
     if (insertion) {
     if (insertion) {
       return insertion.before + partial0 + separator + partial1 + insertion.after
       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))
   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()
     parts.reverse()
   }
   }
 
 

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

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

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

@@ -1,7 +1,7 @@
 import { DateEnv, DateInput } from './datelib/env'
 import { DateEnv, DateInput } from './datelib/env'
 import { createFormatter } from './datelib/formatting'
 import { createFormatter } from './datelib/formatting'
 import { organizeRawLocales, buildLocale } from './datelib/locale'
 import { organizeRawLocales, buildLocale } from './datelib/locale'
-import { globalDefaults } from './options'
+import { RAW_BASE_DEFAULTS } from './options'
 
 
 export function formatDate(dateInput: DateInput, settings = {}) {
 export function formatDate(dateInput: DateInput, settings = {}) {
   let dateEnv = buildDateEnv(settings)
   let dateEnv = buildDateEnv(settings)
@@ -23,7 +23,7 @@ export function formatRange(
   settings // mixture of env and formatter settings
   settings // mixture of env and formatter settings
 ) {
 ) {
   let dateEnv = buildDateEnv(typeof settings === 'object' && settings ? settings : {}) // pass in if non-null object
   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 startMeta = dateEnv.createMarkerMeta(startInput)
   let endMeta = dateEnv.createMarkerMeta(endInput)
   let endMeta = dateEnv.createMarkerMeta(endInput)
 
 
@@ -44,7 +44,7 @@ function buildDateEnv(settings) {
 
 
   // ensure required settings
   // ensure required settings
   settings = {
   settings = {
-    timeZone: globalDefaults.timeZone,
+    timeZone: RAW_BASE_DEFAULTS.timeZone,
     calendarSystem: 'gregory',
     calendarSystem: 'gregory',
     ...settings,
     ...settings,
     locale
     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.
 this array is exposed on the root namespace so that UMD plugins can add to it.
 see the rollup-bundles script.
 see the rollup-bundles script.
 */
 */
-export let globalPlugins: PluginDef[] = [
+export let globalPlugins: PluginDef[] = [ // TODO: make a const?
   arrayEventSourcePlugin,
   arrayEventSourcePlugin,
   funcEventSourcePlugin,
   funcEventSourcePlugin,
   jsonFeedEventSourcePlugin,
   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
 export const version: string = '<%= version %>' // important to type it, so .d.ts has generic string
 
 
 // types
 // types
-export { OptionsInput } from './types/input-types'
 export { EventDef, EventDefHash } from './structs/event-def'
 export { EventDef, EventDefHash } from './structs/event-def'
 export { EventInstance, EventInstanceHash, createEventInstance } from './structs/event-instance'
 export { EventInstance, EventInstanceHash, createEventInstance } from './structs/event-instance'
 export { EventInput, parseEventDef, EventTuple } from './structs/event-parse'
 export { EventInput, parseEventDef, EventTuple } from './structs/event-parse'
@@ -16,7 +15,6 @@ export {
   applyAll,
   applyAll,
   padStart,
   padStart,
   isInt,
   isInt,
-  capitaliseFirstLetter,
   parseFieldSpecs,
   parseFieldSpecs,
   compareByFieldSpecs,
   compareByFieldSpecs,
   compareByFieldSpec,
   compareByFieldSpec,
@@ -40,7 +38,7 @@ export {
   isArraysEqual
   isArraysEqual
 } from './util/array'
 } from './util/array'
 
 
-export { memoize, memoizeArraylike, memoizeHashlike } from './util/memoize'
+export { memoize, memoizeObjArg, memoizeArraylike, memoizeHashlike } from './util/memoize'
 
 
 export {
 export {
   intersectRects,
   intersectRects,
@@ -64,7 +62,7 @@ export {
 } from './util/dom-manip'
 } from './util/dom-manip'
 
 
 export { EventStore, filterEventStoreDefs, createEmptyEventStore, mergeEventStores, getRelevantEvents, eventTupleToStore } from './structs/event-store'
 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 { Splitter, SplittableProps } from './component/event-splitting'
 export { getDayClassNames, getDateMeta, DateMeta, getSlotClassNames } from './component/date-rendering'
 export { getDayClassNames, getDateMeta, DateMeta, getSlotClassNames } from './component/date-rendering'
 export { buildNavLinkData } from './common/nav-link'
 export { buildNavLinkData } from './common/nav-link'
@@ -115,6 +113,7 @@ export { DateEnv, DateMarkerMeta } from './datelib/env'
 
 
 export {
 export {
   createFormatter,
   createFormatter,
+  FormatterInput
 } from './datelib/formatting'
 } from './datelib/formatting'
 export {
 export {
   DateFormatter,
   DateFormatter,
@@ -140,7 +139,14 @@ export { ElementDragging } from './interactions/ElementDragging'
 
 
 export { formatDate, formatRange } from './formatting-api'
 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'
 export { RecurringType, ParsedRecurring } from './structs/recurring-event'
 
 
@@ -154,7 +160,7 @@ export { CalendarContentProps, CalendarContent, computeCalendarClassNames, compu
 
 
 export { DayHeader } from './common/DayHeader'
 export { DayHeader } from './common/DayHeader'
 export { computeFallbackHeaderFormat } from './common/table-utils'
 export { computeFallbackHeaderFormat } from './common/table-utils'
-export { TableDateCell, TableDowCell, DateHeaderCellHookProps } from './common/TableDateCell'
+export { TableDateCell, TableDowCell } from './common/TableDateCell'
 
 
 export { DaySeriesModel } from './common/DaySeriesModel'
 export { DaySeriesModel } from './common/DaySeriesModel'
 
 
@@ -203,7 +209,10 @@ export { getIsRtlScrollbarOnLeft } from './util/scrollbar-side'
 export { NowTimer } from './NowTimer'
 export { NowTimer } from './NowTimer'
 export { ScrollResponder, ScrollRequest } from './ScrollResponder'
 export { ScrollResponder, ScrollRequest } from './ScrollResponder'
 export { globalPlugins } from './global-plugins'
 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 { StandardEvent, StandardEventProps } from './common/StandardEvent'
 export { NowIndicatorRoot, NowIndicatorRootProps } from './common/NowIndicatorRoot'
 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 { 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: ' - ',
   defaultRangeSeparator: ' - ',
   titleRangeSeparator: ' \u2013 ', // en dash
   titleRangeSeparator: ' \u2013 ', // en dash
-
   defaultTimedEventDuration: '01:00:00',
   defaultTimedEventDuration: '01:00:00',
   defaultAllDayEventDuration: { day: 1 },
   defaultAllDayEventDuration: { day: 1 },
   forceEventDuration: false,
   forceEventDuration: false,
   nextDayThreshold: '00:00:00',
   nextDayThreshold: '00:00:00',
-
-  // display
   dayHeaders: true,
   dayHeaders: true,
   initialView: '',
   initialView: '',
   aspectRatio: 1.35,
   aspectRatio: 1.35,
@@ -23,68 +224,127 @@ export const globalDefaults = {
   },
   },
   weekends: true,
   weekends: true,
   weekNumbers: false,
   weekNumbers: false,
-  weekNumberCalculation: 'local',
-
+  weekNumberCalculation: 'local' as WeekNumberCalculation,
   editable: false,
   editable: false,
-
-  // nowIndicator: false,
-
+  nowIndicator: false,
   scrollTime: '06:00:00',
   scrollTime: '06:00:00',
   slotMinTime: '00:00:00',
   slotMinTime: '00:00:00',
   slotMaxTime: '24:00:00',
   slotMaxTime: '24:00:00',
   showNonCurrentDates: true,
   showNonCurrentDates: true,
-
-  // event ajax
   lazyFetching: true,
   lazyFetching: true,
   startParam: 'start',
   startParam: 'start',
   endParam: 'end',
   endParam: 'end',
   timeZoneParam: 'timeZone',
   timeZoneParam: 'timeZone',
-
   timeZone: 'local', // TODO: throw error if given falsy value?
   timeZone: 'local', // TODO: throw error if given falsy value?
-
-  // defaultAllDay: undefined,
-
-  // locale
   locales: [],
   locales: [],
   locale: '', // blank values means it will compute based off locales[]
   locale: '', // blank values means it will compute based off locales[]
-  // direction: will get this from the default locale
-  // buttonIcons: null,
-
   themeSystem: 'standard',
   themeSystem: 'standard',
-
-  // eventResizableFromStart: false,
   dragRevertDuration: 500,
   dragRevertDuration: 500,
   dragScroll: true,
   dragScroll: true,
-
   allDayMaintainDuration: false,
   allDayMaintainDuration: false,
-
-  // selectable: false,
   unselectAuto: true,
   unselectAuto: true,
-  // selectMinDistance: 0,
-
   dropAccept: '*',
   dropAccept: '*',
-
   eventOrder: 'start,-duration,allDay,title',
   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' },
   dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' },
-
   handleWindowResize: true,
   handleWindowResize: true,
   windowResizeDelay: 100, // milliseconds before an updateSize happens
   windowResizeDelay: 100, // milliseconds before an updateSize happens
-
   longPressDelay: 1000,
   longPressDelay: 1000,
   eventDragMinDistance: 5, // only applies to mouse
   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',
   'headerToolbar',
   'footerToolbar',
   'footerToolbar',
   'buttonText',
   '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 { ComponentChildren } from './vdom'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
 import { ContentTypeHandlers } from './common/render-hook'
 import { ContentTypeHandlers } from './common/render-hook'
+import { GenericRefiners } from './options'
 
 
 // TODO: easier way to add new hooks? need to update a million things
 // TODO: easier way to add new hooks? need to update a million things
 
 
@@ -54,6 +55,7 @@ export interface PluginDefInput {
   optionChangeHandlers?: OptionChangeHandlerMap
   optionChangeHandlers?: OptionChangeHandlerMap
   scrollGridImpl?: ScrollGridImpl
   scrollGridImpl?: ScrollGridImpl
   contentTypeHandlers?: ContentTypeHandlers
   contentTypeHandlers?: ContentTypeHandlers
+  optionRefiners?: GenericRefiners
 }
 }
 
 
 export interface PluginHooks {
 export interface PluginHooks {
@@ -84,6 +86,7 @@ export interface PluginHooks {
   optionChangeHandlers: OptionChangeHandlerMap
   optionChangeHandlers: OptionChangeHandlerMap
   scrollGridImpl: ScrollGridImpl | null
   scrollGridImpl: ScrollGridImpl | null
   contentTypeHandlers: ContentTypeHandlers
   contentTypeHandlers: ContentTypeHandlers
+  optionRefiners: GenericRefiners
 }
 }
 
 
 export interface PluginDef extends PluginHooks {
 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,
     elementDraggingImpl: input.elementDraggingImpl,
     optionChangeHandlers: input.optionChangeHandlers || {},
     optionChangeHandlers: input.optionChangeHandlers || {},
     scrollGridImpl: input.scrollGridImpl || null,
     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,
     elementDraggingImpl: null,
     optionChangeHandlers: {},
     optionChangeHandlers: {},
     scrollGridImpl: null,
     scrollGridImpl: null,
-    contentTypeHandlers: {}
+    contentTypeHandlers: {},
+    optionRefiners: {}
   }
   }
 
 
   function addDefs(defs: PluginDef[]) {
   function addDefs(defs: PluginDef[]) {
@@ -120,6 +122,7 @@ function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
     elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
     elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
     optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers },
     optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers },
     scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl,
     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'
 import { DateMarker } from '../datelib/marker'
 
 
 export type Action =
 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: 'PREV' } |
   { type: 'NEXT' } |
   { 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 { memoize, memoizeObjArg } from '../util/memoize'
 import { Action } from './Action'
 import { Action } from './Action'
 import { buildPluginHooks } from '../plugin-system'
 import { buildPluginHooks } from '../plugin-system'
@@ -7,7 +7,7 @@ import { DateEnv } from '../datelib/env'
 import { CalendarApi } from '../CalendarApi'
 import { CalendarApi } from '../CalendarApi'
 import { StandardTheme } from '../theme/StandardTheme'
 import { StandardTheme } from '../theme/StandardTheme'
 import { EventSourceHash } from '../structs/event-source'
 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 { mapHash, isPropsEqual } from '../util/object'
 import { DateProfileGenerator, DateProfileGeneratorProps } from '../DateProfileGenerator'
 import { DateProfileGenerator, DateProfileGeneratorProps } from '../DateProfileGenerator'
 import { reduceViewType } from './view-type'
 import { reduceViewType } from './view-type'
@@ -21,18 +21,16 @@ import { reduceSelectedEvent } from './selected-event'
 import { reduceEventDrag } from './event-drag'
 import { reduceEventDrag } from './event-drag'
 import { reduceEventResize } from './event-resize'
 import { reduceEventResize } from './event-resize'
 import { Emitter } from '../common/Emitter'
 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 { EventDefHash } from '../structs/event-def'
 import { parseToolbars } from '../toolbar-parse'
 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 { rangeContainsMarker } from '../datelib/date-range'
 import { ViewApi } from '../ViewApi'
 import { ViewApi } from '../ViewApi'
 import { parseBusinessHours } from '../structs/business-hours'
 import { parseBusinessHours } from '../structs/business-hours'
 import { globalPlugins } from '../global-plugins'
 import { globalPlugins } from '../global-plugins'
 import { createEmptyEventStore } from '../structs/event-store'
 import { createEmptyEventStore } from '../structs/event-store'
 import { CalendarContext } from '../CalendarContext'
 import { CalendarContext } from '../CalendarContext'
-import { buildComputedOptions } from '../ComputedOptions'
 import { CalendarDataManagerState, CalendarOptionsData, CalendarCurrentViewData, CalendarData } from './data-types'
 import { CalendarDataManagerState, CalendarOptionsData, CalendarCurrentViewData, CalendarData } from './data-types'
 import { __assign } from 'tslib'
 import { __assign } from 'tslib'
 import { TaskRunner } from '../util/runner'
 import { TaskRunner } from '../util/runner'
@@ -40,7 +38,7 @@ import { buildTitle } from './title-formatting'
 
 
 
 
 export interface CalendarDataManagerProps {
 export interface CalendarDataManagerProps {
-  optionOverrides: any
+  optionOverrides: RawCalendarOptions
   calendarApi: CalendarApi
   calendarApi: CalendarApi
   onAction?: (action: Action) => void
   onAction?: (action: Action) => void
   onData?: (data: CalendarData) => void
   onData?: (data: CalendarData) => void
@@ -62,16 +60,12 @@ export class CalendarDataManager {
   private computeOptionsData = memoize(this._computeOptionsData)
   private computeOptionsData = memoize(this._computeOptionsData)
   private computeCurrentViewData = memoize(this._computeCurrentViewData)
   private computeCurrentViewData = memoize(this._computeCurrentViewData)
   private organizeRawLocales = memoize(organizeRawLocales)
   private organizeRawLocales = memoize(organizeRawLocales)
-  private buildCalendarOptions = memoize(mergeOptionSets)
-  private buildComputedCalendarOptions = memoize(buildComputedOptions)
   private buildLocale = memoize(buildLocale)
   private buildLocale = memoize(buildLocale)
   private buildPluginHooks = memoize(buildPluginHooks)
   private buildPluginHooks = memoize(buildPluginHooks)
   private buildDateEnv = memoize(buildDateEnv)
   private buildDateEnv = memoize(buildDateEnv)
   private buildTheme = memoize(buildTheme)
   private buildTheme = memoize(buildTheme)
   private parseToolbars = memoize(parseToolbars)
   private parseToolbars = memoize(parseToolbars)
   private buildViewSpecs = memoize(buildViewSpecs)
   private buildViewSpecs = memoize(buildViewSpecs)
-  private buildViewOptions = memoize(mergeOptionSets)
-  private buildComputedViewOptions = memoize(buildComputedOptions)
   private buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator)
   private buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator)
   private buildViewApi = memoize(buildViewApi)
   private buildViewApi = memoize(buildViewApi)
   private buildViewUiProps = memoizeObjArg(buildViewUiProps)
   private buildViewUiProps = memoizeObjArg(buildViewUiProps)
@@ -80,18 +74,23 @@ export class CalendarDataManager {
   private parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours)
   private parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours)
   private buildTitle = memoize(buildTitle)
   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 actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this))
   private props: CalendarDataManagerProps
   private props: CalendarDataManagerProps
   private state: CalendarDataManagerState
   private state: CalendarDataManagerState
   private data: CalendarData
   private data: CalendarData
 
 
+  public currentRawCalendarOptions: RawCalendarOptions = {}
+  private currentRefinedCalendarOptions: RefinedCalendarOptions = ({} as any)
+  private currentRawViewOptions: RawViewOptions = {}
+  private currentRefinedViewOptions: RefinedViewOptions = ({} as any)
+
 
 
   constructor(props: CalendarDataManagerProps) {
   constructor(props: CalendarDataManagerProps) {
     this.props = props
     this.props = props
     this.actionRunner.pause()
     this.actionRunner.pause()
 
 
-    let dynamicOptionOverrides = {}
+    let dynamicOptionOverrides: RawCalendarOptions = {}
     let optionsData = this.computeOptionsData(
     let optionsData = this.computeOptionsData(
       props.optionOverrides,
       props.optionOverrides,
       dynamicOptionOverrides,
       dynamicOptionOverrides,
@@ -100,8 +99,8 @@ export class CalendarDataManager {
 
 
     let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView
     let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView
     let currentViewData = this.computeCurrentViewData(
     let currentViewData = this.computeCurrentViewData(
-      optionsData,
       currentViewType,
       currentViewType,
+      optionsData,
       props.optionOverrides,
       props.optionOverrides,
       dynamicOptionOverrides
       dynamicOptionOverrides
     )
     )
@@ -122,7 +121,6 @@ export class CalendarDataManager {
     let calendarContext: CalendarContext = {
     let calendarContext: CalendarContext = {
       dateEnv: optionsData.dateEnv,
       dateEnv: optionsData.dateEnv,
       options: optionsData.calendarOptions,
       options: optionsData.calendarOptions,
-      computedOptions: optionsData.computedCalendarOptions,
       pluginHooks: optionsData.pluginHooks,
       pluginHooks: optionsData.pluginHooks,
       calendarApi: props.calendarApi,
       calendarApi: props.calendarApi,
       dispatch: this.dispatch,
       dispatch: this.dispatch,
@@ -176,7 +174,7 @@ export class CalendarDataManager {
   }
   }
 
 
 
 
-  resetOptions(optionOverrides, append?: boolean) {
+  resetOptions(optionOverrides: RawCalendarOptions, append?: boolean) {
     let { props } = this
     let { props } = this
 
 
     props.optionOverrides = append
     props.optionOverrides = append
@@ -201,8 +199,8 @@ export class CalendarDataManager {
 
 
     let currentViewType = reduceViewType(state.currentViewType, action)
     let currentViewType = reduceViewType(state.currentViewType, action)
     let currentViewData = this.computeCurrentViewData(
     let currentViewData = this.computeCurrentViewData(
-      optionsData,
       currentViewType,
       currentViewType,
+      optionsData,
       props.optionOverrides,
       props.optionOverrides,
       dynamicOptionOverrides
       dynamicOptionOverrides
     )
     )
@@ -216,7 +214,6 @@ export class CalendarDataManager {
     let calendarContext: CalendarContext = {
     let calendarContext: CalendarContext = {
       dateEnv: optionsData.dateEnv,
       dateEnv: optionsData.dateEnv,
       options: optionsData.calendarOptions,
       options: optionsData.calendarOptions,
-      computedOptions: optionsData.computedCalendarOptions,
       pluginHooks: optionsData.pluginHooks,
       pluginHooks: optionsData.pluginHooks,
       calendarApi: props.calendarApi,
       calendarApi: props.calendarApi,
       dispatch: this.dispatch,
       dispatch: this.dispatch,
@@ -302,8 +299,8 @@ export class CalendarDataManager {
     )
     )
 
 
     let currentViewData = this.computeCurrentViewData(
     let currentViewData = this.computeCurrentViewData(
-      optionsData,
       state.currentViewType,
       state.currentViewType,
+      optionsData,
       props.optionOverrides,
       props.optionOverrides,
       state.dynamicOptionOverrides
       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
     // 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(
     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,
       pluginHooks,
-      availableLocaleData
+      availableLocaleData,
+      refinedOptions.defaultRangeSeparator
     )
     )
 
 
     let viewSpecs = this.buildViewSpecs(pluginHooks.views, optionOverrides, dynamicOptionOverrides, localeDefaults)
     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 {
     return {
-      calendarOptions,
-      computedCalendarOptions,
-      availableRawLocales,
+      calendarOptions: refinedOptions,
       pluginHooks,
       pluginHooks,
       dateEnv,
       dateEnv,
       viewSpecs,
       viewSpecs,
       theme,
       theme,
       toolbarConfig,
       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) {
     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,
       optionsData.localeDefaults,
       optionOverrides,
       optionOverrides,
-      viewSpec.optionOverrides,
       dynamicOptionOverrides
       dynamicOptionOverrides
     )
     )
 
 
-    let computedOptions = this.buildComputedViewOptions(options)
+    warnUnknownOptions(extra)
 
 
     let dateProfileGenerator = this.buildDateProfileGenerator({
     let dateProfileGenerator = this.buildDateProfileGenerator({
       viewSpec,
       viewSpec,
       dateEnv: optionsData.dateEnv,
       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(
 function buildDateEnv(
   timeZone: string,
   timeZone: string,
-  explicitLocale: string,
+  explicitLocale: LocaleSingularArg,
   weekNumberCalculation,
   weekNumberCalculation,
   firstDay,
   firstDay,
   weekText,
   weekText,
   pluginHooks: PluginHooks,
   pluginHooks: PluginHooks,
-  availableLocaleData: RawLocaleInfo
+  availableLocaleData: RawLocaleInfo,
+  defaultSeparator: string
 ) {
 ) {
   let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map)
   let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map)
 
 
@@ -465,15 +548,16 @@ function buildDateEnv(
     weekNumberCalculation,
     weekNumberCalculation,
     firstDay,
     firstDay,
     weekText,
     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) {
 function buildViewUiProps(calendarContext: CalendarContext) {
+  let { options } = calendarContext
+
   return {
   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) {
 function parseContextBusinessHours(calendarContext: CalendarContext) {
   return parseBusinessHours(calendarContext.options.businessHours, 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 { DateMarker } from '../datelib/marker'
 import { Action } from './Action'
 import { Action } from './Action'
+import { RefinedBaseOptions } from '../options'
 
 
 
 
 export function reduceCurrentDate(currentDate: DateMarker, action: Action) {
 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
   let initialDateInput = options.initialDate
 
 
   // compute the initial ambig-timezone date
   // compute the initial ambig-timezone date
   if (initialDateInput != null) {
   if (initialDateInput != null) {
     return dateEnv.createMarker(initialDateInput)
     return dateEnv.createMarker(initialDateInput)
   } else {
   } 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.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 { EventStore } from '../structs/event-store'
 import { DateSpan } from '../structs/date-span'
 import { DateSpan } from '../structs/date-span'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { EventInteractionState } from '../interactions/event-interaction-state'
-import { ComputedOptions } from '../ComputedOptions'
+import { RefinedCalendarOptions, RefinedViewOptions, GenericRefiners, RawCalendarOptions } from '../options'
 
 
 
 
 export interface CalendarDataManagerState {
 export interface CalendarDataManagerState {
-  dynamicOptionOverrides: object // raw
+  dynamicOptionOverrides: RawCalendarOptions
   currentViewType: string
   currentViewType: string
   currentDate: DateMarker
   currentDate: DateMarker
   dateProfile: DateProfile
   dateProfile: DateProfile
@@ -35,11 +35,11 @@ export interface CalendarDataManagerState {
 }
 }
 
 
 export interface CalendarOptionsData {
 export interface CalendarOptionsData {
-  localeDefaults: any
-  calendarOptions: any
-  computedCalendarOptions: ComputedOptions
+  localeDefaults: RawCalendarOptions
+  calendarOptions: RefinedCalendarOptions
   toolbarConfig: any
   toolbarConfig: any
   availableRawLocales: any
   availableRawLocales: any
+  refiners: GenericRefiners
   dateEnv: DateEnv
   dateEnv: DateEnv
   theme: Theme
   theme: Theme
   pluginHooks: PluginHooks
   pluginHooks: PluginHooks
@@ -48,8 +48,7 @@ export interface CalendarOptionsData {
 
 
 export interface CalendarCurrentViewData {
 export interface CalendarCurrentViewData {
   viewSpec: ViewSpec
   viewSpec: ViewSpec
-  options: any // is VIEW-SPECIFIC
-  computedOptions: ComputedOptions // is VIEW-SPECIFIC
+  options: RefinedViewOptions
   viewApi: ViewApi
   viewApi: ViewApi
   dateProfileGenerator: DateProfileGenerator
   dateProfileGenerator: DateProfileGenerator
 }
 }

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

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

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

@@ -1,12 +1,13 @@
 import { DateProfile } from '../DateProfileGenerator'
 import { DateProfile } from '../DateProfileGenerator'
 import { diffWholeDays } from '../datelib/marker'
 import { diffWholeDays } from '../datelib/marker'
-import { createFormatter } from '../datelib/formatting'
+import { createFormatter, FormatterInput } from '../datelib/formatting'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
 import { DateEnv } from '../datelib/env'
 import { DateEnv } from '../datelib/env'
+import { RefinedCalendarOptions } from '../options'
 
 
 
 
 // Computes what the title at the top of the calendarApi should be for this view
 // 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
   let range: DateRange
 
 
   // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
   // 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.
 // 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`.
 // 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
   let currentRangeUnit = dateProfile.currentRangeUnit
 
 
   if (currentRangeUnit === 'year') {
   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 { computeSmallestCellWidth } from '../util/misc'
 import { isPropsEqual } from '../util/object'
 import { isPropsEqual } from '../util/object'
 import { isArraysEqual } from '../util/array'
 import { isArraysEqual } from '../util/array'
+import { RefinedCalendarOptions } from '../options'
 
 
 
 
 export type CssDimValue = string | number // TODO: move to more general file
 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
   let { stickyHeaderDates } = options
 
 
   if (stickyHeaderDates == null || stickyHeaderDates === 'auto') {
   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
   let { stickyFooterScrollbar } = options
 
 
   if (stickyFooterScrollbar == null || stickyFooterScrollbar === 'auto') {
   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 { DateInput } from '../datelib/env'
 import { startOfDay } from '../datelib/marker'
 import { startOfDay } from '../datelib/marker'
 import { parseRecurring } from './recurring-event'
 import { parseRecurring } from './recurring-event'
-import { UnscopedEventUiInput, processUnscopedUiProps } from '../component/event-ui'
+import { RawEventUi, processUiProps } from '../component/event-ui'
 import { __assign } from 'tslib'
 import { __assign } from 'tslib'
 import { CalendarContext } from '../CalendarContext'
 import { CalendarContext } from '../CalendarContext'
 import { EventDef, DATE_PROPS, NON_DATE_PROPS } from './event-def'
 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.
 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
   id?: string | number
   groupId?: string | number
   groupId?: string | number
   title?: string
   title?: string
@@ -164,8 +164,8 @@ function parseSingle(raw: EventInput, defaultAllDay: boolean | null, context: Ca
     endMarker = context.dateEnv.add(
     endMarker = context.dateEnv.add(
       startMarker,
       startMarker,
       allDay ?
       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?) {
 function pluckNonDateProps(raw: EventInput, context: CalendarContext, leftovers?) {
   let preLeftovers = {}
   let preLeftovers = {}
   let props = refineProps(raw, NON_DATE_PROPS, {}, 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
   props.publicId = props.id
   delete 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 { EventSource, EventSourceSuccessResponseHandler, EventSourceErrorResponseHandler } from './event-source'
 import { CalendarContext } from '../CalendarContext'
 import { CalendarContext } from '../CalendarContext'
 import { refineProps, guid } from '../util/misc'
 import { refineProps, guid } from '../util/misc'
-import { processUnscopedUiProps } from '../component/event-ui'
+import { processUiProps } from '../component/event-ui'
 
 
 
 
 export interface ExtendedEventSourceInput {
 export interface ExtendedEventSourceInput {
@@ -87,7 +87,7 @@ function parseEventSourceProps(raw: ExtendedEventSourceInput, meta: object, sour
   let leftovers0 = {}
   let leftovers0 = {}
   let props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0)
   let props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0)
   let leftovers1 = {}
   let leftovers1 = {}
-  let ui = processUnscopedUiProps(leftovers0, context, leftovers1)
+  let ui = processUiProps(leftovers0, context, leftovers1)
 
 
   props.isFetching = false
   props.isFetching = false
   props.latestFetchId = ''
   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 {
 export function expandRecurring(eventStore: EventStore, framingRange: DateRange, context: CalendarContext): EventStore {
-  let { dateEnv, pluginHooks, computedOptions } = context
+  let { dateEnv, pluginHooks, options } = context
   let { defs, instances } = eventStore
   let { defs, instances } = eventStore
 
 
   // remove existing recurring instances
   // remove existing recurring instances
@@ -83,8 +83,8 @@ export function expandRecurring(eventStore: EventStore, framingRange: DateRange,
 
 
       if (!duration) {
       if (!duration) {
         duration = def.allDay ?
         duration = def.allDay ?
-          computedOptions.defaultAllDayEventDuration :
-          computedOptions.defaultTimedEventDuration
+          options.defaultAllDayEventDuration :
+          options.defaultTimedEventDuration
       }
       }
 
 
       let starts = expandRecurringRanges(def, duration, framingRange, dateEnv, pluginHooks.recurringTypes)
       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 { ViewProps } from '../View'
-import { refineProps } from '../util/misc'
 import { mapHash } from '../util/object'
 import { mapHash } from '../util/object'
 import { ComponentType, Component, h } from '../vdom'
 import { ComponentType, Component, h } from '../vdom'
 import { ViewRoot } from '../common/ViewRoot'
 import { ViewRoot } from '../common/ViewRoot'
 import { RenderHook } from '../common/render-hook'
 import { RenderHook } from '../common/render-hook'
 import { ViewContext, ViewContextType } from '../ViewContext'
 import { ViewContext, ViewContextType } from '../ViewContext'
+import { RawViewOptions } from '../options'
+import { Duration } from '../datelib/duration'
 
 
 /*
 /*
 A view-config represents information for either:
 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 ViewComponent = Component<ViewProps> // an instance
 export type ViewComponentType = ComponentType<ViewProps>
 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 type ViewConfigInputHash = { [viewType: string]: ViewConfigInput }
 
 
 export interface ViewConfig {
 export interface ViewConfig {
   superType: string
   superType: string
   component: ViewComponentType | null
   component: ViewComponentType | null
-  options: any
+  rawOptions: RawViewOptions
 }
 }
 
 
 export type ViewConfigHash = { [viewType: string]: ViewConfig }
 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 {
 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?
     // TODO: remove content/classNames/didMount/etc from options?
   }
   }
 
 
   return {
   return {
-    superType: props.type,
+    superType: rawOptions.type,
     component,
     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 function(viewProps: ViewProps) {
     return (
     return (
       <ViewContextType.Consumer>
       <ViewContextType.Consumer>
         {(context: ViewContext) => (
         {(context: ViewContext) => (
           <ViewRoot viewSpec={context.viewSpec}>
           <ViewRoot viewSpec={context.viewSpec}>
             {(rootElRef, viewClassNames) => {
             {(rootElRef, viewClassNames) => {
-              let hookProps = { ...viewProps, nextDayThreshold: context.computedOptions.nextDayThreshold }
+              let hookProps: ViewHookProps = {
+                ...viewProps,
+                nextDayThreshold: context.options.nextDayThreshold
+              }
+
               return (
               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) => (
                   {(rootElRef, customClassNames, innerElRef, innerContent) => (
                     <div className={viewClassNames.concat(customClassNames).join(' ')} ref={rootElRef}>
                     <div className={viewClassNames.concat(customClassNames).join(' ')} ref={rootElRef}>
                       {innerContent}
                       {innerContent}

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

@@ -1,4 +1,5 @@
 import { ViewConfigHash, ViewComponentType } from './view-config'
 import { ViewConfigHash, ViewComponentType } from './view-config'
+import { RawViewOptions } from '../options'
 
 
 /*
 /*
 Represents information for an instantiatable View class along with settings
 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 {
 export interface ViewDef {
   type: string
   type: string
   component: ViewComponentType
   component: ViewComponentType
-  overrides: any
-  defaults: any
+  overrides: RawViewOptions
+  defaults: RawViewOptions
 }
 }
 
 
 export type ViewDefHash = { [viewType: string]: ViewDef }
 export type ViewDefHash = { [viewType: string]: ViewDef }
@@ -77,11 +78,11 @@ function buildViewDef(viewType: string, hash: ViewDefHash, defaultConfigs: ViewC
     component: theComponent,
     component: theComponent,
     defaults: {
     defaults: {
       ...(superDef ? superDef.defaults : {}),
       ...(superDef ? superDef.defaults : {}),
-      ...(defaultConfig ? defaultConfig.options : {})
+      ...(defaultConfig ? defaultConfig.rawOptions : {})
     },
     },
     overrides: {
     overrides: {
       ...(superDef ? superDef.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 { ViewDef, compileViewDefs } from './view-def'
 import { Duration, createDuration, greatestDurationDenominator, getWeeksFromInput } from '../datelib/duration'
 import { Duration, createDuration, greatestDurationDenominator, getWeeksFromInput } from '../datelib/duration'
 import { mapHash } from '../util/object'
 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'
 import { ViewConfigInputHash, parseViewConfigs, ViewConfigHash, ViewComponentType } from './view-config'
 
 
 /*
 /*
@@ -18,8 +18,8 @@ export interface ViewSpec {
   duration: Duration
   duration: Duration
   durationUnit: string
   durationUnit: string
   singleUnit: string
   singleUnit: string
-  optionDefaults: any
-  optionOverrides: any
+  optionDefaults: RawViewOptions
+  optionOverrides: RawViewOptions
   buttonTextOverride: string
   buttonTextOverride: string
   buttonTextDefault: string
   buttonTextDefault: string
 }
 }
@@ -27,7 +27,7 @@ export interface ViewSpec {
 export type ViewSpecHash = { [viewType: string]: 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 defaultConfigs = parseViewConfigs(defaultInputs)
   let overrideConfigs = parseViewConfigs(optionOverrides.views)
   let overrideConfigs = parseViewConfigs(optionOverrides.views)
   let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs)
   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 =
   let durationInput =
     viewDef.overrides.duration ||
     viewDef.overrides.duration ||
     viewDef.defaults.duration ||
     viewDef.defaults.duration ||
@@ -48,7 +48,7 @@ function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, option
   let duration = null
   let duration = null
   let durationUnit = ''
   let durationUnit = ''
   let singleUnit = ''
   let singleUnit = ''
-  let singleUnitOverrides = {}
+  let singleUnitOverrides: RawViewOptions = {}
 
 
   if (durationInput) {
   if (durationInput) {
     duration = createDuration(durationInput)
     duration = createDuration(durationInput)
@@ -63,7 +63,7 @@ function buildViewSpec(viewDef: ViewDef, overrideConfigs: ViewConfigHash, option
 
 
       if (denom.value === 1) {
       if (denom.value === 1) {
         singleUnit = durationUnit
         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:
     buttonTextDefault:
       queryButtonText(localeDefaults) ||
       queryButtonText(localeDefaults) ||
       viewDef.defaults.buttonText ||
       viewDef.defaults.buttonText ||
-      queryButtonText(globalDefaults) ||
+      queryButtonText(RAW_BASE_DEFAULTS) ||
       viewDef.type // fall back to given view name
       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'
   nextYear: 'fc-icon-chevrons-left'
 }
 }
 
 
-StandardTheme.prototype.iconOverrideOption = 'buttonIcons'
+StandardTheme.prototype.iconOverrideOption = 'buttonIcons' // TODO: make TS-friendly
 StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'
 StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'
 StandardTheme.prototype.iconOverridePrefix = 'fc-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 {
 export class Theme {
 
 
@@ -11,7 +12,7 @@ export class Theme {
   iconOverridePrefix: string
   iconOverridePrefix: string
 
 
 
 
-  constructor(calendarOptions) {
+  constructor(calendarOptions: RefinedCalendarOptions) {
     if (this.iconOverrideOption) {
     if (this.iconOverrideOption) {
       this.setIconOverride(
       this.setIconOverride(
         calendarOptions[this.iconOverrideOption]
         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 { Theme } from './theme/Theme'
 import { mapHash } from './util/object'
 import { mapHash } from './util/object'
 import { CalendarApi } from './CalendarApi'
 import { CalendarApi } from './CalendarApi'
+import { RefinedCalendarOptions } from './options'
 
 
 export interface ToolbarModel {
 export interface ToolbarModel {
   [sectionName: string]: ToolbarWidget[][]
   [sectionName: string]: ToolbarWidget[][]
@@ -14,11 +15,18 @@ export interface ToolbarWidget {
   buttonText?: any
   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(
 export function parseToolbars(
-  calendarOptions: any,
-  calendarOptionOverrides: any,
+  calendarOptions: RefinedCalendarOptions,
+  calendarOptionOverrides: Partial<RefinedCalendarOptions>,
   theme: Theme,
   theme: Theme,
   viewSpecs: ViewSpecHash,
   viewSpecs: ViewSpecHash,
   calendarApi: CalendarApi
   calendarApi: CalendarApi
@@ -32,15 +40,18 @@ export function parseToolbars(
 
 
 
 
 function parseToolbar(
 function parseToolbar(
-  sectionStrHash: { [sectionName: string]: string },
-  calendarOptions: any,
-  calendarOptionOverrides: any,
+  sectionStrHash: ToolbarInput,
+  calendarOptions: RefinedCalendarOptions,
+  calendarOptionOverrides: Partial<RefinedCalendarOptions>,
   theme: Theme,
   theme: Theme,
   viewSpecs: ViewSpecHash,
   viewSpecs: ViewSpecHash,
   calendarApi: CalendarApi,
   calendarApi: CalendarApi,
   viewsWithButtons: string[] // dump side effects
   viewsWithButtons: string[] // dump side effects
 ) : ToolbarModel {
 ) : 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(
 function parseSection(
   sectionStr: string,
   sectionStr: string,
-  calendarOptions: any,
-  calendarOptionOverrides: any,
+  calendarOptions: RefinedCalendarOptions,
+  calendarOptionOverrides: Partial<RefinedCalendarOptions>,
   theme: Theme,
   theme: Theme,
   viewSpecs: ViewSpecHash,
   viewSpecs: ViewSpecHash,
   calendarApi: CalendarApi,
   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 type ClassNameInput = string | string[]
 
 
-export function parseClassName(raw: ClassNameInput) {
+export function parseClassNames(raw: ClassNameInput) {
   if (Array.isArray(raw)) {
   if (Array.isArray(raw)) {
     return raw
     return raw
   } else if (typeof raw === 'string') {
   } 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
 export function padStart(val, len) { // doesn't work with total length more than 3
   let s = String(val)
   let s = String(val)
   return '000'.substr(0, len - s.length) + s
   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
 // Number and Boolean are only types that defaults or not computed for
 // TODO: write more comments
 // TODO: write more comments
+// TODO: will kill
 export function refineProps(rawProps: GenericHash, processors: GenericHash, defaults: GenericHash = {}, leftoverProps?: GenericHash): GenericHash {
 export function refineProps(rawProps: GenericHash, processors: GenericHash, defaults: GenericHash = {}, leftoverProps?: GenericHash): GenericHash {
   let refined: GenericHash = {}
   let refined: GenericHash = {}
 
 

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

@@ -98,8 +98,8 @@ function isInteractionPropsValid(state: SplittableProps, context: CalendarContex
 
 
     // overlap
     // 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) {
     for (let otherInstanceId in otherInstances) {
       let otherInstance = otherInstances[otherInstanceId]
       let otherInstance = otherInstances[otherInstanceId]
@@ -117,7 +117,7 @@ function isInteractionPropsValid(state: SplittableProps, context: CalendarContex
           return false
           return false
         }
         }
 
 
-        if (overlapFunc && !overlapFunc(
+        if (eventOverlapFunc && !eventOverlapFunc(
           new EventApi(context, otherDefs[otherInstance.defId], otherInstance), // still event
           new EventApi(context, otherDefs[otherInstance.defId], otherInstance), // still event
           new EventApi(context, subjectDef, subjectInstance) // moving event
           new EventApi(context, subjectDef, subjectInstance) // moving event
         )) {
         )) {
@@ -186,8 +186,8 @@ function isDateSelectionPropsValid(state: SplittableProps, context: CalendarCont
 
 
   // overlap
   // 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) {
   for (let relevantInstanceId in relevantInstances) {
     let relevantInstance = relevantInstances[relevantInstanceId]
     let relevantInstance = relevantInstances[relevantInstanceId]
@@ -199,8 +199,9 @@ function isDateSelectionPropsValid(state: SplittableProps, context: CalendarCont
         return false
         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
         return false
       }
       }

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

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

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

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

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

@@ -20,7 +20,7 @@ export class DayTableView extends TableView {
 
 
 
 
   render() {
   render() {
-    let { options, computedOptions, dateProfileGenerator } = this.context
+    let { options, dateProfileGenerator } = this.context
     let { props } = this
     let { props } = this
     let dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator)
     let dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator)
 
 
@@ -44,7 +44,7 @@ export class DayTableView extends TableView {
         eventSelection={props.eventSelection}
         eventSelection={props.eventSelection}
         eventDrag={props.eventDrag}
         eventDrag={props.eventDrag}
         eventResize={props.eventResize}
         eventResize={props.eventResize}
-        nextDayThreshold={computedOptions.nextDayThreshold}
+        nextDayThreshold={options.nextDayThreshold}
         colGroupNode={contentArg.tableColGroupNode}
         colGroupNode={contentArg.tableColGroupNode}
         tableMinWidth={contentArg.tableMinWidth}
         tableMinWidth={contentArg.tableMinWidth}
         dayMaxEvents={options.dayMaxEvents}
         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 { TableSeg } from './TableSeg'
 import { TableBlockEvent } from './TableBlockEvent'
 import { TableBlockEvent } from './TableBlockEvent'
 import { TableListItemEvent } from './TableListItemEvent'
 import { TableListItemEvent } from './TableListItemEvent'
@@ -28,7 +28,7 @@ export class MorePopover extends DateComponent<MorePopoverProps> {
     let { options, dateEnv } = this.context
     let { options, dateEnv } = this.context
     let { props } = this
     let { props } = this
     let { date, hiddenInstances, todayRange, dateProfile, selectedInstanceId } = props
     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 (
     return (
       <DayCellRoot date={date} dateProfile={dateProfile} todayRange={todayRange} elRef={this.handlePopoverEl}>
       <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"
   handleMoreLinkClick = (arg: MoreLinkArg) => { // TODO: bad names "more link click" versus "more click"
     let { context } = this
     let { context } = this
     let { dateEnv } = context
     let { dateEnv } = context
-    let clickOption = context.options.moreLinkClick
+    let clickOption = context.options.moreLinkClick || 'popover' // TODO: define the default elsewhere
 
 
     function segForPublic(seg: TableSeg) {
     function segForPublic(seg: TableSeg) {
       let { def, instance, range } = seg.eventRange
       let { def, instance, range } = seg.eventRange

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

@@ -19,6 +19,8 @@ import {
   DateProfile,
   DateProfile,
   VUIEvent,
   VUIEvent,
   setRef,
   setRef,
+  createFormatter,
+  ViewApi,
 } from '@fullcalendar/common'
 } from '@fullcalendar/common'
 import { TableSeg } from './TableSeg'
 import { TableSeg } from './TableSeg'
 
 
@@ -71,7 +73,13 @@ export interface HookProps {
   isToday: boolean
   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> {
 export class TableCell extends DateComponent<TableCellProps> {
@@ -84,6 +92,12 @@ export class TableCell extends DateComponent<TableCellProps> {
     let { props } = this
     let { props } = this
     let { date, dateProfile } = props
     let { date, dateProfile } = props
 
 
+    let hookProps: MoreLinkHookProps = {
+      num: props.moreCnt,
+      text: props.buildMoreLinkText(props.moreCnt),
+      view: viewApi
+    }
+
     return (
     return (
       <DayCellRoot
       <DayCellRoot
         date={date}
         date={date}
@@ -131,9 +145,13 @@ export class TableCell extends DateComponent<TableCellProps> {
                 {props.fgContent}
                 {props.fgContent}
                 {Boolean(props.moreCnt) &&
                 {Boolean(props.moreCnt) &&
                   <div className='fc-daygrid-day-bottom' style={{ marginTop: props.moreMarginTop }}>
                   <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}
                       defaultContent={renderMoreLinkInner}
+                      didMount={options.moreLinkDidMount}
+                      willUnmount={options.moreLinkWillUnmount}
                     >
                     >
                       {(rootElRef, classNames, innerElRef, innerContent) => (
                       {(rootElRef, classNames, innerElRef, innerContent) => (
                         <a onClick={this.handleMoreLinkClick} ref={rootElRef} className={[ 'fc-daygrid-more-link' ].concat(classNames).join(' ')}>
                         <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'
 import { DEFAULT_TABLE_EVENT_TIME_FORMAT } from './event-rendering'
 
 
 
 
@@ -17,15 +17,8 @@ export class TableListItemEvent extends BaseComponent<DotTableEventProps> {
   render() {
   render() {
     let { props, context } = this
     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)
     let timeText = buildSegTimeText(props.seg, timeFormat, context, true, props.defaultDisplayEventEnd)
 
 
     return (
     return (

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

@@ -88,7 +88,7 @@ export class TableRow extends DateComponent<TableRowProps, TableRowState> {
       state.segHeights,
       state.segHeights,
       state.maxContentHeight,
       state.maxContentHeight,
       colCnt,
       colCnt,
-      context.computedOptions.eventOrderSpecs
+      context.options.eventOrderSpecs
     )
     )
 
 
     let selectedInstanceHash = // TODO: messy way to compute this
     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',
   hour: 'numeric',
   minute: '2-digit',
   minute: '2-digit',
   omitZeroMinute: true,
   omitZeroMinute: true,
   meridiem: 'narrow'
   meridiem: 'narrow'
-}
+})
 
 
 
 
 export function hasListItemDisplay(eventRange: EventRenderRange) {
 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 { DayTableView } from './DayTableView'
 import './main.scss'
 import './main.scss'
 import { TableDateProfileGenerator } from './TableDateProfileGenerator'
 import { TableDateProfileGenerator } from './TableDateProfileGenerator'
+import { OPTION_REFINERS } from './options'
 
 
 export { DayTable, DayTableSlicer } from './DayTable'
 export { DayTable, DayTableSlicer } from './DayTable'
 export { Table } from './Table'
 export { Table } from './Table'
@@ -13,6 +14,7 @@ export { DayTableView as DayGridView } // export as old name!
 
 
 export default createPlugin({
 export default createPlugin({
   initialView: 'dayGridMonth',
   initialView: 'dayGridMonth',
+  optionRefiners: OPTION_REFINERS,
   views: {
   views: {
 
 
     dayGrid: {
     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 { createPlugin, EventSourceDef, refineProps, addDays, DateEnv, requestJson } from '@fullcalendar/common'
+import { OPTION_REFINERS } from './options'
 
 
 // TODO: expose somehow
 // TODO: expose somehow
 const API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'
 const API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'
@@ -11,14 +12,8 @@ const STANDARD_PROPS = { // for event source parsing
   data: null
   data: null
 }
 }
 
 
-
 declare module '@fullcalendar/common' {
 declare module '@fullcalendar/common' {
-
-  interface OptionsInput {
-    googleCalendarApiKey?: string
-  }
-
-  interface ExtendedEventSourceInput {
+  interface ExtendedEventSourceInput { // add this to refiner system somehow
     googleCalendarApiKey?: string
     googleCalendarApiKey?: string
     googleCalendarId?: string
     googleCalendarId?: string
     googleCalendarApiBase?: string
     googleCalendarApiBase?: string
@@ -190,5 +185,6 @@ function injectQsComponent(url, component) {
 }
 }
 
 
 export default createPlugin({
 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 { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
 import { ExternalElementDragging, DragMetaGenerator } from './ExternalElementDragging'
 import { ExternalElementDragging, DragMetaGenerator } from './ExternalElementDragging'
 
 
@@ -47,11 +47,11 @@ export class ExternalDraggable {
     dragging.minDistance =
     dragging.minDistance =
       minDistance != null ?
       minDistance != null ?
         minDistance :
         minDistance :
-        (ev.isTouch ? 0 : globalDefaults.eventDragMinDistance)
+        (ev.isTouch ? 0 : RAW_BASE_DEFAULTS.eventDragMinDistance)
 
 
     dragging.delay =
     dragging.delay =
       ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
       ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
-        (longPressDelay != null ? longPressDelay : globalDefaults.longPressDelay) :
+        (longPressDelay != null ? longPressDelay : RAW_BASE_DEFAULTS.longPressDelay) :
         0
         0
   }
   }
 
 

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

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

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

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

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

@@ -1,14 +1,14 @@
 import {
 import {
-  MinimalEventProps, BaseComponent, ViewContext, h,
+  MinimalEventProps, BaseComponent, ViewContext, h, AllDayHookProps,
   Seg, isMultiDayRange, DateFormatter, buildSegTimeText, createFormatter, EventMeta, EventRoot, ComponentChildren, RenderHook
   Seg, isMultiDayRange, DateFormatter, buildSegTimeText, createFormatter, EventMeta, EventRoot, ComponentChildren, RenderHook
 } from "@fullcalendar/common"
 } from "@fullcalendar/common"
 
 
 
 
-const DEFAULT_TIME_FORMAT = {
+const DEFAULT_TIME_FORMAT = createFormatter({
   hour: 'numeric',
   hour: 'numeric',
   minute: '2-digit',
   minute: '2-digit',
   meridiem: 'short'
   meridiem: 'short'
-}
+})
 
 
 
 
 export class ListViewEventRow extends BaseComponent<MinimalEventProps> {
 export class ListViewEventRow extends BaseComponent<MinimalEventProps> {
@@ -17,11 +17,7 @@ export class ListViewEventRow extends BaseComponent<MinimalEventProps> {
     let { props, context } = this
     let { props, context } = this
     let { seg } = props
     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 (
     return (
       <EventRoot
       <EventRoot
@@ -72,9 +68,9 @@ function renderEventInnerContent(props: EventMeta) {
 
 
 
 
 function buildTimeContent(seg: Seg, timeFormat: DateFormatter, context: ViewContext): ComponentChildren {
 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 eventDef = seg.eventRange.def
     let eventInstance = seg.eventRange.instance
     let eventInstance = seg.eventRange.instance
     let doAllDay = false
     let doAllDay = false
@@ -120,13 +116,20 @@ function buildTimeContent(seg: Seg, timeFormat: DateFormatter, context: ViewCont
     }
     }
 
 
     if (doAllDay) {
     if (doAllDay) {
-      let hookProps = {
+      let hookProps: AllDayHookProps = {
         text: context.options.allDayText,
         text: context.options.allDayText,
         view: context.viewApi
         view: context.viewApi
       }
       }
 
 
       return (
       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) => (
           {(rootElRef, classNames, innerElRef, innerContent) => (
             <td className={[ 'fc-list-event-time' ].concat(classNames).join(' ')} ref={rootElRef}>
             <td className={[ 'fc-list-event-time' ].concat(classNames).join(' ')} ref={rootElRef}>
               {innerContent}
               {innerContent}

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

@@ -1,6 +1,6 @@
 import {
 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'
 } from '@fullcalendar/common'
 
 
 
 
@@ -9,7 +9,7 @@ export interface ListViewHeaderRowProps {
   todayRange: DateRange
   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
   text: string
   sideText: string
   sideText: string
 }
 }
@@ -23,10 +23,8 @@ export class ListViewHeaderRow extends BaseComponent<ListViewHeaderRowProps> {
     let { theme, dateEnv, options, viewApi } = this.context
     let { theme, dateEnv, options, viewApi } = this.context
 
 
     let dayMeta = getDateMeta(dayDate, todayRange)
     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
     let navLinkData = options.navLinks
       ? buildNavLinkData(dayDate)
       ? buildNavLinkData(dayDate)
@@ -47,7 +45,14 @@ export class ListViewHeaderRow extends BaseComponent<ListViewHeaderRowProps> {
 
 
     // TODO: make a reusable HOC for dayHeader (used in daygrid/timegrid too)
     // TODO: make a reusable HOC for dayHeader (used in daygrid/timegrid too)
     return (
     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) => (
         {(rootElRef, customClassNames, innerElRef, innerContent) => (
           <tr
           <tr
             ref={rootElRef}
             ref={rootElRef}

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

@@ -1,10 +1,12 @@
 import { createPlugin } from '@fullcalendar/common'
 import { createPlugin } from '@fullcalendar/common'
 import { ListView } from './ListView'
 import { ListView } from './ListView'
+import { OPTION_REFINERS } from './options'
 import './main.scss'
 import './main.scss'
 
 
 export { ListView }
 export { ListView }
 
 
 export default createPlugin({
 export default createPlugin({
+  optionRefiners: OPTION_REFINERS,
   views: {
   views: {
 
 
     list: {
     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() {
   render() {
-    let { options, computedOptions, dateEnv, dateProfileGenerator } = this.context
+    let { options, dateEnv, dateProfileGenerator } = this.context
     let { props } = this
     let { props } = this
     let { dateProfile } = props
     let { dateProfile } = props
     let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator)
     let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator)
     let splitProps = this.allDaySplitter.splitProps(props)
     let splitProps = this.allDaySplitter.splitProps(props)
-    let 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 { dayMinWidth } = options
 
 
     let headerContent = options.dayHeaders &&
     let headerContent = options.dayHeaders &&
@@ -36,12 +36,12 @@ export class DayTimeColsView extends TimeColsView {
         renderIntro={dayMinWidth ? null : this.renderHeadAxis}
         renderIntro={dayMinWidth ? null : this.renderHeadAxis}
       />
       />
 
 
-    let allDayContent = options.allDaySlot && ((contentArg: ChunkContentCallbackArgs) => (
+    let allDayContent = (options.allDaySlot !== false) && ((contentArg: ChunkContentCallbackArgs) => (
       <DayTable
       <DayTable
         {...splitProps['allDay']}
         {...splitProps['allDay']}
         dateProfile={dateProfile}
         dateProfile={dateProfile}
         dayTableModel={dayTableModel}
         dayTableModel={dayTableModel}
-        nextDayThreshold={computedOptions.nextDayThreshold}
+        nextDayThreshold={options.nextDayThreshold}
         tableMinWidth={contentArg.tableMinWidth}
         tableMinWidth={contentArg.tableMinWidth}
         colGroupNode={contentArg.tableColGroupNode}
         colGroupNode={contentArg.tableColGroupNode}
         renderRowIntro={dayMinWidth ? null : this.renderTableRowAxis}
         renderRowIntro={dayMinWidth ? null : this.renderTableRowAxis}
@@ -60,7 +60,7 @@ export class DayTimeColsView extends TimeColsView {
         dayTableModel={dayTableModel}
         dayTableModel={dayTableModel}
         dateProfile={dateProfile}
         dateProfile={dateProfile}
         axis={!dayMinWidth}
         axis={!dayMinWidth}
-        slotDuration={computedOptions.slotDuration}
+        slotDuration={options.slotDuration}
         slatMetas={slatMetas}
         slatMetas={slatMetas}
         forPrint={props.forPrint}
         forPrint={props.forPrint}
         tableColGroupNode={contentArg.tableColGroupNode}
         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
     // assigns TO THE SEGS THEMSELVES
     // also, receives resorted array
     // 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) => {
     return segs.map((seg) => {
       let instanceId = seg.eventRange.instance.instanceId
       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',
   hour: 'numeric',
   minute: '2-digit',
   minute: '2-digit',
   meridiem: false
   meridiem: false
-}
+})
 
 
 
 
 export class TimeColEvent extends BaseComponent<MinimalEventProps> {
 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) {
   positionToHit(positionLeft, positionTop) {
-    let { dateEnv, computedOptions } = this.context
+    let { dateEnv, options } = this.context
     let { colCoords } = this
     let { colCoords } = this
     let { dateProfile } = this.props
     let { dateProfile } = this.props
     let { slatCoords } = this.state
     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 colIndex = colCoords.leftToIndex(positionLeft)
     let slatIndex = slatCoords.positions.topToIndex(positionTop)
     let slatIndex = slatCoords.positions.topToIndex(positionTop)

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

@@ -17,7 +17,9 @@ import {
   DateEnv,
   DateEnv,
   ViewContextType,
   ViewContextType,
   RenderHook,
   RenderHook,
-  DateProfile
+  DateProfile,
+  SlotLabelHookProps,
+  SlotLaneHookProps
 } from '@fullcalendar/common'
 } from '@fullcalendar/common'
 import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
 import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
 
 
@@ -138,16 +140,18 @@ export class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> {
 
 
   render() {
   render() {
     let { props, context } = this
     let { props, context } = this
+    let { options } = context
     let { slatElRefs } = props
     let { slatElRefs } = props
 
 
     return (
     return (
       <tbody>
       <tbody>
         {props.slatMetas.map((slatMeta, i) => {
         {props.slatMetas.map((slatMeta, i) => {
-          let hookProps = {
+          let hookProps: SlotLaneHookProps = {
             time: slatMeta.time,
             time: slatMeta.time,
             date: context.dateEnv.toDate(slatMeta.date),
             date: context.dateEnv.toDate(slatMeta.date),
             view: context.viewApi
             view: context.viewApi
           }
           }
+
           let classNames = [
           let classNames = [
             'fc-timegrid-slot',
             'fc-timegrid-slot',
             'fc-timegrid-slot-lane',
             'fc-timegrid-slot-lane',
@@ -162,7 +166,13 @@ export class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> {
               {props.axis &&
               {props.axis &&
                 <TimeColsAxisCell {...slatMeta} />
                 <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) => (
                 {(rootElRef, customClassNames, innerElRef, innerContent) => (
                   <td
                   <td
                     ref={rootElRef}
                     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',
   hour: 'numeric',
   minute: '2-digit',
   minute: '2-digit',
   omitZeroMinute: true,
   omitZeroMinute: true,
   meridiem: 'short'
   meridiem: 'short'
-}
+})
 
 
 export function TimeColsAxisCell(props: TimeSlatMeta) {
 export function TimeColsAxisCell(props: TimeSlatMeta) {
   let classNames = [
   let classNames = [
@@ -206,8 +216,8 @@ export function TimeColsAxisCell(props: TimeSlatMeta) {
 
 
         } else {
         } else {
           let { dateEnv, options, viewApi } = context
           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,
             time: props.time,
             date: dateEnv.toDate(props.date),
             date: dateEnv.toDate(props.date),
             view: viewApi,
             view: viewApi,
@@ -215,7 +225,14 @@ export function TimeColsAxisCell(props: TimeSlatMeta) {
           }
           }
 
 
           return (
           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) => (
               {(rootElRef, customClassNames, innerElRef, innerContent) => (
                 <td ref={rootElRef} className={classNames.concat(customClassNames).join(' ')} data-time={props.isoTimeStr}>
                 <td ref={rootElRef} className={classNames.concat(customClassNames).join(' ')} data-time={props.isoTimeStr}>
                   <div className='fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame'>
                   <div className='fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame'>
@@ -247,11 +264,11 @@ export interface TimeSlatMeta {
   isLabeled: boolean
   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 dayStart = new Date(0)
   let slatTime = slotMinTime
   let slatTime = slotMinTime
   let slatIterator = createDuration(0)
   let slatIterator = createDuration(0)
-  let labelInterval = getLabelInterval(labelIntervalInput, slotDuration)
+  let labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration)
   let metas: TimeSlatMeta[] = []
   let metas: TimeSlatMeta[] = []
 
 
   while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) {
   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
 // Computes an automatic value for slotLabelInterval
 function computeLabelInterval(slotDuration) {
 function computeLabelInterval(slotDuration) {
   let i
   let i

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

@@ -16,13 +16,15 @@ import {
   RefObject,
   RefObject,
   renderScrollShim,
   renderScrollShim,
   getStickyHeaderDates,
   getStickyHeaderDates,
-  getStickyFooterScrollbar
+  getStickyFooterScrollbar,
+  createFormatter,
+  AllDayHookProps
 } from '@fullcalendar/common'
 } from '@fullcalendar/common'
 import { AllDaySplitter } from './AllDaySplitter'
 import { AllDaySplitter } from './AllDaySplitter'
 import { TimeSlatMeta, TimeColsAxisCell } from './TimeColsSlats'
 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
 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,
   // 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.
   // but DayGrid still needs to have classNames on inner elements in order to measure.
   renderTableRowAxis = (rowHeight?: number) => {
   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 (
     return (
       // TODO: make reusable hook. used in list view too
       // 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) => (
         {(rootElRef, classNames, innerElRef, innerContent) => (
           <td ref={rootElRef} className={[
           <td ref={rootElRef} className={[
             'fc-timegrid-axis',
             'fc-timegrid-axis',

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

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