Adam Shaw 7 rokov pred
rodič
commit
691112947e

+ 26 - 14
src/Calendar.ts

@@ -19,17 +19,17 @@ import { Duration, createDuration } from './datelib/duration'
 import reduce from './reducers/main'
 import reduce from './reducers/main'
 import { parseDateSpan, DateSpanInput, DateSpan } from './structs/date-span'
 import { parseDateSpan, DateSpanInput, DateSpan } from './structs/date-span'
 import reselector from './util/reselector'
 import reselector from './util/reselector'
-import { assignTo, objectValues } from './util/object'
+import { assignTo } from './util/object'
 import { RenderForceFlags } from './component/Component'
 import { RenderForceFlags } from './component/Component'
 import browserContext from './common/browser-context'
 import browserContext from './common/browser-context'
 import { DateRangeInput, rangeContainsMarker } from './datelib/date-range'
 import { DateRangeInput, rangeContainsMarker } from './datelib/date-range'
 import { DateProfile } from './DateProfileGenerator'
 import { DateProfile } from './DateProfileGenerator'
 import { EventSourceInput, parseEventSource, EventSourceHash } from './structs/event-source'
 import { EventSourceInput, parseEventSource, EventSourceHash } from './structs/event-source'
-import { EventInput, EventDef, EventDefHash } from './structs/event'
+import { EventInput, EventDefHash, parseEvent } from './structs/event'
 import { CalendarState, Action } from './reducers/types'
 import { CalendarState, Action } from './reducers/types'
 import EventSourceApi from './api/EventSourceApi'
 import EventSourceApi from './api/EventSourceApi'
 import EventApi from './api/EventApi'
 import EventApi from './api/EventApi'
-import { parseEventStore, createEmptyEventStore, EventStore } from './structs/event-store'
+import { createEmptyEventStore, EventStore, eventTupleToStore } from './structs/event-store'
 import { computeEventDefUis, EventUiHash } from './component/event-rendering'
 import { computeEventDefUis, EventUiHash } from './component/event-rendering'
 import { BusinessHoursInput, parseBusinessHours } from './structs/business-hours'
 import { BusinessHoursInput, parseBusinessHours } from './structs/business-hours'
 
 
@@ -438,13 +438,18 @@ export default class Calendar {
 
 
 
 
   setOption(name: string, value: any) {
   setOption(name: string, value: any) {
+    let oldDateEnv = this.dateEnv
+
     this.optionsManager.add(name, value)
     this.optionsManager.add(name, value)
     this.handleOptions(this.optionsManager.computed)
     this.handleOptions(this.optionsManager.computed)
 
 
     if (name === 'height' || name === 'contentHeight' || name === 'aspectRatio') {
     if (name === 'height' || name === 'contentHeight' || name === 'aspectRatio') {
       this.updateViewSize(true) // isResize=true
       this.updateViewSize(true) // isResize=true
     } else if (name === 'timezone') {
     } else if (name === 'timezone') {
-      this.refetchEvents()
+      this.dispatch({
+        type: 'CHANGE_TIMEZONE',
+        oldDateEnv
+      })
     } else if (name === 'defaultDate') {
     } else if (name === 'defaultDate') {
       // can't change date this way. use gotoDate instead
       // can't change date this way. use gotoDate instead
     } else if (/^(event|select)(Overlap|Constraint|Allow)$/.test(name)) {
     } else if (/^(event|select)(Overlap|Constraint|Allow)$/.test(name)) {
@@ -1077,23 +1082,30 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
 
 
 
 
-  addEvent(eventInput: EventInput): EventApi | null {
-    let activeRange = this.state.dateProfile.activeRange
-    let subset = parseEventStore([ eventInput ], '', this, activeRange)
-    let def: EventDef = objectValues(subset.defs)[0]
+  addEvent(eventInput: EventInput, sourceInput?: any): EventApi | null {
+    let sourceId
+
+    if (sourceInput && sourceInput.sourceId !== undefined) {
+      sourceId = sourceInput.sourceId
+    } else if (typeof sourceInput === 'string') {
+      sourceId = sourceInput
+    } else {
+      sourceId = ''
+    }
+
+    let tuple = parseEvent(eventInput, sourceId, this)
 
 
-    if (def) {
+    if (tuple) {
 
 
-      // TODO: make this regenerate recurring events
       this.dispatch({
       this.dispatch({
-        type: 'ADD_EVENTS',
-        eventStore: subset
+        type: 'MERGE_EVENTS',
+        eventStore: eventTupleToStore(tuple)
       })
       })
 
 
       return new EventApi(
       return new EventApi(
         this,
         this,
-        def,
-        def.recurringDef ? null : objectValues(subset.instances)[0]
+        tuple.def,
+        tuple.def.recurringDef ? null : tuple.instance
       )
       )
     }
     }
 
 

+ 8 - 5
src/component/DateComponent.ts

@@ -8,7 +8,7 @@ import { DateMarker, DAY_IDS, addDays, startOfDay, diffWholeDays } from '../date
 import { Duration, createDuration } from '../datelib/duration'
 import { Duration, createDuration } from '../datelib/duration'
 import { DateSpan } from '../structs/date-span'
 import { DateSpan } from '../structs/date-span'
 import { EventRenderRange, sliceEventStore, computeEventDefUi, EventUiHash, computeEventDefUis } from '../component/event-rendering'
 import { EventRenderRange, sliceEventStore, computeEventDefUi, EventUiHash, computeEventDefUis } from '../component/event-rendering'
-import { EventStore, expandEventStoreInstances } from '../structs/event-store'
+import { EventStore, expandRecurring } from '../structs/event-store'
 import { DateEnv } from '../datelib/env'
 import { DateEnv } from '../datelib/env'
 import Theme from '../theme/Theme'
 import Theme from '../theme/Theme'
 import { EventInteractionUiState } from '../interactions/event-interaction-state'
 import { EventInteractionUiState } from '../interactions/event-interaction-state'
@@ -17,7 +17,7 @@ import browserContext from '../common/browser-context'
 import { Hit } from '../interactions/HitDragging'
 import { Hit } from '../interactions/HitDragging'
 import { DateRange, rangeContainsMarker } from '../datelib/date-range'
 import { DateRange, rangeContainsMarker } from '../datelib/date-range'
 import EventApi from '../api/EventApi'
 import EventApi from '../api/EventApi'
-import { parseEventDef, createEventInstance } from '../structs/event'
+import { createEventInstance, parseEventDef } from '../structs/event'
 
 
 
 
 export interface DateComponentRenderState {
 export interface DateComponentRenderState {
@@ -527,8 +527,7 @@ export default abstract class DateComponent extends Component {
 
 
   renderBusinessHours(businessHours: EventStore) {
   renderBusinessHours(businessHours: EventStore) {
     if (this.slicingType) { // can use eventStoreToRanges?
     if (this.slicingType) { // can use eventStoreToRanges?
-      let expandedInstances = expandEventStoreInstances(businessHours, this.dateProfile.activeRange, this.getCalendar())
-      let expandedStore: EventStore = { defs: businessHours.defs, instances: expandedInstances }
+      let expandedStore = expandRecurring(businessHours, this.dateProfile.activeRange, this.getCalendar())
 
 
       this.renderBusinessHourRanges(
       this.renderBusinessHourRanges(
         this.eventStoreToRanges(
         this.eventStoreToRanges(
@@ -913,7 +912,11 @@ export default abstract class DateComponent extends Component {
 
 
       // fabricate an eventRange. important for helper
       // fabricate an eventRange. important for helper
       // TODO: make a separate utility for this?
       // TODO: make a separate utility for this?
-      let def = parseEventDef({ editable: false }, '', selection.isAllDay, true)
+      let def = parseEventDef({
+        editable: false,
+        isAllDay: selection.isAllDay,
+        hasEnd: true
+      }, '')
       let eventRange = {
       let eventRange = {
         def,
         def,
         ui: computeEventDefUi(def, {}, {}),
         ui: computeEventDefUi(def, {}, {}),

+ 10 - 7
src/component/renderers/EventRenderer.ts

@@ -328,17 +328,20 @@ export default class EventRenderer {
 
 
   // A cmp function for determining which segments should take visual priority
   // A cmp function for determining which segments should take visual priority
   compareEventSegs(seg1: Seg, seg2: Seg) {
   compareEventSegs(seg1: Seg, seg2: Seg) {
-    let eventDef1 = seg1.eventRange.eventDef
-    let eventDef2 = seg2.eventRange.eventDef
-    let r1 = seg1.eventRange.range
-    let r2 = seg2.eventRange.range
+    let eventDef1 = seg1.eventRange.def
+    let eventDef2 = seg2.eventRange.def
+    let r1 = seg1.eventRange.instance.range
+    let r2 = seg2.eventRange.instance.range
+
+    let eventApi1 = new EventApi(this.view.calendar, eventDef1)
+    let eventApi2 = new EventApi(this.view.calendar, eventDef2)
 
 
     return r1.start.valueOf() - r2.start.valueOf() || // earlier events go first
     return r1.start.valueOf() - r2.start.valueOf() || // earlier events go first
       (r2.end.valueOf() - r2.start.valueOf()) - (r1.end.valueOf() - r1.start.valueOf()) || // tie? longer events go first
       (r2.end.valueOf() - r2.start.valueOf()) - (r1.end.valueOf() - r1.start.valueOf()) || // tie? longer events go first
-      Number(eventDef2.isAllDay) - Number(eventDef1.isAllDay) || // tie? put all-day events first
+      Number(eventDef1.isAllDay) - Number(eventDef2.isAllDay) || // tie? put all-day events first
       compareByFieldSpecs(
       compareByFieldSpecs(
-        eventDef1,
-        eventDef2,
+        eventApi1,
+        eventApi2,
         this.view.eventOrderSpecs,
         this.view.eventOrderSpecs,
         eventDef1.extendedProps,
         eventDef1.extendedProps,
         eventDef2.extendedProps
         eventDef2.extendedProps

+ 4 - 2
src/datelib/env.ts

@@ -410,11 +410,13 @@ export class DateEnv {
 
 
   // Conversion
   // Conversion
 
 
-  toDate(m: DateMarker): Date {
+  toDate(m: DateMarker, forcedTzo?: number): Date {
     if (this.timeZone === 'local') {
     if (this.timeZone === 'local') {
       return arrayToLocalDate(dateToUtcArray(m))
       return arrayToLocalDate(dateToUtcArray(m))
-    } else if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) {
+    } else if (this.timeZone === 'UTC') {
       return new Date(m.valueOf()) // make sure it's a copy
       return new Date(m.valueOf()) // make sure it's a copy
+    } else if (!this.namedTimeZoneImpl) {
+      return new Date(m.valueOf() - (forcedTzo || 0))
     } else {
     } else {
       return new Date(
       return new Date(
         m.valueOf() -
         m.valueOf() -

+ 1 - 1
src/event-sources/array-event-source.ts

@@ -3,7 +3,7 @@ import { EventInput } from '../structs/event'
 
 
 registerEventSourceDef({
 registerEventSourceDef({
 
 
-  singleFetch: true, // can we please NOT store the raw events internally then???
+  ignoreRange: true,
 
 
   parseMeta(raw: any): EventInput[] | null {
   parseMeta(raw: any): EventInput[] | null {
     if (Array.isArray(raw)) { // short form
     if (Array.isArray(raw)) { // short form

+ 11 - 26
src/interactions-external/ExternalElementDragging.ts

@@ -2,8 +2,8 @@ import ElementDragging from '../dnd/ElementDragging'
 import HitDragging, { Hit } from '../interactions/HitDragging'
 import HitDragging, { Hit } from '../interactions/HitDragging'
 import browserContext from '../common/browser-context'
 import browserContext from '../common/browser-context'
 import { PointerDragEvent } from '../dnd/PointerDragging'
 import { PointerDragEvent } from '../dnd/PointerDragging'
-import { parseEventDef, createEventInstance, EventDef, EventInstance } from '../structs/event'
-import { EventStore, createEmptyEventStore } from '../structs/event-store'
+import { parseEventDef, createEventInstance, EventTuple } from '../structs/event'
+import { createEmptyEventStore, eventTupleToStore } from '../structs/event-store'
 import * as externalHooks from '../exports'
 import * as externalHooks from '../exports'
 import { DateSpan } from '../structs/date-span'
 import { DateSpan } from '../structs/date-span'
 import Calendar from '../Calendar'
 import Calendar from '../Calendar'
@@ -12,11 +12,6 @@ import { DragMetaInput, DragMeta, parseDragMeta } from '../structs/drag-meta'
 import EventApi from '../api/EventApi'
 import EventApi from '../api/EventApi'
 import { elementMatches } from '../util/dom-manip'
 import { elementMatches } from '../util/dom-manip'
 
 
-export interface EventRes {
-  def: EventDef
-  instance: EventInstance
-}
-
 /*
 /*
 Given an already instantiated draggable object for one-or-more elements,
 Given an already instantiated draggable object for one-or-more elements,
 Interprets any dragging as an attempt to drag an events that lives outside
 Interprets any dragging as an attempt to drag an events that lives outside
@@ -26,7 +21,7 @@ export default class ExternalElementDragging {
 
 
   hitDragging: HitDragging
   hitDragging: HitDragging
   receivingCalendar: Calendar | null = null
   receivingCalendar: Calendar | null = null
-  droppableEvent: EventRes | null = null
+  droppableEvent: EventTuple | null = null
   explicitDragMeta: DragMeta | null = null
   explicitDragMeta: DragMeta | null = null
   dragMeta: DragMeta | null = null
   dragMeta: DragMeta | null = null
 
 
@@ -52,7 +47,7 @@ export default class ExternalElementDragging {
   handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => {
   handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => {
     let { dragging } = this.hitDragging
     let { dragging } = this.hitDragging
     let receivingCalendar: Calendar | null = null
     let receivingCalendar: Calendar | null = null
-    let droppableEvent: EventRes | null = null
+    let droppableEvent: EventTuple | null = null
 
 
     if (hit) {
     if (hit) {
       receivingCalendar = hit.component.getCalendar()
       receivingCalendar = hit.component.getCalendar()
@@ -67,7 +62,7 @@ export default class ExternalElementDragging {
     }
     }
 
 
     // TODO: always store as event-store?
     // TODO: always store as event-store?
-    let droppableEventStore = droppableEvent ? toEventStore(droppableEvent) : createEmptyEventStore()
+    let droppableEventStore = droppableEvent ? eventTupleToStore(droppableEvent) : createEmptyEventStore()
 
 
     this.displayDrag(receivingCalendar, {
     this.displayDrag(receivingCalendar, {
       affectedEvents: createEmptyEventStore(),
       affectedEvents: createEmptyEventStore(),
@@ -112,8 +107,8 @@ export default class ExternalElementDragging {
 
 
       if (dragMeta.create) {
       if (dragMeta.create) {
         receivingCalendar.dispatch({
         receivingCalendar.dispatch({
-          type: 'ADD_EVENTS',
-          eventStore: toEventStore(droppableEvent)
+          type: 'MERGE_EVENTS',
+          eventStore: eventTupleToStore(droppableEvent)
         })
         })
 
 
         // signal that an external event landed
         // signal that an external event landed
@@ -170,13 +165,10 @@ export default class ExternalElementDragging {
 // Utils for computing event store from the DragMeta
 // Utils for computing event store from the DragMeta
 // ----------------------------------------------------------------------------------------------------
 // ----------------------------------------------------------------------------------------------------
 
 
-function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, calendar: Calendar): EventRes {
-  let def = parseEventDef(
-    dragMeta.leftoverProps || {},
-    '',
-    dateSpan.isAllDay,
-    Boolean(dragMeta.duration) // hasEnd
-  )
+function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, calendar: Calendar): EventTuple {
+  let def = parseEventDef(dragMeta.leftoverProps || {}, '')
+  def.isAllDay = dateSpan.isAllDay
+  def.hasEnd = Boolean(dragMeta.duration)
 
 
   let start = dateSpan.range.start
   let start = dateSpan.range.start
 
 
@@ -195,13 +187,6 @@ function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, calenda
   return { def, instance }
   return { def, instance }
 }
 }
 
 
-function toEventStore(res: EventRes): EventStore {
-  return {
-    defs: { [res.def.defId]: res.def },
-    instances: { [res.instance.instanceId]: res.instance }
-  }
-}
-
 // Utils for extracting data from element
 // Utils for extracting data from element
 // ----------------------------------------------------------------------------------------------------
 // ----------------------------------------------------------------------------------------------------
 
 

+ 3 - 3
src/interactions/EventDragging.ts

@@ -236,7 +236,7 @@ export default class EventDragging { // TODO: rename to EventSelectingAndDraggin
         if (receivingCalendar === initialCalendar) {
         if (receivingCalendar === initialCalendar) {
 
 
           initialCalendar.dispatch({
           initialCalendar.dispatch({
-            type: 'ADD_EVENTS', // will merge
+            type: 'MERGE_EVENTS',
             eventStore: mutatedRelatedEvents
             eventStore: mutatedRelatedEvents
           })
           })
 
 
@@ -252,7 +252,7 @@ export default class EventDragging { // TODO: rename to EventSelectingAndDraggin
               ),
               ),
               revert: function() {
               revert: function() {
                 initialCalendar.dispatch({
                 initialCalendar.dispatch({
-                  type: 'ADD_EVENTS', // will merge
+                  type: 'MERGE_EVENTS',
                   eventStore: relatedEvents
                   eventStore: relatedEvents
                 })
                 })
               },
               },
@@ -269,7 +269,7 @@ export default class EventDragging { // TODO: rename to EventSelectingAndDraggin
             instances: this.mutatedRelatedEvents!.instances
             instances: this.mutatedRelatedEvents!.instances
           })
           })
           receivingCalendar.dispatch({
           receivingCalendar.dispatch({
-            type: 'ADD_EVENTS',
+            type: 'MERGE_EVENTS',
             eventStore: this.mutatedRelatedEvents!
             eventStore: this.mutatedRelatedEvents!
           })
           })
         }
         }

+ 2 - 2
src/interactions/EventResizing.ts

@@ -158,7 +158,7 @@ export default class EventDragging {
 
 
     if (this.validMutation) {
     if (this.validMutation) {
       calendar.dispatch({
       calendar.dispatch({
-        type: 'ADD_EVENTS', // will merge
+        type: 'MERGE_EVENTS',
         eventStore: mutatedRelatedEvents
         eventStore: mutatedRelatedEvents
       })
       })
 
 
@@ -175,7 +175,7 @@ export default class EventDragging {
           ),
           ),
           revert: function() {
           revert: function() {
             calendar.dispatch({
             calendar.dispatch({
-              type: 'ADD_EVENTS', // will merge
+              type: 'MERGE_EVENTS',
               eventStore: relatedEvents
               eventStore: relatedEvents
             })
             })
           },
           },

+ 59 - 54
src/reducers/eventSources.ts

@@ -1,4 +1,4 @@
-import { EventSource, EventSourceHash, getEventSourceDef, EventSourceDef } from '../structs/event-source'
+import { EventSource, EventSourceHash, getEventSourceDef, doesSourceNeedRange } from '../structs/event-source'
 import Calendar from '../Calendar'
 import Calendar from '../Calendar'
 import { arrayToHash, assignTo, filterHash } from '../util/object'
 import { arrayToHash, assignTo, filterHash } from '../util/object'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
@@ -6,99 +6,105 @@ import { warn } from '../util/misc'
 import { DateProfile } from '../DateProfileGenerator'
 import { DateProfile } from '../DateProfileGenerator'
 import { Action, SimpleError } from './types'
 import { Action, SimpleError } from './types'
 
 
-export default function(eventSourceHash: EventSourceHash, action: Action, dateProfile: DateProfile | null, calendar: Calendar): EventSourceHash {
+export default function(eventSources: EventSourceHash, action: Action, dateProfile: DateProfile | null, calendar: Calendar): EventSourceHash {
   switch (action.type) {
   switch (action.type) {
 
 
     case 'ADD_EVENT_SOURCES': // already parsed
     case 'ADD_EVENT_SOURCES': // already parsed
-      return addSources(eventSourceHash, action.sources, dateProfile, calendar)
+      return addSources(eventSources, action.sources, dateProfile ? dateProfile.activeRange : null, calendar)
 
 
     case 'REMOVE_EVENT_SOURCE':
     case 'REMOVE_EVENT_SOURCE':
-      return removeSource(eventSourceHash, action.sourceId)
+      return removeSource(eventSources, action.sourceId)
 
 
     case 'SET_DATE_PROFILE':
     case 'SET_DATE_PROFILE':
-      return fetchDirtySources(eventSourceHash, action.dateProfile, calendar)
+      return fetchDirtySources(eventSources, action.dateProfile.activeRange, calendar)
 
 
     case 'FETCH_EVENT_SOURCES':
     case 'FETCH_EVENT_SOURCES':
-      if (dateProfile) {
-        return fetchSourcesByIds(eventSourceHash, action.sourceIds || null, dateProfile, calendar)
-      } else {
-        return eventSourceHash // can't fetch if don't know the framing range
-      }
+    case 'CHANGE_TIMEZONE':
+      return fetchSourcesByIds(
+        eventSources,
+        (action as any).sourceIds ?
+          arrayToHash((action as any).sourceIds) :
+          excludeStaticSources(eventSources),
+        dateProfile ? dateProfile.activeRange : null,
+        calendar
+      )
 
 
     case 'RECEIVE_EVENTS':
     case 'RECEIVE_EVENTS':
     case 'RECEIVE_EVENT_ERROR':
     case 'RECEIVE_EVENT_ERROR':
-      return receiveResponse(eventSourceHash, action.sourceId, action.fetchId, action.fetchRange)
+      return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange)
 
 
     case 'REMOVE_ALL_EVENT_SOURCES':
     case 'REMOVE_ALL_EVENT_SOURCES':
       return {}
       return {}
 
 
     default:
     default:
-      return eventSourceHash
+      return eventSources
   }
   }
 }
 }
 
 
+
 let uid = 0
 let uid = 0
 
 
-function addSources(eventSourceHash: EventSourceHash, sources: EventSource[], dateProfile: DateProfile | null, calendar: Calendar): EventSourceHash {
+
+function addSources(eventSourceHash: EventSourceHash, sources: EventSource[], fetchRange: DateRange | null, calendar: Calendar): EventSourceHash {
   let hash: EventSourceHash = {}
   let hash: EventSourceHash = {}
 
 
   for (let source of sources) {
   for (let source of sources) {
     hash[source.sourceId] = source
     hash[source.sourceId] = source
   }
   }
 
 
-  if (dateProfile) {
-    hash = fetchDirtySources(hash, dateProfile, calendar)
-  }
+  hash = fetchDirtySources(hash, fetchRange, calendar)
 
 
   return assignTo({}, eventSourceHash, hash)
   return assignTo({}, eventSourceHash, hash)
 }
 }
 
 
+
 function removeSource(eventSourceHash: EventSourceHash, sourceId: string): EventSourceHash {
 function removeSource(eventSourceHash: EventSourceHash, sourceId: string): EventSourceHash {
   return filterHash(eventSourceHash, function(eventSource: EventSource) {
   return filterHash(eventSourceHash, function(eventSource: EventSource) {
     return eventSource.sourceId !== sourceId
     return eventSource.sourceId !== sourceId
   })
   })
 }
 }
 
 
-function fetchDirtySources(sourceHash: EventSourceHash, dateProfile: DateProfile, calendar: Calendar): EventSourceHash {
-  let activeRange = dateProfile.activeRange
-  let dirtySourceIds = []
 
 
+function fetchDirtySources(sourceHash: EventSourceHash, fetchRange: DateRange | null, calendar: Calendar): EventSourceHash {
+  return fetchSourcesByIds(
+    sourceHash,
+    filterHash(sourceHash, function(eventSource) {
+      return isSourceDirty(eventSource, fetchRange, calendar)
+    }),
+    fetchRange,
+    calendar
+  )
+}
 
 
-  for (let sourceId in sourceHash) {
-    let eventSource = sourceHash[sourceId]
 
 
-    if (
-      !calendar.opt('lazyFetching') ||
-      !eventSource.fetchRange ||
-      activeRange.start < eventSource.fetchRange.start ||
-      activeRange.end > eventSource.fetchRange.end
-    ) {
-      dirtySourceIds.push(eventSource.sourceId)
-    }
-  }
+function isSourceDirty(eventSource: EventSource, fetchRange: DateRange | null, calendar: Calendar) {
 
 
-  if (dirtySourceIds.length) {
-    sourceHash = fetchSourcesByIds(sourceHash, dirtySourceIds, dateProfile, calendar)
+  if (!doesSourceNeedRange(eventSource)) {
+    return !eventSource.latestFetchId
+  } else if (fetchRange) {
+    return !calendar.opt('lazyFetching') ||
+      !eventSource.fetchRange ||
+      fetchRange.start < eventSource.fetchRange.start ||
+      fetchRange.end > eventSource.fetchRange.end
   }
   }
 
 
-  return sourceHash
+  return false
 }
 }
 
 
+
 function fetchSourcesByIds(
 function fetchSourcesByIds(
   prevSources: EventSourceHash,
   prevSources: EventSourceHash,
-  sourceIds: string[] | null,
-  dateProfile: DateProfile,
+  sourceIdHash: { [sourceId: string]: any },
+  fetchRange: DateRange | null,
   calendar: Calendar
   calendar: Calendar
 ): EventSourceHash {
 ): EventSourceHash {
-  let sourceIdHash = sourceIds ? arrayToHash(sourceIds) : null
   let nextSources: EventSourceHash = {}
   let nextSources: EventSourceHash = {}
-  let activeRange = dateProfile.activeRange
 
 
   for (let sourceId in prevSources) {
   for (let sourceId in prevSources) {
     let source = prevSources[sourceId]
     let source = prevSources[sourceId]
 
 
-    if (!sourceIdHash || sourceIdHash[sourceId]) {
-      nextSources[sourceId] = fetchSource(source, activeRange, calendar)
+    if (sourceIdHash[sourceId]) {
+      nextSources[sourceId] = fetchSource(source, fetchRange, calendar)
     } else {
     } else {
       nextSources[sourceId] = source
       nextSources[sourceId] = source
     }
     }
@@ -107,24 +113,16 @@ function fetchSourcesByIds(
   return nextSources
   return nextSources
 }
 }
 
 
-function fetchSource(eventSource: EventSource, range: DateRange, calendar: Calendar) {
-  let sourceDef = getEventSourceDef(eventSource.sourceDefId)
 
 
-  if (sourceDef.singleFetch && eventSource.fetchRange) {
-    return eventSource
-  } else {
-    return fetchSourceAsync(eventSource, sourceDef, range, calendar)
-  }
-}
-
-function fetchSourceAsync(eventSource: EventSource, sourceDef: EventSourceDef, range: DateRange, calendar: Calendar) {
+function fetchSource(eventSource: EventSource, fetchRange: DateRange | null, calendar: Calendar) {
+  let sourceDef = getEventSourceDef(eventSource.sourceDefId)
   let fetchId = String(uid++)
   let fetchId = String(uid++)
 
 
   sourceDef.fetch(
   sourceDef.fetch(
     {
     {
       eventSource,
       eventSource,
       calendar,
       calendar,
-      range
+      range: fetchRange
     },
     },
     function(rawEvents) {
     function(rawEvents) {
 
 
@@ -136,7 +134,7 @@ function fetchSourceAsync(eventSource: EventSource, sourceDef: EventSourceDef, r
         type: 'RECEIVE_EVENTS',
         type: 'RECEIVE_EVENTS',
         sourceId: eventSource.sourceId,
         sourceId: eventSource.sourceId,
         fetchId,
         fetchId,
-        fetchRange: range,
+        fetchRange,
         rawEvents
         rawEvents
       })
       })
     },
     },
@@ -153,20 +151,19 @@ function fetchSourceAsync(eventSource: EventSource, sourceDef: EventSourceDef, r
         type: 'RECEIVE_EVENT_ERROR',
         type: 'RECEIVE_EVENT_ERROR',
         sourceId: eventSource.sourceId,
         sourceId: eventSource.sourceId,
         fetchId,
         fetchId,
-        fetchRange: range,
+        fetchRange,
         error
         error
       })
       })
     }
     }
   )
   )
 
 
-  // TODO: if singleFetch, remove the meta at this point?
-
   return assignTo({}, eventSource, {
   return assignTo({}, eventSource, {
     isFetching: true,
     isFetching: true,
     latestFetchId: fetchId
     latestFetchId: fetchId
   })
   })
 }
 }
 
 
+
 function receiveResponse(sourceHash: EventSourceHash, sourceId: string, fetchId: string, fetchRange: DateRange) {
 function receiveResponse(sourceHash: EventSourceHash, sourceId: string, fetchId: string, fetchRange: DateRange) {
   let eventSource: EventSource = sourceHash[sourceId]
   let eventSource: EventSource = sourceHash[sourceId]
 
 
@@ -182,6 +179,7 @@ function receiveResponse(sourceHash: EventSourceHash, sourceId: string, fetchId:
   return sourceHash
   return sourceHash
 }
 }
 
 
+
 function normalizeError(input: any): SimpleError {
 function normalizeError(input: any): SimpleError {
   if (typeof input === 'string') {
   if (typeof input === 'string') {
     return { message: input }
     return { message: input }
@@ -189,3 +187,10 @@ function normalizeError(input: any): SimpleError {
     return input || {}
     return input || {}
   }
   }
 }
 }
+
+
+function excludeStaticSources(eventSources: EventSourceHash): EventSourceHash {
+  return filterHash(eventSources, function(eventSource) {
+    return doesSourceNeedRange(eventSource)
+  })
+}

+ 79 - 65
src/reducers/eventStore.ts

@@ -1,38 +1,58 @@
 import Calendar from '../Calendar'
 import Calendar from '../Calendar'
-import { filterHash } from '../util/object'
+import { filterHash, assignTo, mapHash } from '../util/object'
 import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation'
 import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation'
 import { EventDef, EventInstance, EventInput, EventInstanceHash } from '../structs/event'
 import { EventDef, EventInstance, EventInput, EventInstanceHash } from '../structs/event'
 import {
 import {
   EventStore,
   EventStore,
-  parseEventStore,
   mergeEventStores,
   mergeEventStores,
   getRelatedEvents,
   getRelatedEvents,
   createEmptyEventStore,
   createEmptyEventStore,
-  expandEventDefInstances,
-  filterEventStoreDefs
+  filterEventStoreDefs,
+  transformRawEvents,
+  parseEvents,
+  expandRecurring
 } from '../structs/event-store'
 } from '../structs/event-store'
 import { Action } from './types'
 import { Action } from './types'
-import { EventSourceHash, EventSource, getEventSourceDef } from '../structs/event-source'
+import { EventSourceHash, EventSource } from '../structs/event-source'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
+import { DateProfile } from '../DateProfileGenerator'
+import { DateEnv } from '../datelib/env'
 
 
-// how to let user modify recurring def AFTER?
 
 
-export default function(eventStore: EventStore, action: Action, sourceHash: EventSourceHash, calendar: Calendar): EventStore {
+export default function(eventStore: EventStore, action: Action, eventSources: EventSourceHash, dateProfile: DateProfile, calendar: Calendar): EventStore {
   switch(action.type) {
   switch(action.type) {
 
 
-    case 'ADD_EVENTS': // already parsed
-      return mergeEventStores(eventStore, action.eventStore)
-
     case 'RECEIVE_EVENTS': // raw
     case 'RECEIVE_EVENTS': // raw
-      return receiveEvents(
+      return receiveRawEvents(
         eventStore,
         eventStore,
-        sourceHash[action.sourceId],
+        eventSources[action.sourceId],
         action.fetchId,
         action.fetchId,
         action.fetchRange,
         action.fetchRange,
         action.rawEvents,
         action.rawEvents,
         calendar
         calendar
       )
       )
 
 
+    case 'ADD_EVENTS': // already parsed, but not expanded
+      return addEvent(
+        eventStore,
+        action.eventStore, // new ones
+        dateProfile ? dateProfile.activeRange : null,
+        calendar
+      )
+
+    case 'MERGE_EVENTS': // already parsed and expanded
+      return mergeEventStores(eventStore, action.eventStore)
+
+    case 'SET_DATE_PROFILE':
+      if (dateProfile) {
+        return expandRecurring(eventStore, dateProfile.activeRange, calendar)
+      } else {
+        return eventStore
+      }
+
+    case 'CHANGE_TIMEZONE':
+      return rezoneDates(eventStore, action.oldDateEnv, calendar.dateEnv)
+
     case 'MUTATE_EVENTS':
     case 'MUTATE_EVENTS':
       return applyMutationToRelated(eventStore, action.instanceId, action.mutation, calendar)
       return applyMutationToRelated(eventStore, action.instanceId, action.mutation, calendar)
 
 
@@ -55,19 +75,17 @@ export default function(eventStore: EventStore, action: Action, sourceHash: Even
     case 'REMOVE_ALL_EVENTS':
     case 'REMOVE_ALL_EVENTS':
       return createEmptyEventStore()
       return createEmptyEventStore()
 
 
-    case 'SET_DATE_PROFILE':
-      return expandStaticEventDefs(eventStore, sourceHash, action.dateProfile.activeRange, calendar)
-
     default:
     default:
       return eventStore
       return eventStore
   }
   }
 }
 }
 
 
-function receiveEvents(
+
+function receiveRawEvents(
   eventStore: EventStore,
   eventStore: EventStore,
   eventSource: EventSource,
   eventSource: EventSource,
   fetchId: string,
   fetchId: string,
-  fetchRange: DateRange,
+  fetchRange: DateRange | null,
   rawEvents: EventInput[],
   rawEvents: EventInput[],
   calendar: Calendar
   calendar: Calendar
 ): EventStore {
 ): EventStore {
@@ -76,80 +94,76 @@ function receiveEvents(
     eventSource && // not already removed
     eventSource && // not already removed
     fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources
     fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources
   ) {
   ) {
+    rawEvents = transformRawEvents(rawEvents, eventSource.eventDataTransform)
+    let subset = parseEvents(rawEvents, eventSource.sourceId, calendar)
 
 
-    rawEvents = runEventDataTransform(rawEvents, eventSource.eventDataTransform)
-    rawEvents = runEventDataTransform(rawEvents, calendar.opt('eventDataTransform'))
+    if (fetchRange) {
+      subset = expandRecurring(subset, fetchRange, calendar)
+    }
 
 
-    return parseEventStore(
-      rawEvents,
-      eventSource.sourceId,
-      calendar,
-      fetchRange,
-      excludeEventsBySourceId(eventStore, eventSource.sourceId)
+    return mergeEventStores(
+      excludeEventsBySourceId(eventStore, eventSource.sourceId),
+      subset
     )
     )
   }
   }
 
 
   return eventStore
   return eventStore
 }
 }
 
 
-function runEventDataTransform(rawEvents, func) {
-  let refinedEvents
-
-  if (!func) {
-    refinedEvents = rawEvents
-  } else {
-    refinedEvents = []
 
 
-    for (let rawEvent of rawEvents) {
-      let refinedEvent = func(rawEvent)
+function addEvent(eventStore: EventStore, subset: EventStore, expandRange: DateRange | null, calendar: Calendar): EventStore {
 
 
-      if (refinedEvent) {
-        refinedEvents.push(refinedEvent)
-      } else if (refinedEvent == null) {
-        refinedEvents.push(rawEvent)
-      } // if a different falsy value, do nothing
-    }
+  if (expandRange) {
+    subset = expandRecurring(subset, expandRange, calendar)
   }
   }
 
 
-  return refinedEvents
+  return mergeEventStores(eventStore, subset)
 }
 }
 
 
-function excludeInstances(eventStore: EventStore, removals: EventInstanceHash): EventStore {
-  return {
-    defs: eventStore.defs,
-    instances: filterHash(eventStore.instances, function(instance: EventInstance) {
-      return !removals[instance.instanceId]
-    })
-  }
-}
 
 
-function excludeEventsBySourceId(eventStore, sourceId) {
-  return filterEventStoreDefs(eventStore, function(eventDef: EventDef) {
-    return eventDef.sourceId !== sourceId
+function rezoneDates(eventStore: EventStore, oldDateEnv: DateEnv, newDateEnv: DateEnv): EventStore {
+  let defs = eventStore.defs
+
+  let instances = mapHash(eventStore.instances, function(instance: EventInstance): EventInstance {
+    let def = defs[instance.defId]
+
+    if (def.isAllDay || def.recurringDef) {
+      return instance // isn't dependent on timezone
+    } else {
+      return assignTo({}, instance, {
+        range: {
+          start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)),
+          end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo))
+        },
+        forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo,
+        forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo
+      })
+    }
   })
   })
+
+  return { defs, instances }
 }
 }
 
 
+
 function applyMutationToRelated(eventStore: EventStore, instanceId: string, mutation: EventMutation, calendar: Calendar): EventStore {
 function applyMutationToRelated(eventStore: EventStore, instanceId: string, mutation: EventMutation, calendar: Calendar): EventStore {
   let related = getRelatedEvents(eventStore, instanceId)
   let related = getRelatedEvents(eventStore, instanceId)
   related = applyMutationToEventStore(related, mutation, calendar)
   related = applyMutationToEventStore(related, mutation, calendar)
   return mergeEventStores(eventStore, related)
   return mergeEventStores(eventStore, related)
 }
 }
 
 
-function expandStaticEventDefs(eventStore: EventStore, eventSources: EventSourceHash, framingRange: DateRange, calendar: Calendar): EventStore {
-  let staticSources = filterHash(eventSources, function(eventSource: EventSource) { // sources that won't change
-    return eventSource.fetchRange && getEventSourceDef(eventSource.sourceDefId).singleFetch // only needs one fetch, and already got it
-  }) as EventSourceHash
 
 
-  let defs = eventStore.defs
-  let instances = filterHash(eventStore.instances, function(eventInstance) {
-    let def = defs[eventInstance.defId]
-
-    return !def.recurringDef || !staticSources[def.sourceId]
+function excludeEventsBySourceId(eventStore, sourceId) {
+  return filterEventStoreDefs(eventStore, function(eventDef: EventDef) {
+    return eventDef.sourceId !== sourceId
   })
   })
+}
 
 
-  for (let defId in defs) {
-    expandEventDefInstances(defs[defId], framingRange, calendar, instances)
-  }
 
 
-  return { defs, instances }
+function excludeInstances(eventStore: EventStore, removals: EventInstanceHash): EventStore {
+  return {
+    defs: eventStore.defs,
+    instances: filterHash(eventStore.instances, function(instance: EventInstance) {
+      return !removals[instance.instanceId]
+    })
+  }
 }
 }

+ 1 - 1
src/reducers/main.ts

@@ -17,7 +17,7 @@ export default function(state: CalendarState, action: Action, calendar: Calendar
   return {
   return {
     dateProfile,
     dateProfile,
     eventSources,
     eventSources,
-    eventStore: reduceEventStore(state.eventStore, action, eventSources, calendar),
+    eventStore: reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendar),
     eventUis: state.eventUis, // TODO: should really be internal state
     eventUis: state.eventUis, // TODO: should really be internal state
     businessHours: state.businessHours,
     businessHours: state.businessHours,
     dateSelection: reduceDateSelection(state.dateSelection, action),
     dateSelection: reduceDateSelection(state.dateSelection, action),

+ 8 - 4
src/reducers/types.ts

@@ -7,6 +7,7 @@ import { EventSource, EventSourceHash } from '../structs/event-source'
 import { DateProfile } from '../DateProfileGenerator'
 import { DateProfile } from '../DateProfileGenerator'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { DateSpan } from '../structs/date-span'
 import { DateSpan } from '../structs/date-span'
+import { DateEnv } from '../datelib/env'
 
 
 export interface CalendarState extends DateComponentRenderState {
 export interface CalendarState extends DateComponentRenderState {
   eventSources: EventSourceHash
   eventSources: EventSourceHash
@@ -36,13 +37,16 @@ export type Action =
 
 
   { type: 'ADD_EVENT_SOURCES', sources: EventSource[] } |
   { type: 'ADD_EVENT_SOURCES', sources: EventSource[] } |
   { type: 'REMOVE_EVENT_SOURCE', sourceId: string } |
   { type: 'REMOVE_EVENT_SOURCE', sourceId: string } |
-  { type: 'FETCH_EVENT_SOURCES', sourceIds?: string[] } | // if no sourceIds, fetch all
   { type: 'REMOVE_ALL_EVENT_SOURCES' } |
   { type: 'REMOVE_ALL_EVENT_SOURCES' } |
 
 
-  { type: 'RECEIVE_EVENTS', sourceId: string, fetchId: string, fetchRange: DateRange, rawEvents: EventInput[] } |
-  { type: 'RECEIVE_EVENT_ERROR', sourceId: string, fetchId: string, fetchRange: DateRange, error: SimpleError } |
+  { type: 'FETCH_EVENT_SOURCES', sourceIds?: string[] } | // if no sourceIds, fetch all
+  { type: 'CHANGE_TIMEZONE', oldDateEnv: DateEnv } |
+
+  { type: 'RECEIVE_EVENTS', sourceId: string, fetchId: string, fetchRange: DateRange | null, rawEvents: EventInput[] } |
+  { type: 'RECEIVE_EVENT_ERROR', sourceId: string, fetchId: string, fetchRange: DateRange | null, error: SimpleError } |
 
 
-  { type: 'ADD_EVENTS', eventStore: EventStore } | // rename to MERGE_EVENTS?
+  { type: 'ADD_EVENTS', eventStore: EventStore } |
+  { type: 'MERGE_EVENTS', eventStore: EventStore } |
   { type: 'MUTATE_EVENTS', instanceId: string, mutation: EventMutation } |
   { type: 'MUTATE_EVENTS', instanceId: string, mutation: EventMutation } |
   { type: 'REMOVE_EVENT_DEF', defId: string } |
   { type: 'REMOVE_EVENT_DEF', defId: string } |
   { type: 'REMOVE_EVENT_INSTANCES', instances: EventInstanceHash } |
   { type: 'REMOVE_EVENT_INSTANCES', instances: EventInstanceHash } |

+ 2 - 2
src/structs/business-hours.ts

@@ -1,7 +1,7 @@
 import Calendar from '../Calendar'
 import Calendar from '../Calendar'
 import { assignTo } from '../util/object'
 import { assignTo } from '../util/object'
 import { EventInput } from './event'
 import { EventInput } from './event'
-import { EventStore, parseEventStore } from './event-store'
+import { EventStore, parseEvents } from './event-store'
 
 
 /*
 /*
 Utils for converting raw business hour input into an EventStore,
 Utils for converting raw business hour input into an EventStore,
@@ -20,7 +20,7 @@ const DEF_DEFAULTS = {
 }
 }
 
 
 export function parseBusinessHours(input: BusinessHoursInput, calendar: Calendar): EventStore {
 export function parseBusinessHours(input: BusinessHoursInput, calendar: Calendar): EventStore {
-  return parseEventStore(
+  return parseEvents(
     refineInputs(input),
     refineInputs(input),
     '',
     '',
     calendar
     calendar

+ 5 - 0
src/structs/event-source.ts

@@ -89,6 +89,7 @@ export type EventSourceFetcher = (
 ) => void
 ) => void
 
 
 export interface EventSourceDef {
 export interface EventSourceDef {
+  ignoreRange?: boolean
   parseMeta: (raw: EventSourceInput) => object | null
   parseMeta: (raw: EventSourceInput) => object | null
   fetch: EventSourceFetcher
   fetch: EventSourceFetcher
 }
 }
@@ -123,6 +124,10 @@ export function getEventSourceDef(id: number): EventSourceDef {
   return defs[id]
   return defs[id]
 }
 }
 
 
+export function doesSourceNeedRange(eventSource: EventSource) {
+  return !defs[eventSource.sourceDefId].ignoreRange
+}
+
 export function parseEventSource(raw: EventSourceInput): EventSource | null {
 export function parseEventSource(raw: EventSourceInput): EventSource | null {
   for (let i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
   for (let i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
     let def = defs[i]
     let def = defs[i]

+ 61 - 58
src/structs/event-store.ts

@@ -4,11 +4,11 @@ import {
   EventDefHash,
   EventDefHash,
   EventInstance,
   EventInstance,
   EventInstanceHash,
   EventInstanceHash,
-  parseEventDef,
-  parseEventDateSpan,
-  createEventInstance
+  createEventInstance,
+  parseEvent,
+  EventTuple
 } from './event'
 } from './event'
-import { parseEventDefRecurring, expandEventDef } from './recurring-event'
+import { expandRecurringRanges } from './recurring-event'
 import Calendar from '../Calendar'
 import Calendar from '../Calendar'
 import { assignTo, filterHash } from '../util/object'
 import { assignTo, filterHash } from '../util/object'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
@@ -24,80 +24,61 @@ export interface EventStore {
   instances: EventInstanceHash
   instances: EventInstanceHash
 }
 }
 
 
-export function parseEventStore(
+export function parseEvents(
   rawEvents: EventInput[],
   rawEvents: EventInput[],
   sourceId: string,
   sourceId: string,
-  calendar: Calendar,
-  expandRange?: DateRange,
-  dest: EventStore = createEmptyEventStore(), // specify this arg to append to an existing EventStore
+  calendar: Calendar
 ): EventStore {
 ): EventStore {
+  let eventStore = createEmptyEventStore()
+  let transform = calendar.opt('eventDataTransform')
 
 
-  for (let rawEvent of rawEvents) {
-    let leftovers = {}
-    let parsedRecurring = parseEventDefRecurring(rawEvent, leftovers)
-
-    // a recurring event?
-    if (parsedRecurring) {
-      let def = parseEventDef(leftovers, sourceId, parsedRecurring.isAllDay, parsedRecurring.hasEnd)
-
-      def.recurringDef = {
-        typeId: parsedRecurring.typeId,
-        typeData: parsedRecurring.typeData
-      }
-
-      dest.defs[def.defId] = def
-
-      if (expandRange) {
-        expandEventDefInstances(def, expandRange, calendar, dest.instances)
-      }
-
-    // a non-recurring event
-    } else {
-      let dateSpan = parseEventDateSpan(rawEvent, sourceId, calendar, leftovers)
+  if (transform) {
+    rawEvents = transformRawEvents(rawEvents, transform)
+  }
 
 
-      if (dateSpan) {
-        let def = parseEventDef(leftovers, sourceId, dateSpan.isAllDay, dateSpan.hasEnd)
-        let instance = createEventInstance(def.defId, dateSpan.range, dateSpan.forcedStartTzo, dateSpan.forcedEndTzo)
+  for (let rawEvent of rawEvents) {
+    let tuple = parseEvent(rawEvent, sourceId, calendar)
 
 
-        dest.defs[def.defId] = def
-        dest.instances[instance.instanceId] = instance
-      }
+    if (tuple) {
+      eventTupleToStore(tuple, eventStore)
     }
     }
   }
   }
 
 
-  return dest
+  return eventStore
 }
 }
 
 
-export function expandEventStoreInstances(
-  eventStore: EventStore,
-  framingRange: DateRange,
-  calendar: Calendar
-): EventInstanceHash {
-  let dest: EventInstanceHash = {}
+export function eventTupleToStore(tuple: EventTuple, eventStore: EventStore = createEmptyEventStore()) {
+  eventStore.defs[tuple.def.defId] = tuple.def
 
 
-  for (let defId in eventStore.defs) {
-    expandEventDefInstances(eventStore.defs[defId], framingRange, calendar, dest)
+  if (tuple.instance) {
+    eventStore.instances[tuple.instance.instanceId] = tuple.instance
   }
   }
 
 
-  return dest
+  return eventStore
 }
 }
 
 
-// TODO: be smarter about where this and expandEventStoreInstances are called
-export function expandEventDefInstances(
-  def: EventDef,
-  framingRange: DateRange,
-  calendar: Calendar,
-  dest: EventInstanceHash
-) {
-  if (def.recurringDef) { // need to have this check?
-    let ranges = expandEventDef(def, framingRange, calendar)
+export function expandRecurring(eventStore: EventStore, framingRange: DateRange, calendar: Calendar): EventStore {
+  let { defs, instances } = eventStore
 
 
-    for (let range of ranges) {
-      let instance = createEventInstance(def.defId, range)
+  // remove existing recurring instances
+  instances = filterHash(instances, function(instance: EventInstance) {
+    return !defs[instance.defId].recurringDef
+  })
+
+  for (let defId in defs) {
+    let def = defs[defId]
 
 
-      dest[instance.instanceId] = instance
+    if (def.recurringDef) {
+      let ranges = expandRecurringRanges(def, framingRange, calendar)
+
+      for (let range of ranges) {
+        let instance = createEventInstance(defId, range)
+        instances[instance.instanceId] = instance
+      }
     }
     }
   }
   }
+
+  return { defs, instances }
 }
 }
 
 
 // retrieves events that have the same groupId as the instance specified by `instanceId`
 // retrieves events that have the same groupId as the instance specified by `instanceId`
@@ -132,6 +113,28 @@ export function getRelatedEvents(eventStore: EventStore, instanceId: string): Ev
   return dest
   return dest
 }
 }
 
 
+export function transformRawEvents(rawEvents, func) {
+  let refinedEvents
+
+  if (!func) {
+    refinedEvents = rawEvents
+  } else {
+    refinedEvents = []
+
+    for (let rawEvent of rawEvents) {
+      let refinedEvent = func(rawEvent)
+
+      if (refinedEvent) {
+        refinedEvents.push(refinedEvent)
+      } else if (refinedEvent == null) {
+        refinedEvents.push(rawEvent)
+      } // if a different falsy value, do nothing
+    }
+  }
+
+  return refinedEvents
+}
+
 export function createEmptyEventStore(): EventStore {
 export function createEmptyEventStore(): EventStore {
   return { defs: {}, instances: {} }
   return { defs: {}, instances: {} }
 }
 }

+ 123 - 66
src/structs/event.ts

@@ -5,6 +5,7 @@ import Calendar from '../Calendar'
 import { assignTo } from '../util/object'
 import { assignTo } from '../util/object'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
 import { startOfDay } from '../datelib/marker'
 import { startOfDay } from '../datelib/marker'
+import { parseRecurring } from './recurring-event'
 
 
 /*
 /*
 Utils for parsing event-input data. Each util parses a subset of the event-input's data.
 Utils for parsing event-input data. Each util parses a subset of the event-input's data.
@@ -42,8 +43,14 @@ export interface EventDateInput {
 
 
 export type EventInput = EventNonDateInput & EventDateInput
 export type EventInput = EventNonDateInput & EventDateInput
 
 
-export interface EventDefAttrs { // mirrors NON_DATE_PROPS. can be used elsewhere?
+export interface EventDef {
+  defId: string
+  sourceId: string
+  publicId: string
   groupId: string
   groupId: string
+  isAllDay: boolean
+  hasEnd: boolean
+  recurringDef: { typeId: number, typeData: any } | null
   title: string
   title: string
   url: string
   url: string
   startEditable: boolean | null
   startEditable: boolean | null
@@ -55,15 +62,6 @@ export interface EventDefAttrs { // mirrors NON_DATE_PROPS. can be used elsewher
   backgroundColor: string
   backgroundColor: string
   borderColor: string
   borderColor: string
   textColor: string
   textColor: string
-}
-
-export interface EventDef extends EventDefAttrs {
-  defId: string
-  sourceId: string
-  publicId: string
-  hasEnd: boolean
-  isAllDay: boolean
-  recurringDef: { typeId: number, typeData: {} } | null
   extendedProps: object
   extendedProps: object
 }
 }
 
 
@@ -75,20 +73,16 @@ export interface EventInstance {
   forcedEndTzo: number | null
   forcedEndTzo: number | null
 }
 }
 
 
-// information about an event's dates.
-// only used as an intermediate object. never stored anywhere.
-export interface EventDateSpan {
-  isAllDay: boolean
-  hasEnd: boolean
-  range: DateRange
-  forcedStartTzo: number | null
-  forcedEndTzo: number | null
+export interface EventTuple {
+  def: EventDef
+  instance: EventInstance
 }
 }
 
 
 export type EventInstanceHash = { [instanceId: string]: EventInstance }
 export type EventInstanceHash = { [instanceId: string]: EventInstance }
 export type EventDefHash = { [defId: string]: EventDef }
 export type EventDefHash = { [defId: string]: EventDef }
 
 
 const NON_DATE_PROPS = {
 const NON_DATE_PROPS = {
+  id: String,
   groupId: String,
   groupId: String,
   title: String,
   title: String,
   url: String,
   url: String,
@@ -108,84 +102,128 @@ const NON_DATE_PROPS = {
 
 
 const DATE_PROPS = {
 const DATE_PROPS = {
   start: null,
   start: null,
-  date: null,
+  date: null, // alias for start
   end: null,
   end: null,
   isAllDay: null
   isAllDay: null
 }
 }
 
 
 let uid = 0
 let uid = 0
 
 
-export function parseEventDef(raw: EventNonDateInput, sourceId: string, isAllDay: boolean, hasEnd: boolean): EventDef {
-  let leftovers = {} as any
-  let props = refineProps(raw, NON_DATE_PROPS, {}, leftovers) as EventDef
 
 
-  props.defId = String(uid++)
-  props.sourceId = sourceId
-  props.isAllDay = isAllDay
-  props.hasEnd = hasEnd
-  props.recurringDef = null
+export function parseEvent(raw: EventInput, sourceId: string, calendar: Calendar): EventTuple | null {
+  let leftovers0 = {} as any
+  let dateProps = pluckDateProps(raw, leftovers0)
+  let leftovers1 = {} as any
+  let def = parseEventDef(raw, sourceId, leftovers1)
+  let instance: EventInstance = null
 
 
-  if ('id' in leftovers) {
-    props.publicId = String(leftovers.id)
-    delete leftovers.id
+  if (dateProps.start !== null) {
+    let instanceRes = parseEventInstance(dateProps, def.defId, sourceId, calendar)
+
+    if (instanceRes) {
+      def.isAllDay = instanceRes.isAllDay
+      def.hasEnd = instanceRes.hasEnd
+      instance = instanceRes.instance
+    } else {
+      return null // TODO: give a warning
+    }
+  } else {
+    let recurringRes = parseRecurring(
+      leftovers0, // non-date props and other non-standard props
+      leftovers1 // dest
+    )
+
+    if (recurringRes) {
+      def.isAllDay = recurringRes.isAllDay
+      def.hasEnd = recurringRes.hasEnd
+      def.recurringDef = { typeId: recurringRes.typeId, typeData: recurringRes.typeData }
+    } else {
+      return null // TODO: give a warning
+    }
   }
   }
 
 
-  if ('editable' in leftovers) {
+  def.extendedProps = assignTo(leftovers1, def.extendedProps)
+
+  return { def, instance }
+}
+
+
+/*
+Will NOT populate extendedProps with the leftover properties.
+The EventNonDateInput has been normalized (id => publicId, etc).
+*/
+export function parseEventDef(raw: EventNonDateInput, sourceId: string, leftovers?: any): EventDef {
+  let def = pluckNonDateProps(raw, leftovers) as EventDef
+
+  def.defId = String(uid++)
+  def.sourceId = sourceId
+
+  if (!def.extendedProps) {
+    def.extendedProps = {}
+  }
+
+  return def
+}
+
+
+function pluckDateProps(raw: EventInput, leftovers: any) {
+  let props = refineProps(raw, DATE_PROPS, {}, leftovers)
+
+  if (props.date !== null) {
+    if (props.start === null) {
+      props.start = props.date
+    }
+    delete props.date
+  }
+
+  return props
+}
+
+
+function pluckNonDateProps(raw: EventInput, leftovers: any) {
+  let props = refineProps(raw, NON_DATE_PROPS, {}, leftovers)
+
+  if (props.id !== null) {
+    props.publicId = String(props.id)
+    delete props.id
+  }
+
+  if (props.editable !== null) {
     if (props.startEditable === null) {
     if (props.startEditable === null) {
-      props.startEditable = leftovers.editable
+      props.startEditable = props.editable
     }
     }
     if (props.durationEditable === null) {
     if (props.durationEditable === null) {
-      props.durationEditable = leftovers.editable
+      props.durationEditable = props.editable
     }
     }
-    delete leftovers.editable
+    delete props.editable
   }
   }
 
 
-  if ('color' in leftovers) {
+  if (props.color !== null) {
     if (!props.backgroundColor) {
     if (!props.backgroundColor) {
-      props.backgroundColor = leftovers.color
+      props.backgroundColor = props.color
     }
     }
     if (!props.borderColor) {
     if (!props.borderColor) {
-      props.borderColor = leftovers.color
+      props.borderColor = props.color
     }
     }
-    delete leftovers.color
+    delete props.color
   }
   }
 
 
-  props.extendedProps = assignTo(leftovers, props.extendedProps || {})
-
   return props
   return props
 }
 }
 
 
-export function createEventInstance(
-  defId: string,
-  range: DateRange,
-  forcedStartTzo: number | null = null,
-  forcedEndTzo: number | null = null
-): EventInstance {
-  let instanceId = String(uid++)
-  return { instanceId, defId, range, forcedStartTzo, forcedEndTzo }
-}
 
 
-export function parseEventDateSpan(
-  raw: EventDateInput,
-  sourceId: string,
-  calendar: Calendar,
-  leftovers: object
-): EventDateSpan | null {
-  let dateProps = refineProps(raw, DATE_PROPS, {}, leftovers)
-  let rawStart = dateProps.start
+/*
+The EventDateInput has been normalized (date => start, etc).
+*/
+function parseEventInstance(dateProps: EventDateInput, defId: string, sourceId: string, calendar: Calendar) {
   let startMeta
   let startMeta
   let startMarker
   let startMarker
   let hasEnd = false
   let hasEnd = false
   let endMeta = null
   let endMeta = null
   let endMarker = null
   let endMarker = null
 
 
-  if (rawStart == null) {
-    rawStart = dateProps.date
-  }
+  startMeta = calendar.dateEnv.createMarkerMeta(dateProps.start)
 
 
-  if (rawStart != null) {
-    startMeta = calendar.dateEnv.createMarkerMeta(rawStart)
-  }
   if (!startMeta) {
   if (!startMeta) {
     return null
     return null
   }
   }
@@ -238,8 +276,27 @@ export function parseEventDateSpan(
   return {
   return {
     isAllDay,
     isAllDay,
     hasEnd,
     hasEnd,
-    range: { start: startMarker, end: endMarker },
-    forcedStartTzo: startMeta.forcedTzo,
-    forcedEndTzo: endMeta ? endMeta.forcedTzo : null
+    instance: createEventInstance(
+      defId,
+      { start: startMarker, end: endMarker },
+      startMeta.forcedTzo,
+      endMeta ? endMeta.forcedTzo : null
+    )
+  }
+}
+
+
+export function createEventInstance(
+  defId: string,
+  range: DateRange,
+  forcedStartTzo?: number,
+  forcedEndTzo?: number
+): EventInstance {
+  return {
+    instanceId: String(uid++),
+    defId,
+    range,
+    forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo,
+    forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo
   }
   }
 }
 }

+ 12 - 9
src/structs/recurring-event.ts

@@ -12,10 +12,6 @@ export interface ParsedRecurring {
   typeData: any
   typeData: any
 }
 }
 
 
-export interface IddParsedRecurring extends ParsedRecurring {
-  typeId: number
-}
-
 export interface RecurringType {
 export interface RecurringType {
   parse: (rawEvent: EventInput, leftoverProps: any) => ParsedRecurring | null
   parse: (rawEvent: EventInput, leftoverProps: any) => ParsedRecurring | null
   expand: (typeData: any, eventDef: EventDef, framingRange: DateRange, calendar: Calendar) => DateRange[]
   expand: (typeData: any, eventDef: EventDef, framingRange: DateRange, calendar: Calendar) => DateRange[]
@@ -29,13 +25,17 @@ export function registerRecurringType(recurringType: RecurringType) {
 }
 }
 
 
 
 
-export function parseEventDefRecurring(eventInput: EventInput, leftovers: any): IddParsedRecurring | null {
+export function parseRecurring(eventInput: EventInput, leftovers: any) {
   for (let i = 0; i < recurringTypes.length; i++) {
   for (let i = 0; i < recurringTypes.length; i++) {
-    let parsed = recurringTypes[i].parse(eventInput, leftovers) as IddParsedRecurring
+    let parsed = recurringTypes[i].parse(eventInput, leftovers) as ParsedRecurring
 
 
     if (parsed) {
     if (parsed) {
-      parsed.typeId = i
-      return parsed
+      return {
+        isAllDay: parsed.isAllDay,
+        hasEnd: parsed.hasEnd,
+        typeData: parsed.typeData,
+        typeId: i
+      }
     }
     }
   }
   }
 
 
@@ -43,7 +43,10 @@ export function parseEventDefRecurring(eventInput: EventInput, leftovers: any):
 }
 }
 
 
 
 
-export function expandEventDef(eventDef: EventDef, framingRange: DateRange, calendar: Calendar) {
+/*
+Event MUST have a recurringDef
+*/
+export function expandRecurringRanges(eventDef: EventDef, framingRange: DateRange, calendar: Calendar): DateRange[] {
   let typeDef = recurringTypes[eventDef.recurringDef.typeId]
   let typeDef = recurringTypes[eventDef.recurringDef.typeId]
 
 
   return typeDef.expand(
   return typeDef.expand(

+ 0 - 19
src/util/object.ts

@@ -18,14 +18,6 @@ export function assignTo(target, ...sources) {
 }
 }
 
 
 
 
-export function isEmptyObject(obj) {
-  for (let _key in obj) {
-    return false
-  }
-  return true
-}
-
-
 export function copyOwnProps(src, dest) {
 export function copyOwnProps(src, dest) {
   for (let name in src) {
   for (let name in src) {
     if (hasOwnProp(src, name)) {
     if (hasOwnProp(src, name)) {
@@ -123,14 +115,3 @@ export function arrayToHash(a): { [key: string]: true } {
 
 
   return hash
   return hash
 }
 }
-
-
-export function objectValues(obj) {
-  let vals = []
-
-  for (let key in obj) {
-    vals.push(obj[key])
-  }
-
-  return vals
-}