Переглянути джерело

reorganize a lot of stuff

Adam Shaw 7 роки тому
батько
коміт
3fba2ae176

+ 3 - 3
plugins/gcal/main.ts

@@ -1,5 +1,5 @@
 import * as request from 'superagent'
-import { registerSourceType, refineProps, addDays, assignTo } from 'fullcalendar'
+import { registerSourceDef, refineProps, addDays, assignTo } from 'fullcalendar'
 
 // TODO: expose somehow
 const API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'
@@ -11,7 +11,7 @@ const STANDARD_PROPS = {
   data: null
 }
 
-registerSourceType('google-calendar', {
+registerSourceDef({
 
   parseMeta(raw) {
     if (typeof raw === 'string') {
@@ -35,7 +35,7 @@ registerSourceType('google-calendar', {
 
   fetch(arg, onSuccess, onFailure) {
     let calendar = arg.calendar
-    let meta = arg.eventSource.sourceTypeMeta
+    let meta = arg.eventSource.meta
     let apiKey = meta.googleCalendarApiKey || calendar.opt('googleCalendarApiKey')
 
     if (!apiKey) {

+ 9 - 30
src/component/DateComponent.ts

@@ -5,18 +5,19 @@ import Calendar from '../Calendar'
 import View from '../View'
 import { DateProfile } from '../DateProfileGenerator'
 import { DateMarker, DAY_IDS, addDays, startOfDay, diffDays, diffWholeDays } from '../datelib/marker'
-import { Duration, createDuration, asRoughMs } from '../datelib/duration'
+import { Duration, createDuration } from '../datelib/duration'
 import { DateSpan } from '../structs/date-span'
 import UnzonedRange from '../models/UnzonedRange'
 import { EventRenderRange, sliceEventStore } from '../component/event-rendering'
 import { EventStore } from '../structs/event-store'
-import { BusinessHourDef, buildBusinessHourEventStore } from '../structs/business-hours'
+import { BusinessHoursDef, buildBusinessHours } from '../structs/business-hours'
 import { DateEnv } from '../datelib/env'
 import Theme from '../theme/Theme'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { assignTo } from '../util/object'
 import browserContext from '../common/browser-context'
 import { Hit } from '../interactions/HitDragging'
+import { computeVisibleDayRange } from '../util/misc'
 
 
 export interface DateComponentRenderState {
@@ -25,8 +26,8 @@ export interface DateComponentRenderState {
   selection: DateSpan | null
   dragState: EventInteractionState | null
   eventResizeState: EventInteractionState | null
-  businessHoursDef: BusinessHourDef // BusinessHourDef's `false` is the empty state
-  selectedEventInstanceId: string | null
+  businessHoursDef: BusinessHoursDef // BusinessHoursDef's `false` is the empty state
+  selectedEventInstanceId: string
 }
 
 // NOTE: for fg-events, eventRange.range is NOT sliced,
@@ -78,7 +79,7 @@ export default abstract class DateComponent extends Component {
   dirtySizeFlags: any = {}
 
   dateProfile: DateProfile = null
-  businessHoursDef: BusinessHourDef = false
+  businessHoursDef: BusinessHoursDef = false
   selection: DateSpan = null
   eventStore: EventStore = null
   dragState: EventInteractionState = null
@@ -494,12 +495,12 @@ export default abstract class DateComponent extends Component {
   // ---------------------------------------------------------------------------------------------------------------
 
 
-  renderBusinessHours(businessHoursDef: BusinessHourDef) {
+  renderBusinessHours(businessHoursDef: BusinessHoursDef) {
     if (this.fillRenderer) {
       this.fillRenderer.renderSegs(
         'businessHours',
         this.eventStoreToSegs(
-          buildBusinessHourEventStore(
+          buildBusinessHours(
             businessHoursDef,
             this.hasAllDayBusinessHours,
             this.dateProfile.activeUnzonedRange,
@@ -984,7 +985,7 @@ export default abstract class DateComponent extends Component {
   // Returns the date range of the full days the given range visually appears to occupy.
   // Returns a plain object with start/end, NOT an UnzonedRange!
   computeDayRange(unzonedRange): UnzonedRange {
-    return computeDayRange(unzonedRange, this.nextDayThreshold)
+    return computeVisibleDayRange(unzonedRange, this.nextDayThreshold)
   }
 
 
@@ -1010,25 +1011,3 @@ export default abstract class DateComponent extends Component {
   }
 
 }
-
-
-function computeDayRange(unzonedRange: UnzonedRange, nextDayThreshold: Duration): UnzonedRange {
-  let startDay: DateMarker = startOfDay(unzonedRange.start) // the beginning of the day the range starts
-  let end: DateMarker = unzonedRange.end
-  let endDay: DateMarker = startOfDay(end)
-  let endTimeMS: number = end.valueOf() - endDay.valueOf() // # of milliseconds into `endDay`
-
-  // If the end time is actually inclusively part of the next day and is equal to or
-  // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
-  // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
-  if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) {
-    endDay = addDays(endDay, 1)
-  }
-
-  // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
-  if (endDay <= startDay) {
-    endDay = addDays(startDay, 1)
-  }
-
-  return new UnzonedRange(startDay, endDay)
-}

+ 5 - 4
src/event-sources/array-event-source.ts

@@ -1,18 +1,19 @@
-import { registerSourceType } from '../structs/event-source'
+import { registerEventSourceDef } from '../structs/event-source'
 import { EventInput } from '../structs/event'
 
-registerSourceType('array', {
+registerEventSourceDef({
 
-  parseMeta(raw: any): EventInput[] {
+  parseMeta(raw: any): EventInput[] | null {
     if (Array.isArray(raw)) { // short form
       return raw
     } else if (Array.isArray(raw.events)) {
       return raw.events
     }
+    return null
   },
 
   fetch(arg, success) {
-    success(arg.eventSource.sourceTypeMeta as EventInput[])
+    success(arg.eventSource.meta as EventInput[])
   }
 
 })

+ 3 - 3
src/event-sources/func-event-source.ts

@@ -1,8 +1,8 @@
 import { unpromisify } from '../util/promise'
-import { registerSourceType } from '../structs/event-source'
+import { registerEventSourceDef } from '../structs/event-source'
 import { EventInput } from '../structs/event'
 
-registerSourceType('function', {
+registerEventSourceDef({
 
   parseMeta(raw: any): EventInput[] {
     if (typeof raw === 'function') { // short form
@@ -16,7 +16,7 @@ registerSourceType('function', {
     const dateEnv = arg.calendar.dateEnv
 
     unpromisify(
-      arg.eventSource.sourceTypeMeta({ // the function
+      arg.eventSource.meta({ // the function
         start: dateEnv.toDate(arg.range.start),
         end: dateEnv.toDate(arg.range.end),
         timeZone: dateEnv.timeZone

+ 5 - 5
src/event-sources/json-feed-event-source.ts

@@ -2,7 +2,7 @@ import UnzonedRange from '../models/UnzonedRange'
 import * as request from 'superagent'
 import { assignTo } from '../util/object'
 import Calendar from '../Calendar'
-import { registerSourceType } from '../structs/event-source'
+import { registerEventSourceDef } from '../structs/event-source'
 
 interface JsonFeedMeta {
   url: string
@@ -13,9 +13,9 @@ interface JsonFeedMeta {
   timezoneParam?: string
 }
 
-registerSourceType('json-feed', {
+registerEventSourceDef({
 
-  parseMeta(raw: any): JsonFeedMeta {
+  parseMeta(raw: any): JsonFeedMeta | null {
     if (typeof raw === 'string') { // short form
       raw = { url: raw }
     } else if (!raw || typeof raw !== 'object' || !raw.url) {
@@ -33,14 +33,14 @@ registerSourceType('json-feed', {
   },
 
   fetch(arg, success, failure) {
-    let meta: JsonFeedMeta = arg.eventSource.sourceTypeMeta
+    let meta: JsonFeedMeta = arg.eventSource.meta
     let theRequest
     let requestParams = buildRequestParams(meta, arg.range, arg.calendar)
 
     if (meta.method === 'GET') {
       theRequest = request.get(meta.url).query(requestParams) // querystring params
     } else {
-      theRequest = request(meta.method, this.url).send(requestParams) // body data
+      theRequest = request(meta.method, meta.url).send(requestParams) // body data
     }
 
     theRequest.end((error, res) => {

+ 1 - 1
src/exports.ts

@@ -106,7 +106,7 @@ export { defineLocale, getLocale, getLocaleCodes } from './datelib/locale'
 export { DateFormatter, createFormatter } from './datelib/formatting'
 export { parse as parseMarker } from './datelib/parsing'
 
-export { registerSourceType } from './structs/event-source'
+export { registerEventSourceDef } from './structs/event-source'
 export { refineProps } from './util/misc'
 
 export { default as PointerDragging, PointerDragEvent } from './dnd/PointerDragging'

+ 3 - 2
src/interactions/EventDragging.ts

@@ -2,7 +2,7 @@ import { default as DateComponent, Seg } from '../component/DateComponent'
 import { getElSeg } from '../component/renderers/EventRenderer'
 import { PointerDragEvent } from '../dnd/PointerDragging'
 import HitDragging, { isHitsEqual, Hit } from './HitDragging'
-import { EventMutation, diffDates, applyMutationToAll } from '../structs/event-mutation'
+import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation'
 import browserContext from '../common/browser-context'
 import { startOfDay } from '../datelib/marker'
 import { elementClosest } from '../util/dom-manip'
@@ -10,6 +10,7 @@ import FeaturefulElementDragging from '../dnd/FeaturefulElementDragging'
 import { EventStore, getRelatedEvents, createEmptyEventStore } from '../structs/event-store'
 import Calendar from '../Calendar'
 import { EventInteractionState } from '../interactions/event-interaction-state'
+import { diffDates } from '../util/misc'
 
 export default class EventDragging {
 
@@ -114,7 +115,7 @@ export default class EventDragging {
         validMutation = computeEventMutation(initialHit, hit)
 
         if (validMutation) {
-          mutatedRelatedEvents = applyMutationToAll(relatedEvents, validMutation, receivingCalendar)
+          mutatedRelatedEvents = applyMutationToEventStore(relatedEvents, validMutation, receivingCalendar)
         }
       }
     }

+ 3 - 2
src/interactions/EventResizing.ts

@@ -1,6 +1,6 @@
 import { default as DateComponent, Seg } from '../component/DateComponent'
 import HitDragging, { isHitsEqual, Hit } from './HitDragging'
-import { EventMutation, diffDates, applyMutationToAll } from '../structs/event-mutation'
+import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation'
 import { elementClosest } from '../util/dom-manip'
 import UnzonedRange from '../models/UnzonedRange'
 import FeaturefulElementDragging from '../dnd/FeaturefulElementDragging'
@@ -8,6 +8,7 @@ import { PointerDragEvent } from '../dnd/PointerDragging'
 import { getElSeg } from '../component/renderers/EventRenderer'
 import { EventInstance } from '../structs/event'
 import { EventStore, getRelatedEvents } from '../structs/event-store'
+import { diffDates } from '../util/misc'
 
 export default class EventDragging {
 
@@ -78,7 +79,7 @@ export default class EventDragging {
     }
 
     if (mutation) {
-      let mutatedRelated = applyMutationToAll(relatedEvents, mutation, calendar)
+      let mutatedRelated = applyMutationToEventStore(relatedEvents, mutation, calendar)
 
       calendar.dispatch({
         type: 'SET_EVENT_RESIZE',

+ 2 - 0
src/main.ts

@@ -9,4 +9,6 @@ import './list/config'
 import './event-sources/json-feed-event-source'
 import './event-sources/array-event-source'
 
+import './structs/recurring-event-simple'
+
 export = exportHooks

+ 4 - 3
src/reducers/event-sources.ts

@@ -1,7 +1,7 @@
 import { assignTo } from '../util/object'
 import Calendar from '../Calendar'
 import { warn } from '../util/misc'
-import { EventSource, EventSourceHash, parseSource, sourceTypes } from '../structs/event-source'
+import { EventSource, EventSourceHash, parseEventSource, getEventSourceDef } from '../structs/event-source'
 
 let uid = 0
 
@@ -13,7 +13,7 @@ export function reduceEventSourceHash(sourceHash: EventSourceHash, action: any,
   switch (action.type) {
 
     case 'ADD_EVENT_SOURCE':
-      eventSource = parseSource(action.rawSource)
+      eventSource = parseEventSource(action.rawSource)
 
       if (eventSource) {
         if (calendar.state.dateProfile) {
@@ -44,7 +44,8 @@ export function reduceEventSourceHash(sourceHash: EventSourceHash, action: any,
       eventSource = sourceHash[action.sourceId]
 
       let fetchId = String(uid++)
-      sourceTypes[eventSource.sourceType].fetch(
+
+      getEventSourceDef(eventSource.sourceDefId).fetch(
         {
           eventSource,
           calendar,

+ 19 - 8
src/reducers/event-store.ts

@@ -1,8 +1,8 @@
 import Calendar from '../Calendar'
 import { filterHash } from '../util/object'
-import { applyMutationToRelated } from '../structs/event-mutation'
+import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation'
 import { EventDef, EventInstance } from '../structs/event'
-import { EventStore, addRawEvents, mergeStores } from '../structs/event-store'
+import { EventStore, parseEventStore, mergeEventStores, getRelatedEvents } from '../structs/event-store'
 
 // reducing
 
@@ -15,12 +15,17 @@ export function reduceEventStore(eventStore: EventStore, action: any, calendar:
       eventSource = calendar.state.eventSources[action.sourceId]
 
       if (eventSource.latestFetchId === action.fetchId) { // this is checked in event-sources too :(
-        eventStore = excludeSource(eventStore, action.sourceId)
-        addRawEvents(eventStore, action.sourceId, action.fetchRange, action.rawEvents, calendar)
+        return parseEventStore(
+          action.rawEvents,
+          action.sourceId,
+          action.fetchRange,
+          calendar,
+          excludeSource(eventStore, action.sourceId)
+        )
+      } else {
+        return eventStore
       }
 
-      return eventStore
-
     case 'CLEAR_EVENT_SOURCE': // TODO: wire up
       return excludeSource(eventStore, action.sourceId)
 
@@ -28,7 +33,7 @@ export function reduceEventStore(eventStore: EventStore, action: any, calendar:
       return applyMutationToRelated(eventStore, action.instanceId, action.mutation, calendar)
 
     case 'ADD_EVENTS':
-      return mergeStores(eventStore, action.eventStore)
+      return mergeEventStores(eventStore, action.eventStore)
 
     case 'REMOVE_EVENTS':
       return excludeEventInstances(eventStore, action.eventStore)
@@ -38,7 +43,7 @@ export function reduceEventStore(eventStore: EventStore, action: any, calendar:
   }
 }
 
-function excludeEventInstances(eventStore: EventStore, removals: EventStore) {
+function excludeEventInstances(eventStore: EventStore, removals: EventStore): EventStore {
   return {
     defs: eventStore.defs,
     instances: filterHash(eventStore.instances, function(instance: EventInstance) {
@@ -58,3 +63,9 @@ function excludeSource(eventStore: EventStore, sourceId: string): EventStore {
   }
 }
 
+function applyMutationToRelated(eventStore: EventStore, instanceId: string, mutation: EventMutation, calendar: Calendar): EventStore {
+  let relatedStore = getRelatedEvents(eventStore, instanceId)
+  relatedStore = applyMutationToEventStore(relatedStore, mutation, calendar)
+  return mergeEventStores(eventStore, relatedStore)
+}
+

+ 15 - 30
src/structs/business-hours.ts

@@ -1,11 +1,13 @@
 import Calendar from '../Calendar'
 import UnzonedRange from '../models/UnzonedRange'
 import { assignTo } from '../util/object'
-import { expandRecurring } from './recurring-event'
-import { EventInput, parseEventDef, createEventInstance } from './event'
-import { EventStore } from './event-store'
+import { EventInput } from './event'
+import { EventStore, parseEventStore } from './event-store'
 
-export type BusinessHourDef = boolean | EventInput | EventInput[] // TODO: rename to plural?
+/*
+*/
+
+export type BusinessHoursDef = boolean | EventInput | EventInput[]
 
 const BUSINESS_HOUR_EVENT_DEFAULTS = {
   startTime: '09:00',
@@ -15,38 +17,21 @@ const BUSINESS_HOUR_EVENT_DEFAULTS = {
   className: 'fc-nonbusiness'
 }
 
-
-export function buildBusinessHourEventStore(
-  input: BusinessHourDef,
+export function buildBusinessHours(
+  input: BusinessHoursDef,
   isAllDay: boolean,
   framingRange: UnzonedRange,
   calendar: Calendar
 ): EventStore {
-  let eventInputs = refineEventInputs(input, isAllDay)
-  let eventStore: EventStore = {
-    defs: {},
-    instances: {}
-  }
-
-  // TODO: join with event-store
-  for (let eventInput of eventInputs) {
-    let def = parseEventDef(eventInput, '', isAllDay, true) // nooo, do this second, with lefotvers
-    let ranges = expandRecurring(eventInput, framingRange, calendar).ranges
-
-    eventStore.defs[def.defId] = def
-
-    for (let range of ranges) {
-      let instance = createEventInstance(def.defId, range)
-
-      eventStore.instances[instance.instanceId] = instance
-    }
-  }
-
-  return eventStore
+  return parseEventStore(
+    refineInputs(input, isAllDay),
+    '',
+    framingRange,
+    calendar
+  )
 }
 
-
-function refineEventInputs(input: BusinessHourDef, isAllDay: boolean): EventInput[] {
+function refineInputs(input: BusinessHoursDef, isAllDay: boolean): EventInput[] {
   let rawDefs: EventInput[]
 
   if (input === true) {

+ 6 - 1
src/structs/date-span.ts

@@ -2,6 +2,9 @@ import UnzonedRange from '../models/UnzonedRange'
 import { DateInput, DateEnv } from '../datelib/env'
 import { refineProps } from '../util/misc'
 
+/*
+*/
+
 export interface DateSpanInput {
   start: DateInput
   end: DateInput
@@ -21,7 +24,7 @@ const STANDARD_PROPS = {
   isAllDay: Boolean
 }
 
-export function parseDateSpan(raw: DateSpanInput, dateEnv: DateEnv): DateSpan {
+export function parseDateSpan(raw: DateSpanInput, dateEnv: DateEnv): DateSpan | null {
   let otherProps = {} as any
   let standardProps = refineProps(raw, STANDARD_PROPS, otherProps)
   let startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null
@@ -40,6 +43,8 @@ export function parseDateSpan(raw: DateSpanInput, dateEnv: DateEnv): DateSpan {
 
     return otherProps
   }
+
+  return null
 }
 
 export function isDateSpansEqual(span0: DateSpan, span1: DateSpan): boolean {

+ 3 - 0
src/structs/drag-meta.ts

@@ -2,6 +2,9 @@ import { createDuration, Duration, DurationInput } from '../datelib/duration'
 import { refineProps } from '../util/misc'
 import { EventNonDateInput } from '../structs/event'
 
+/*
+*/
+
 export interface DragMetaInput extends EventNonDateInput {
   time?: DurationInput
   duration?: DurationInput

+ 18 - 37
src/structs/event-mutation.ts

@@ -1,10 +1,13 @@
 import UnzonedRange from '../models/UnzonedRange'
-import { diffDayAndTime, diffDays, startOfDay, addDays } from '../datelib/marker'
-import { Duration, createDuration } from '../datelib/duration'
-import { EventStore, mergeStores, getRelatedEvents } from './event-store'
+import { Duration } from '../datelib/duration'
+import { EventStore, createEmptyEventStore } from './event-store'
 import { EventDef, EventInstance } from './event'
 import { assignTo } from '../util/object'
 import Calendar from '../Calendar'
+import { computeAlignedDayRange } from '../util/misc'
+
+/*
+*/
 
 export interface EventMutation {
   startDelta?: Duration
@@ -13,32 +16,25 @@ export interface EventMutation {
   extendedProps?: any // for the def
 }
 
-export function applyMutationToRelated(eventStore: EventStore, instanceId: string, mutation: EventMutation, calendar: Calendar): EventStore {
-  let relatedStore = getRelatedEvents(eventStore, instanceId)
-  relatedStore = applyMutationToAll(relatedStore, mutation, calendar)
-  return mergeStores(eventStore, relatedStore)
-}
-
-export function applyMutationToAll(eventStore: EventStore, mutation: EventMutation, calendar: Calendar): EventStore {
-  let newStore = { defs: {}, instances: {} }
+// applies to ALL defs/instances within the event store
+export function applyMutationToEventStore(eventStore: EventStore, mutation: EventMutation, calendar: Calendar): EventStore {
+  let dest = createEmptyEventStore()
 
   for (let defId in eventStore.defs) {
     let def = eventStore.defs[defId]
-
-    newStore.defs[defId] = applyMutationToDef(def, mutation)
+    dest.defs[defId] = applyMutationToEventDef(def, mutation)
   }
 
   for (let instanceId in eventStore.instances) {
     let instance = eventStore.instances[instanceId]
-    let def = newStore.defs[instance.defId] // the newly MODIFIED def
-
-    newStore.instances[instanceId] = applyMutationToInstance(instance, def, mutation, calendar)
+    let def = dest.defs[instance.defId] // important to grab the newly modified def
+    dest.instances[instanceId] = applyMutationToEventInstance(instance, def, mutation, calendar)
   }
 
-  return newStore
+  return dest
 }
 
-function applyMutationToDef(eventDef: EventDef, mutation: EventMutation) {
+function applyMutationToEventDef(eventDef: EventDef, mutation: EventMutation): EventDef {
   let copy = assignTo({}, eventDef)
 
   if (mutation.standardProps) {
@@ -52,23 +48,19 @@ function applyMutationToDef(eventDef: EventDef, mutation: EventMutation) {
   return copy
 }
 
-function applyMutationToInstance(
+function applyMutationToEventInstance(
   eventInstance: EventInstance,
-  eventDef: EventDef, // after already having been modified
+  eventDef: EventDef, // must first be modified by applyMutationToEventDef
   mutation: EventMutation,
   calendar: Calendar
-) {
+): EventInstance {
   let dateEnv = calendar.dateEnv
   let forceAllDay = mutation.standardProps && mutation.standardProps.isAllDay === true
   let clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false
   let copy = assignTo({}, eventInstance)
 
   if (forceAllDay) {
-    // TODO: make a util for this?
-    let dayCnt = Math.floor(diffDays(copy.range.start, copy.range.end)) || 1
-    let start = startOfDay(copy.range.start)
-    let end = addDays(start, dayCnt)
-    copy.range = new UnzonedRange(start, end)
+    copy.range = computeAlignedDayRange(copy.range)
   }
 
   if (mutation.startDelta) {
@@ -92,14 +84,3 @@ function applyMutationToInstance(
 
   return copy
 }
-
-// best place?
-export function diffDates(date0, date1, dateEnv, largeUnit) {
-  if (largeUnit === 'year') {
-    return createDuration(dateEnv.diffWholeYears(date0, date1), 'year')!
-  } else if (largeUnit === 'month') {
-    return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month')!
-  } else {
-    return diffDayAndTime(date0, date1) // returns a duration
-  }
-}

+ 69 - 43
src/structs/event-source.ts

@@ -4,6 +4,7 @@ import { refineProps } from '../util/misc'
 import { EventInput } from './event'
 import Calendar from '../Calendar'
 
+// TODO: unify with EventNonDateInput
 export interface EventSourceInput {
   id?: string | number
   allDayDefault?: boolean
@@ -21,50 +22,49 @@ export interface EventSourceInput {
   textColor?: string
   success?: (eventInputs: EventInput[]) => void
   failure?: (errorObj: any) => void
-  [otherProp: string]: any
+  [extendedProp: string]: any
 }
 
 export interface EventSource {
   sourceId: string
-  sourceType: string
-  sourceTypeMeta: any
+  sourceDefId: number // one of the few IDs that's a NUMBER not a string
+  meta: any
   publicId: string
   isFetching: boolean
-  latestFetchId: string | null
-  fetchRange: UnzonedRange
+  latestFetchId: string
+  fetchRange: UnzonedRange | null
   allDayDefault: boolean | null
-  eventDataTransform: any
+  eventDataTransform: any // TODO: make this a real type. AND use it
   editable: boolean | null
   startEditable: boolean | null
   durationEditable: boolean | null
   overlap: any
   constraint: any
-  rendering: string | null
+  rendering: string
   className: string[]
-  color: string | null
-  backgroundColor: string | null
-  borderColor: string | null
-  textColor: string | null
-  success?: (eventInputs: EventInput[]) => void
-  failure?: (errorObj: any) => void
+  backgroundColor: string
+  borderColor: string
+  textColor: string
+  success: null | ((eventInputs: EventInput[]) => void)
+  failure: null | ((errorObj: any) => void)
 }
 
-// need this?
 export type EventSourceHash = { [sourceId: string]: EventSource }
 
-export interface EventSourceTypeSettings {
-  parseMeta: (raw: any) => any
-  fetch: (
-    arg: {
-      eventSource: EventSource
-      calendar: Calendar
-      range: UnzonedRange
-    },
-    success: (rawEvents: EventInput) => void,
-    failure: (errorObj: any) => void
-  ) => void
-}
+export type EventSourceFetcher = (
+  arg: {
+    eventSource: EventSource
+    calendar: Calendar
+    range: UnzonedRange
+  },
+  success: (rawEvents: EventInput) => void,
+  failure: (errorObj: any) => void
+) => void
 
+export interface EventSourceDef {
+  parseMeta: (raw: EventSourceInput) => object | null
+  fetch: EventSourceFetcher
+}
 
 const SIMPLE_SOURCE_PROPS = {
   allDayDefault: Boolean,
@@ -84,31 +84,57 @@ const SIMPLE_SOURCE_PROPS = {
   failure: null
 }
 
-export let sourceTypes: { [sourceTypeName: string]: EventSourceTypeSettings } = {}
+let defs: EventSourceDef[] = []
 let uid = 0
 
-export function registerSourceType(type: string, settings: EventSourceTypeSettings) {
-  sourceTypes[type] = settings
+// NOTE: if we ever want to remove defs,
+// we need to null out the entry in the array, not delete it,
+// because our event source IDs rely on the index.
+export function registerEventSourceDef(def: EventSourceDef) {
+  defs.push(def)
 }
 
-export function parseSource(raw: EventSourceInput): EventSource {
-  for (let sourceTypeName in sourceTypes) {
-    let sourceTypeSettings = sourceTypes[sourceTypeName]
-    let sourceTypeMeta = sourceTypeSettings.parseMeta(raw)
-
-    if (sourceTypeMeta) {
-      let source: EventSource = refineProps(raw, SIMPLE_SOURCE_PROPS)
-      source.sourceId = String(uid++)
-      source.sourceType = sourceTypeName
-      source.sourceTypeMeta = sourceTypeMeta
+export function getEventSourceDef(id: number): EventSourceDef {
+  return defs[id]
+}
 
-      if (raw.id != null) {
-        source.publicId = String(raw.id)
-      }
+export function parseEventSource(raw: EventSourceInput): EventSource | null {
+  for (let i = 0; i < defs.length; i++) {
+    let def = defs[i]
+    let meta = def.parseMeta(raw)
 
-      return source
+    if (meta) {
+      return parseEventSourceProps(raw, meta, i)
     }
   }
 
   return null
 }
+
+function parseEventSourceProps(raw: EventSourceInput, meta: object, sourceDefId: number): EventSource {
+  let props = refineProps(raw, SIMPLE_SOURCE_PROPS)
+
+  return {
+    sourceId: String(uid++),
+    sourceDefId,
+    meta,
+    publicId: props.id || '',
+    isFetching: false,
+    latestFetchId: '',
+    fetchRange: null,
+    allDayDefault: props.allDayDefault,
+    eventDataTransform: props.eventDataTransform,
+    editable: props.editable,
+    startEditable: props.startEditable,
+    durationEditable: props.durationEditable,
+    overlap: props.overlap,
+    constraint: props.constraint,
+    rendering: props.rendering || '',
+    className: props.className || [],
+    backgroundColor: props.backgroundColor || props.color || '',
+    borderColor: props.borderColor || props.color || '',
+    textColor: props.textColor,
+    success: props.success,
+    failure: props.failure
+  }
+}

+ 44 - 30
src/structs/event-store.ts

@@ -4,52 +4,66 @@ import { expandRecurring } from './recurring-event'
 import Calendar from '../Calendar'
 import { assignTo } from '../util/object'
 
+/*
+*/
+
 export interface EventStore {
   defs: EventDefHash
   instances: EventInstanceHash
 }
 
-export function addRawEvents(eventStore: EventStore, sourceId: string, fetchRange: UnzonedRange, rawEvents: any, calendar: Calendar) {
-  rawEvents.forEach(function(rawEvent: EventInput) {
-    let leftoverProps = {}
-    let recurringDateInfo = expandRecurring(rawEvent, fetchRange, calendar, leftoverProps)
+export function createEmptyEventStore(): EventStore {
+  return { defs: {}, instances: {} }
+}
+
+export function mergeEventStores(store0: EventStore, store1: EventStore): EventStore {
+  return {
+    defs: assignTo({}, store0.defs, store1.defs),
+    instances: assignTo({}, store0.instances, store1.instances)
+  }
+}
+
+export function parseEventStore(
+  rawEvents: EventInput[],
+  sourceId: string,
+  fetchRange: UnzonedRange,
+  calendar: Calendar,
+  dest: EventStore = createEmptyEventStore()
+): EventStore {
 
-    if (recurringDateInfo) {
-      let def = parseEventDef(leftoverProps, sourceId, recurringDateInfo.isAllDay, recurringDateInfo.hasEnd)
-      eventStore.defs[def.defId] = def
+  for (let rawEvent of rawEvents) {
+    let leftovers = {}
+    let recurringDateSpans = expandRecurring(rawEvent, fetchRange, calendar, leftovers)
 
-      for (let range of recurringDateInfo.ranges) {
+    // a recurring event?
+    if (recurringDateSpans) {
+      let def = parseEventDef(leftovers, sourceId, recurringDateSpans.isAllDay, recurringDateSpans.hasEnd)
+      dest.defs[def.defId] = def
+
+      for (let range of recurringDateSpans.ranges) {
         let instance = createEventInstance(def.defId, range)
-        eventStore.instances[instance.instanceId] = instance
+        dest.instances[instance.instanceId] = instance
       }
 
+    // a non-recurring event
     } else {
-      let dateInfo = parseEventDateSpan(rawEvent, sourceId, calendar, leftoverProps)
+      let dateSpan = parseEventDateSpan(rawEvent, sourceId, calendar, leftovers)
 
-      if (dateInfo) {
-        let def = parseEventDef(leftoverProps, sourceId, dateInfo.isAllDay, dateInfo.hasEnd)
-        let instance = createEventInstance(def.defId, dateInfo.range, dateInfo.forcedStartTzo, dateInfo.forcedEndTzo)
+      if (dateSpan) {
+        let def = parseEventDef(leftovers, sourceId, dateSpan.isAllDay, dateSpan.hasEnd)
+        let instance = createEventInstance(def.defId, dateSpan.range, dateSpan.forcedStartTzo, dateSpan.forcedEndTzo)
 
-        eventStore.defs[def.defId] = def
-        eventStore.instances[instance.instanceId] = instance
+        dest.defs[def.defId] = def
+        dest.instances[instance.instanceId] = instance
       }
     }
-  })
-}
-
-export function createEmptyEventStore(): EventStore {
-  return { defs: {}, instances: {} }
-}
-
-export function mergeStores(store0: EventStore, store1: EventStore): EventStore {
-  return {
-    defs: assignTo({}, store0.defs, store1.defs),
-    instances: assignTo({}, store0.instances, store1.instances)
   }
+
+  return dest
 }
 
 export function getRelatedEvents(eventStore: EventStore, instanceId: string): EventStore {
-  let newStore = { defs: {}, instances: {} } // TODO: better name
+  let dest = createEmptyEventStore()
   let eventInstance = eventStore.instances[instanceId]
   let eventDef = eventStore.defs[eventInstance.defId]
 
@@ -60,7 +74,7 @@ export function getRelatedEvents(eventStore: EventStore, instanceId: string): Ev
       let def = eventStore.defs[defId]
 
       if (def === eventDef || matchGroupId && matchGroupId === def.groupId) {
-        newStore.defs[defId] = def
+        dest.defs[defId] = def
       }
     }
 
@@ -71,10 +85,10 @@ export function getRelatedEvents(eventStore: EventStore, instanceId: string): Ev
         instance === eventInstance ||
         matchGroupId && matchGroupId === eventStore.defs[instance.defId].groupId
       ) {
-        newStore.instances[instanceId] = instance
+        dest.instances[instanceId] = instance
       }
     }
   }
 
-  return newStore
+  return dest
 }

+ 3 - 0
src/structs/event.ts

@@ -5,6 +5,9 @@ import UnzonedRange from '../models/UnzonedRange'
 import Calendar from '../Calendar'
 import { assignTo } from '../util/object'
 
+/*
+*/
+
 export type EventRenderingChoice = '' | 'background' | 'inverse-background' | 'none'
 
 export interface EventNonDateInput {

+ 18 - 92
src/structs/recurring-event.ts

@@ -1,116 +1,42 @@
-import { startOfDay, addDays } from '../datelib/marker'
-import { Duration, createDuration } from '../datelib/duration'
 import UnzonedRange from '../models/UnzonedRange'
 import Calendar from '../Calendar'
-import { arrayToHash } from '../util/object'
-import { refineProps } from '../util/misc'
 import { EventInput } from './event'
 
-// types
+/*
+*/
 
-export interface RecurringEventDateInfo {
+export interface RecurringEventDateSpans {
   isAllDay: boolean
   hasEnd: boolean
   ranges: UnzonedRange[]
 }
 
-export type RecurringExpandFunc = (
+export type RecurringExpander = (
   rawEvent: EventInput,
   range: UnzonedRange,
   calendar: Calendar,
-  leftoverProps: object
-) => RecurringEventDateInfo
+  leftovers: object
+) => RecurringEventDateSpans | null
 
-// vars
+let recurringExpanders: RecurringExpander[] = []
 
-const SIMPLE_RECURRING_PROPS = {
-  daysOfWeek: null,
-  startTime: createDuration,
-  endTime: createDuration
-}
-
-let recurringTypes: { [recurringType: string]: RecurringExpandFunc } = {}
-
-// expanding API
-
-export function expandRecurring(rawEvent: EventInput, range: UnzonedRange, calendar: Calendar, leftoverProps?: object): RecurringEventDateInfo {
-  for (let recurringType in recurringTypes) {
-    let expandFunc = recurringTypes[recurringType]
-    let dateInfo = expandFunc(rawEvent, range, calendar, leftoverProps || {})
+export function expandRecurring(
+  rawEvent: EventInput,
+  range: UnzonedRange,
+  calendar: Calendar,
+  leftovers?: object
+): RecurringEventDateSpans | null {
+  for (let expander of recurringExpanders) {
+    let dateInfo = expander(rawEvent, range, calendar, leftovers || {})
 
     if (dateInfo) {
       return dateInfo
     }
   }
-}
 
-export function registerRecurringType(recurringType: string, func: RecurringExpandFunc) {
-  recurringTypes[recurringType] = func
+  return null
 }
 
-// simple expanding
-
-registerRecurringType(
-  'simple',
-  function(rawEvent: EventInput, range: UnzonedRange, calendar: Calendar, leftoverProps: object): RecurringEventDateInfo {
-    if (
-      rawEvent.daysOfWeek ||
-      rawEvent.startTime != null ||
-      rawEvent.endTime != null
-    ) {
-      let refinedProps = refineProps(rawEvent, SIMPLE_RECURRING_PROPS, leftoverProps)
-
-      return {
-        isAllDay: !refinedProps.startTime && !refinedProps.endTime,
-        hasEnd: Boolean(refinedProps.endTime),
-        ranges: expandSimple(
-          refinedProps.daysOfWeek,
-          refinedProps.startTime,
-          refinedProps.endTime,
-          range,
-          calendar
-        )
-      }
-    }
-  }
-)
-
-function expandSimple(daysOfWeek: any, startTime: Duration, endTime: Duration, range: UnzonedRange, calendar: Calendar): UnzonedRange[] {
-  let dateEnv = calendar.dateEnv
-  let dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null
-  let dayMarker = startOfDay(range.start)
-  let endMarker = range.end
-  let instanceRanges = []
-
-  while (dayMarker < endMarker) {
-    let instanceStart
-    let instanceEnd
-
-    // if everyday, or this particular day-of-week
-    if (!dowHash || dowHash[dayMarker.getUTCDay()]) {
-
-      if (startTime) {
-        instanceStart = dateEnv.add(dayMarker, startTime)
-      } else {
-        instanceStart = dayMarker
-      }
-
-      if (endTime) {
-        instanceEnd = dateEnv.add(dayMarker, endTime)
-      } else {
-        instanceEnd = dateEnv.add(
-          instanceStart,
-          startTime ? // a timed event?
-            calendar.defaultTimedEventDuration :
-            calendar.defaultAllDayEventDuration
-        )
-      }
-
-      instanceRanges.push(new UnzonedRange(instanceStart, instanceEnd))
-    }
-
-    dayMarker = addDays(dayMarker, 1)
-  }
-
-  return instanceRanges
+export function registerRecurringExpander(expander: RecurringExpander) {
+  recurringExpanders.push(expander)
 }

+ 53 - 0
src/util/misc.ts

@@ -1,6 +1,10 @@
 import { applyStyle } from './dom-manip'
 import { computeHeightAndMargins } from './dom-geom'
 import { preventDefault } from './dom-event'
+import { DateMarker, startOfDay, addDays, diffDays, diffDayAndTime } from '../datelib/marker'
+import { Duration, asRoughMs, createDuration } from '../datelib/duration'
+import { DateEnv } from '../datelib/env'
+import UnzonedRange from '../models/UnzonedRange'
 
 
 /* FullCalendar-specific DOM Utilities
@@ -392,6 +396,10 @@ export function debounce(func, wait) {
 }
 
 
+/* Object Parsing
+----------------------------------------------------------------------------------------------------------------------*/
+
+
 export function refineProps(rawProps, processorFuncs, leftoverProps?): any {
   let refined = {}
 
@@ -415,3 +423,48 @@ export function refineProps(rawProps, processorFuncs, leftoverProps?): any {
 
   return refined
 }
+
+
+/* Date stuff that doesn't belong in datelib core
+----------------------------------------------------------------------------------------------------------------------*/
+
+
+export function computeAlignedDayRange(range: UnzonedRange): UnzonedRange {
+  let dayCnt = Math.floor(diffDays(range.start, range.end)) || 1
+  let start = startOfDay(range.start)
+  let end = addDays(start, dayCnt)
+  return new UnzonedRange(start, end)
+}
+
+
+export function computeVisibleDayRange(unzonedRange: UnzonedRange, nextDayThreshold: Duration): UnzonedRange {
+  let startDay: DateMarker = startOfDay(unzonedRange.start) // the beginning of the day the range starts
+  let end: DateMarker = unzonedRange.end
+  let endDay: DateMarker = startOfDay(end)
+  let endTimeMS: number = end.valueOf() - endDay.valueOf() // # of milliseconds into `endDay`
+
+  // If the end time is actually inclusively part of the next day and is equal to or
+  // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
+  // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
+  if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) {
+    endDay = addDays(endDay, 1)
+  }
+
+  // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
+  if (endDay <= startDay) {
+    endDay = addDays(startDay, 1)
+  }
+
+  return new UnzonedRange(startDay, endDay)
+}
+
+
+export function diffDates(date0: DateMarker, date1: DateMarker, dateEnv: DateEnv, largeUnit?: string) {
+  if (largeUnit === 'year') {
+    return createDuration(dateEnv.diffWholeYears(date0, date1), 'year')!
+  } else if (largeUnit === 'month') {
+    return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month')!
+  } else {
+    return diffDayAndTime(date0, date1) // returns a duration
+  }
+}

+ 1 - 1
src/util/object.ts

@@ -103,7 +103,7 @@ export function filterHash(hash, func) {
 }
 
 
-export function arrayToHash(a) {
+export function arrayToHash(a): { [key: string]: true } {
   let hash = {}
 
   for (let item of a) {