Ver código fonte

a lot of safe changes

Adam Shaw 7 anos atrás
pai
commit
82c43c63b7

+ 41 - 22
src/Calendar.ts

@@ -103,16 +103,10 @@ export default class Calendar {
     this.buildDelayedRerender = reselector(buildDelayedRerender)
     this.buildDelayedRerender = reselector(buildDelayedRerender)
 
 
     this.handleOptions(this.optionsManager.computed)
     this.handleOptions(this.optionsManager.computed)
-    this.constructed()
     this.hydrate()
     this.hydrate()
   }
   }
 
 
 
 
-  constructed() {
-    // useful for monkeypatching. used?
-  }
-
-
   getView(): View {
   getView(): View {
     return this.view
     return this.view
   }
   }
@@ -151,7 +145,10 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
 
 
 
 
-  _render(forceFlags: RenderForceFlags = {}) {
+  _render() {
+    let { rerenderFlags } = this
+    this.rerenderFlags = null // clear for future requestRerender calls, which might happen during render
+
     this.isRendering = true
     this.isRendering = true
 
 
     this.applyElClassNames()
     this.applyElClassNames()
@@ -162,8 +159,8 @@ export default class Calendar {
     }
     }
 
 
     this.freezeContentHeight() // do after contentEl is created in renderSkeleton
     this.freezeContentHeight() // do after contentEl is created in renderSkeleton
-    this.renderToolbars(forceFlags)
-    this.renderView(forceFlags)
+    this.renderToolbars(rerenderFlags)
+    this.renderView(rerenderFlags)
     this.thawContentHeight()
     this.thawContentHeight()
     this.releaseAfterSizingTriggers()
     this.releaseAfterSizingTriggers()
 
 
@@ -279,8 +276,6 @@ export default class Calendar {
 
 
   unrenderSkeleton() {
   unrenderSkeleton() {
     removeElement(this.contentEl)
     removeElement(this.contentEl)
-    this.contentEl = null
-
     this.removeNavLinkListener()
     this.removeNavLinkListener()
   }
   }
 
 
@@ -410,9 +405,7 @@ export default class Calendar {
       !this.renderingPauseDepth && // not paused
       !this.renderingPauseDepth && // not paused
       !this.isRendering // not currently in the render loop
       !this.isRendering // not currently in the render loop
     ) {
     ) {
-      let { rerenderFlags } = this
-      this.rerenderFlags = null // clear for future requestRerender calls, which might happen during render
-      this._render(rerenderFlags)
+      this._render()
     }
     }
   }
   }
 
 
@@ -504,7 +497,7 @@ export default class Calendar {
   }
   }
 
 
 
 
-  publiclyTrigger(name: string, args) {
+  publiclyTrigger(name: string, args?) {
     let optHandler = this.opt(name)
     let optHandler = this.opt(name)
 
 
     this.triggerWith(name, this, args)
     this.triggerWith(name, this, args)
@@ -710,10 +703,14 @@ export default class Calendar {
   }
   }
 
 
 
 
-  incrementDate(delta) { // is public facing
-    this.setCurrentDateMarker(
-      this.dateEnv.add(this.state.dateProfile.currentDate, delta)
-    )
+  incrementDate(deltaInput) { // is public facing
+    let delta = createDuration(deltaInput)
+
+    if (delta) { // else, warn about invalid input?
+      this.setCurrentDateMarker(
+        this.dateEnv.add(this.state.dateProfile.currentDate, delta)
+      )
+    }
   }
   }
 
 
 
 
@@ -962,11 +959,18 @@ export default class Calendar {
 
 
   // this public method receives start/end dates in any format, with any timezone
   // this public method receives start/end dates in any format, with any timezone
   // NOTE: args were changed from v3
   // NOTE: args were changed from v3
-  select(dateOrObj: DateInput | object, endDate?: DateInput) {
+  select(dateOrObj: DateInput | any, endDate?: DateInput) {
     let selectionInput: DateSpanInput
     let selectionInput: DateSpanInput
 
 
     if (endDate == null) {
     if (endDate == null) {
-      selectionInput = dateOrObj as DateSpanInput
+      if (dateOrObj.start != null) {
+        selectionInput = dateOrObj as DateSpanInput
+      } else {
+        selectionInput = {
+          start: dateOrObj,
+          end: null
+        }
+      }
     } else {
     } else {
       selectionInput = {
       selectionInput = {
         start: dateOrObj,
         start: dateOrObj,
@@ -974,7 +978,12 @@ export default class Calendar {
       } as DateSpanInput
       } as DateSpanInput
     }
     }
 
 
-    let selection = parseDateSpan(selectionInput, this.dateEnv)
+    let selection = parseDateSpan(
+      selectionInput,
+      this.dateEnv,
+      createDuration({ days: 1 }) // TODO: cache this?
+    )
+
     if (selection) { // throw parse error otherwise?
     if (selection) { // throw parse error otherwise?
       this.dispatch({
       this.dispatch({
         type: 'SELECT_DATES',
         type: 'SELECT_DATES',
@@ -1112,6 +1121,11 @@ export default class Calendar {
   }
   }
 
 
 
 
+  removeAllEvents() {
+    this.dispatch({ type: 'REMOVE_ALL_EVENTS' })
+  }
+
+
   rerenderEvents() { // API method. destroys old events if previously rendered.
   rerenderEvents() { // API method. destroys old events if previously rendered.
     this.requestRerender({ events: true }) // TODO: test this
     this.requestRerender({ events: true }) // TODO: test this
   }
   }
@@ -1161,6 +1175,11 @@ export default class Calendar {
   }
   }
 
 
 
 
+  removeAllEventSources() {
+    this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' })
+  }
+
+
   refetchEvents() {
   refetchEvents() {
     this.dispatch({ type: 'FETCH_EVENT_SOURCES' })
     this.dispatch({ type: 'FETCH_EVENT_SOURCES' })
   }
   }

+ 7 - 1
src/DateProfileGenerator.ts

@@ -1,7 +1,7 @@
 import View from './View'
 import View from './View'
 import { DateMarker, startOfDay, addDays } from './datelib/marker'
 import { DateMarker, startOfDay, addDays } from './datelib/marker'
 import { Duration, createDuration, getWeeksFromInput, asRoughDays, asRoughMs, greatestDurationDenominator } from './datelib/duration'
 import { Duration, createDuration, getWeeksFromInput, asRoughDays, asRoughMs, greatestDurationDenominator } from './datelib/duration'
-import { DateRange, OpenDateRange, constrainMarkerToRange, intersectRanges, rangesIntersect } from './datelib/date-range'
+import { DateRange, OpenDateRange, constrainMarkerToRange, intersectRanges, rangesIntersect, rangesEqual } from './datelib/date-range'
 
 
 
 
 export interface DateProfile {
 export interface DateProfile {
@@ -352,3 +352,9 @@ export default class DateProfileGenerator {
   }
   }
 
 
 }
 }
+
+
+export function isDateProfilesEqual(p0: DateProfile, p1: DateProfile) {
+  return rangesEqual(p0.activeRange, p1.activeRange) &&
+    rangesEqual(p0.validRange, p1.validRange)
+}

+ 4 - 4
src/Toolbar.ts

@@ -73,18 +73,18 @@ export default class Toolbar extends Component {
 
 
     if (renderProps.isPrevEnabled !== this.isPrevEnabled || forceFlags === true) {
     if (renderProps.isPrevEnabled !== this.isPrevEnabled || forceFlags === true) {
       if (renderProps.isPrevEnabled) {
       if (renderProps.isPrevEnabled) {
-        this.enableButton('today')
+        this.enableButton('prev')
       } else {
       } else {
-        this.disableButton('today')
+        this.disableButton('prev')
       }
       }
       this.isPrevEnabled = renderProps.isPrevEnabled
       this.isPrevEnabled = renderProps.isPrevEnabled
     }
     }
 
 
     if (renderProps.isNextEnabled !== this.isNextEnabled || forceFlags === true) {
     if (renderProps.isNextEnabled !== this.isNextEnabled || forceFlags === true) {
       if (renderProps.isNextEnabled) {
       if (renderProps.isNextEnabled) {
-        this.enableButton('today')
+        this.enableButton('next')
       } else {
       } else {
-        this.disableButton('today')
+        this.disableButton('next')
       }
       }
       this.isNextEnabled = renderProps.isNextEnabled
       this.isNextEnabled = renderProps.isNextEnabled
     }
     }

+ 16 - 10
src/agenda/TimeGrid.ts

@@ -85,20 +85,26 @@ export default class TimeGrid extends DateComponent {
 
 
   // Slices up the given span (unzoned start/end with other misc data) into an array of segments
   // Slices up the given span (unzoned start/end with other misc data) into an array of segments
   rangeToSegs(range: DateRange): Seg[] {
   rangeToSegs(range: DateRange): Seg[] {
-    let segs = this.sliceRangeByTimes(range)
-    let i
+    range = intersectRanges(range, this.dateProfile.validRange)
 
 
-    for (i = 0; i < segs.length; i++) {
-      if (this.isRTL) {
-        segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex
-      } else {
-        segs[i].col = segs[i].dayIndex
+    if (range) {
+      let segs = this.sliceRangeByTimes(range)
+      let i
+
+      for (i = 0; i < segs.length; i++) {
+        if (this.isRTL) {
+          segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex
+        } else {
+          segs[i].col = segs[i].dayIndex
+        }
+
+        segs[i].component = this
       }
       }
 
 
-      segs[i].component = this
+      return segs
+    } else {
+      return []
     }
     }
-
-    return segs
   }
   }
 
 
 
 

+ 2 - 2
src/api/EventApi.ts

@@ -14,7 +14,7 @@ export default class EventApi {
     this.instance = instance || null
     this.instance = instance || null
   }
   }
 
 
-  updateProp(name: string, val: string) {
+  setProp(name: string, val: string) {
     if (name.match(/^(start|end|date|isAllDay)$/)) {
     if (name.match(/^(start|end|date|isAllDay)$/)) {
       // error. date-related props need other methods
       // error. date-related props need other methods
     } else {
     } else {
@@ -35,7 +35,7 @@ export default class EventApi {
     }
     }
   }
   }
 
 
-  updateExtendedProp(name: string, val: string) {
+  setExtendedProp(name: string, val: string) {
     this.mutate({
     this.mutate({
       extendedProps: { [name]: val }
       extendedProps: { [name]: val }
     })
     })

+ 21 - 12
src/basic/DayGrid.ts

@@ -82,22 +82,31 @@ export default class DayGrid extends DateComponent {
 
 
   // Slices up the given span (unzoned start/end with other misc data) into an array of segments
   // Slices up the given span (unzoned start/end with other misc data) into an array of segments
   rangeToSegs(range: DateRange): Seg[] {
   rangeToSegs(range: DateRange): Seg[] {
-    let segs = this.sliceRangeByRow(range)
+    let validatedRange = intersectRanges(range, this.dateProfile.validRange)
 
 
-    for (let i = 0; i < segs.length; i++) {
-      let seg = segs[i]
-      seg.component = this
+    if (validatedRange) {
+      let segs = this.sliceRangeByRow(validatedRange)
 
 
-      if (this.isRTL) {
-        seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex
-        seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex
-      } else {
-        seg.leftCol = seg.firstRowDayIndex
-        seg.rightCol = seg.lastRowDayIndex
+      for (let i = 0; i < segs.length; i++) {
+        let seg = segs[i]
+        seg.component = this
+
+        if (this.isRTL) {
+          seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex
+          seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex
+        } else {
+          seg.leftCol = seg.firstRowDayIndex
+          seg.rightCol = seg.lastRowDayIndex
+        }
+
+        seg.isStart = seg.isStart && range.start.valueOf() === validatedRange.start.valueOf()
+        seg.isEnd = seg.isEnd && range.end.valueOf() === validatedRange.end.valueOf()
       }
       }
-    }
 
 
-    return segs
+      return segs
+    } else {
+      return []
+    }
   }
   }
 
 
 
 

+ 9 - 2
src/component/DateComponent.ts

@@ -344,7 +344,7 @@ export default abstract class DateComponent extends Component {
       if (dirtyFlags.dates) {
       if (dirtyFlags.dates) {
         for (let name in dirtyFlags) {
         for (let name in dirtyFlags) {
           if (name !== 'skeleton') {
           if (name !== 'skeleton') {
-            forceFlags = true
+            dirtyFlags[name] = true
           }
           }
         }
         }
       }
       }
@@ -592,6 +592,9 @@ export default abstract class DateComponent extends Component {
 
 
 
 
   computeEventsSize() {
   computeEventsSize() {
+    if (this.fillRenderer) {
+      this.fillRenderer.computeSize('bgEvent')
+    }
     if (this.eventRenderer) {
     if (this.eventRenderer) {
       this.eventRenderer.computeFgSize()
       this.eventRenderer.computeFgSize()
     }
     }
@@ -599,6 +602,9 @@ export default abstract class DateComponent extends Component {
 
 
 
 
   assignEventsSize() {
   assignEventsSize() {
+    if (this.fillRenderer) {
+      this.fillRenderer.assignSize('bgEvent')
+    }
     if (this.eventRenderer) {
     if (this.eventRenderer) {
       this.eventRenderer.assignFgSize()
       this.eventRenderer.assignFgSize()
     }
     }
@@ -719,8 +725,9 @@ export default abstract class DateComponent extends Component {
 
 
   selectEventsByInstanceId(instanceId) {
   selectEventsByInstanceId(instanceId) {
     this.getAllEventSegs().forEach(function(seg) {
     this.getAllEventSegs().forEach(function(seg) {
+      let eventInstance = seg.eventRange.eventInstance
       if (
       if (
-        seg.eventRange.eventInstance.instanceId === instanceId &&
+        eventInstance && eventInstance.instanceId === instanceId &&
         seg.el // necessary?
         seg.el // necessary?
       ) {
       ) {
         seg.el.classList.add('fc-selected')
         seg.el.classList.add('fc-selected')

+ 5 - 0
src/datelib/date-range.ts

@@ -113,6 +113,11 @@ export function rangesIntersect(range0: OpenDateRange, range1: OpenDateRange): b
     (range0.start === null || range1.end === null || range0.start < range1.end)
     (range0.start === null || range1.end === null || range0.start < range1.end)
 }
 }
 
 
+export function rangeContainsRange(outerRange: OpenDateRange, innerRange: OpenDateRange): boolean {
+  return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) &&
+    (outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end))
+}
+
 export function rangeContainsMarker(range: OpenDateRange, date: DateMarker | number): boolean { // date can be a millisecond time
 export function rangeContainsMarker(range: OpenDateRange, date: DateMarker | number): boolean { // date can be a millisecond time
   return (range.start === null || date >= range.start) &&
   return (range.start === null || date >= range.start) &&
     (range.end === null || date < range.end)
     (range.end === null || date < range.end)

+ 21 - 5
src/interactions-external/ExternalElementDragging.ts

@@ -10,6 +10,7 @@ import Calendar from '../Calendar'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { DragMetaInput, DragMeta, parseDragMeta } from '../structs/drag-meta'
 import { DragMetaInput, DragMeta, parseDragMeta } from '../structs/drag-meta'
 import EventApi from '../api/EventApi'
 import EventApi from '../api/EventApi'
+import { elementMatches } from '../util/dom-manip'
 
 
 export interface EventRes { // TODO: relate this to EventRenderRange?
 export interface EventRes { // TODO: relate this to EventRenderRange?
   def: EventDef
   def: EventDef
@@ -55,11 +56,14 @@ export default class ExternalElementDragging {
 
 
     if (hit) {
     if (hit) {
       receivingCalendar = hit.component.getCalendar()
       receivingCalendar = hit.component.getCalendar()
-      droppableEvent = computeEventForDateSpan(
-        hit.dateSpan,
-        this.dragMeta!,
-        receivingCalendar
-      )
+
+      if (this.canDropElOnCalendar(ev.subjectEl as HTMLElement, receivingCalendar)) {
+        droppableEvent = computeEventForDateSpan(
+          hit.dateSpan,
+          this.dragMeta!,
+          receivingCalendar
+        )
+      }
     }
     }
 
 
     this.displayDrag(receivingCalendar, {
     this.displayDrag(receivingCalendar, {
@@ -146,6 +150,18 @@ export default class ExternalElementDragging {
     }
     }
   }
   }
 
 
+  canDropElOnCalendar(el: HTMLElement, receivingCalendar: Calendar): boolean {
+    let dropAccept = receivingCalendar.opt('dropAccept')
+
+    if (typeof dropAccept === 'function') {
+      return dropAccept(el)
+    } else if (typeof dropAccept === 'string' && dropAccept) {
+      return Boolean(elementMatches(el, dropAccept))
+    }
+
+    return true
+  }
+
 }
 }
 
 
 // Utils for computing event store from the DragMeta
 // Utils for computing event store from the DragMeta

+ 3 - 2
src/interactions/DateSelecting.ts

@@ -21,8 +21,9 @@ export default class DateSelecting {
   constructor(component: DateComponent) {
   constructor(component: DateComponent) {
     this.component = component
     this.component = component
 
 
-    this.dragging = new FeaturefulElementDragging(component.el)
-    this.dragging.touchScrollAllowed = false
+    let dragging = this.dragging = new FeaturefulElementDragging(component.el)
+    dragging.touchScrollAllowed = false
+    dragging.minDistance = component.opt('selectMinDistance') || 0
 
 
     let hitDragging = this.hitDragging = new HitDragging(this.dragging, component)
     let hitDragging = this.hitDragging = new HitDragging(this.dragging, component)
     hitDragging.emitter.on('pointerdown', this.handlePointerDown)
     hitDragging.emitter.on('pointerdown', this.handlePointerDown)

+ 91 - 23
src/interactions/EventDragging.ts

@@ -10,7 +10,10 @@ import FeaturefulElementDragging from '../dnd/FeaturefulElementDragging'
 import { EventStore, getRelatedEvents, createEmptyEventStore } from '../structs/event-store'
 import { EventStore, getRelatedEvents, createEmptyEventStore } from '../structs/event-store'
 import Calendar from '../Calendar'
 import Calendar from '../Calendar'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { EventInteractionState } from '../interactions/event-interaction-state'
-import { diffDates } from '../util/misc'
+import { diffDates, enableCursor, disableCursor } from '../util/misc'
+import { EventRenderRange } from '../component/event-rendering'
+import EventApi from '../api/EventApi'
+import { isEventStoreValid } from './constraint'
 
 
 export default class EventDragging {
 export default class EventDragging {
 
 
@@ -20,7 +23,7 @@ export default class EventDragging {
 
 
   // internal state
   // internal state
   draggingSeg: Seg | null = null
   draggingSeg: Seg | null = null
-  eventInstanceId: string = ''
+  eventRange: EventRenderRange | null = null
   relatedEvents: EventStore | null = null
   relatedEvents: EventStore | null = null
   receivingCalendar: Calendar | null = null
   receivingCalendar: Calendar | null = null
   validMutation: EventMutation | null = null
   validMutation: EventMutation | null = null
@@ -51,7 +54,8 @@ export default class EventDragging {
     let { mirror } = dragging
     let { mirror } = dragging
     let initialCalendar = component.getCalendar()
     let initialCalendar = component.getCalendar()
     let draggingSeg = this.draggingSeg = getElSeg(ev.subjectEl as HTMLElement)!
     let draggingSeg = this.draggingSeg = getElSeg(ev.subjectEl as HTMLElement)!
-    let eventInstanceId = this.eventInstanceId = draggingSeg.eventRange!.eventInstance!.instanceId
+    let eventRange = this.eventRange = draggingSeg.eventRange!
+    let eventInstanceId = eventRange.eventInstance!.instanceId
 
 
     this.relatedEvents = getRelatedEvents(
     this.relatedEvents = getRelatedEvents(
       initialCalendar.state.eventStore,
       initialCalendar.state.eventStore,
@@ -79,15 +83,18 @@ export default class EventDragging {
   }
   }
 
 
   handleDragStart = (ev: PointerDragEvent) => {
   handleDragStart = (ev: PointerDragEvent) => {
+    let initialCalendar = this.component.getCalendar()
+    let eventRange = this.eventRange!
+    let eventInstanceId = eventRange.eventInstance.instanceId
+
     if (ev.isTouch) {
     if (ev.isTouch) {
 
 
       // need to select a different event?
       // need to select a different event?
-      if (this.eventInstanceId !== this.component.eventSelection) {
-        let initialCalendar = this.component.getCalendar()
+      if (eventInstanceId !== this.component.eventSelection) {
 
 
         initialCalendar.dispatch({
         initialCalendar.dispatch({
           type: 'SELECT_EVENT',
           type: 'SELECT_EVENT',
-          eventInstanceId: this.eventInstanceId
+          eventInstanceId: eventInstanceId
         })
         })
 
 
         browserContext.reportEventSelection(this.component) // will unselect previous
         browserContext.reportEventSelection(this.component) // will unselect previous
@@ -97,6 +104,15 @@ export default class EventDragging {
     } else {
     } else {
       browserContext.unselectEvent()
       browserContext.unselectEvent()
     }
     }
+
+    initialCalendar.publiclyTrigger('eventDragStart', [
+      {
+        el: this.draggingSeg.el,
+        event: new EventApi(initialCalendar, eventRange.eventDef, eventRange.eventInstance),
+        jsEvent: ev.origEvent,
+        view: this.component.view
+      }
+    ])
   }
   }
 
 
   handleHitUpdate = (hit: Hit | null, isFinal: boolean) => {
   handleHitUpdate = (hit: Hit | null, isFinal: boolean) => {
@@ -106,33 +122,48 @@ export default class EventDragging {
 
 
     // states based on new hit
     // states based on new hit
     let receivingCalendar: Calendar | null = null
     let receivingCalendar: Calendar | null = null
-    let validMutation: EventMutation | null = null
+    let mutation: EventMutation | null = null
     let mutatedRelatedEvents: EventStore | null = null
     let mutatedRelatedEvents: EventStore | null = null
+    let isInvalid = false
 
 
     if (hit) {
     if (hit) {
       receivingCalendar = hit.component.getCalendar()
       receivingCalendar = hit.component.getCalendar()
+      mutation = computeEventMutation(initialHit, hit)
 
 
-      if (
-        initialCalendar !== receivingCalendar || // TODO: write test for this
-        !isHitsEqual(initialHit, hit)
-      ) {
-        validMutation = computeEventMutation(initialHit, hit)
+      if (mutation) {
+        mutatedRelatedEvents = applyMutationToEventStore(relatedEvents, mutation, receivingCalendar)
 
 
-        if (validMutation) {
-          mutatedRelatedEvents = applyMutationToEventStore(relatedEvents, validMutation, receivingCalendar)
+        if (!isEventStoreValid(mutatedRelatedEvents, this.component.dateProfile)) {
+          isInvalid = true
+          mutation = null
+          mutatedRelatedEvents = null
         }
         }
       }
       }
     }
     }
 
 
     this.displayDrag(receivingCalendar, {
     this.displayDrag(receivingCalendar, {
       affectedEvents: relatedEvents,
       affectedEvents: relatedEvents,
-      mutatedEvents: mutatedRelatedEvents || relatedEvents,
+      mutatedEvents: mutatedRelatedEvents || createEmptyEventStore(),
       isEvent: true,
       isEvent: true,
       origSeg: this.draggingSeg
       origSeg: this.draggingSeg
     })
     })
 
 
+    if (!isInvalid || isFinal) {
+      enableCursor()
+    } else {
+      disableCursor()
+    }
+
     if (!isFinal) {
     if (!isFinal) {
-      this.dragging.setMirrorNeedsRevert(!validMutation)
+
+      if (
+        initialCalendar === receivingCalendar && // TODO: write test for this
+        isHitsEqual(initialHit, hit)
+      ) {
+        mutation = null
+      }
+
+      this.dragging.setMirrorNeedsRevert(!mutation)
 
 
       // render the mirror if no already-rendered helper
       // render the mirror if no already-rendered helper
       // TODO: wish we could somehow wait for dispatch to guarantee render
       // TODO: wish we could somehow wait for dispatch to guarantee render
@@ -142,7 +173,7 @@ export default class EventDragging {
 
 
       // assign states based on new hit
       // assign states based on new hit
       this.receivingCalendar = receivingCalendar
       this.receivingCalendar = receivingCalendar
-      this.validMutation = validMutation
+      this.validMutation = mutation
       this.mutatedRelatedEvents = mutatedRelatedEvents
       this.mutatedRelatedEvents = mutatedRelatedEvents
     }
     }
   }
   }
@@ -158,22 +189,56 @@ export default class EventDragging {
     }
     }
   }
   }
 
 
-  handleDragEnd = () => {
+  handleDragEnd = (ev: PointerDragEvent) => {
     let initialCalendar = this.component.getCalendar()
     let initialCalendar = this.component.getCalendar()
+    let initialView = this.component.view
     let { receivingCalendar } = this
     let { receivingCalendar } = this
+    let { eventDef, eventInstance } = this.eventRange!
+    let eventApi = new EventApi(initialCalendar, eventDef, eventInstance)
+    let relatedEvents = this.relatedEvents!
+    let mutatedRelatedEvents = this.mutatedRelatedEvents!
 
 
     this.clearDrag() // must happen after revert animation
     this.clearDrag() // must happen after revert animation
 
 
+    initialCalendar.publiclyTrigger('eventDragStop', [
+      {
+        el: this.draggingSeg.el,
+        event: eventApi,
+        jsEvent: ev.origEvent,
+        view: initialView
+      }
+    ])
+
     if (this.validMutation) {
     if (this.validMutation) {
 
 
       // dropped within same calendar
       // dropped within same calendar
       if (receivingCalendar === initialCalendar) {
       if (receivingCalendar === initialCalendar) {
-        receivingCalendar.dispatch({
-          type: 'MUTATE_EVENTS',
-          mutation: this.validMutation,
-          instanceId: this.eventInstanceId
+
+        initialCalendar.dispatch({
+          type: 'ADD_EVENTS', // will merge
+          eventStore: mutatedRelatedEvents
         })
         })
 
 
+        initialCalendar.publiclyTrigger('eventMutation', [
+          {
+            mutation: this.validMutation, // TODO: public API?
+            prevEvent: eventApi,
+            event: new EventApi( // the data AFTER the mutation
+              initialCalendar,
+              mutatedRelatedEvents.defs[eventDef.defId],
+              eventInstance ? mutatedRelatedEvents.instances[eventInstance.instanceId] : null
+            ),
+            revert: function() {
+              initialCalendar.dispatch({
+                type: 'ADD_EVENTS', // will merge
+                eventStore: relatedEvents
+              })
+            },
+            jsEvent: ev.origEvent,
+            view: initialView
+          }
+        ])
+
       // dropped in different calendar
       // dropped in different calendar
       // TODO: more public triggers
       // TODO: more public triggers
       } else if (receivingCalendar) {
       } else if (receivingCalendar) {
@@ -186,11 +251,14 @@ export default class EventDragging {
           eventStore: this.mutatedRelatedEvents!
           eventStore: this.mutatedRelatedEvents!
         })
         })
       }
       }
+
+    } else {
+      initialCalendar.publiclyTrigger('_noEventDrop')
     }
     }
 
 
     // reset all internal state
     // reset all internal state
     this.draggingSeg = null
     this.draggingSeg = null
-    this.eventInstanceId = ''
+    this.eventRange = null
     this.relatedEvents = null
     this.relatedEvents = null
     this.receivingCalendar = null
     this.receivingCalendar = null
     this.validMutation = null
     this.validMutation = null

+ 80 - 12
src/interactions/EventResizing.ts

@@ -5,10 +5,12 @@ import { elementClosest } from '../util/dom-manip'
 import FeaturefulElementDragging from '../dnd/FeaturefulElementDragging'
 import FeaturefulElementDragging from '../dnd/FeaturefulElementDragging'
 import { PointerDragEvent } from '../dnd/PointerDragging'
 import { PointerDragEvent } from '../dnd/PointerDragging'
 import { getElSeg } from '../component/renderers/EventRenderer'
 import { getElSeg } from '../component/renderers/EventRenderer'
-import { EventInstance } from '../structs/event'
 import { EventStore, getRelatedEvents } from '../structs/event-store'
 import { EventStore, getRelatedEvents } from '../structs/event-store'
-import { diffDates } from '../util/misc'
+import { diffDates, enableCursor, disableCursor } from '../util/misc'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
+import EventApi from '../api/EventApi'
+import { EventRenderRange } from '../component/event-rendering'
+import { isEventStoreValid } from './constraint'
 
 
 export default class EventDragging {
 export default class EventDragging {
 
 
@@ -18,9 +20,10 @@ export default class EventDragging {
 
 
   // internal state
   // internal state
   draggingSeg: Seg | null = null
   draggingSeg: Seg | null = null
-  eventInstance: EventInstance | null = null
+  eventRange: EventRenderRange | null = null
   relatedEvents: EventStore | null = null
   relatedEvents: EventStore | null = null
   validMutation: EventMutation | null = null
   validMutation: EventMutation | null = null
+  mutatedRelatedEvents: EventStore | null = null
 
 
   constructor(component: DateComponent) {
   constructor(component: DateComponent) {
     this.component = component
     this.component = component
@@ -42,32 +45,46 @@ export default class EventDragging {
 
 
   handlePointerDown = (ev: PointerDragEvent) => {
   handlePointerDown = (ev: PointerDragEvent) => {
     let seg = this.querySeg(ev)!
     let seg = this.querySeg(ev)!
-    let eventInstance = this.eventInstance = seg.eventRange!.eventInstance!
+    let eventRange = this.eventRange = seg.eventRange!
+
+    this.dragging.minDistance = 5 // TODO: make this a constant
 
 
     // if touch, need to be working with a selected event
     // if touch, need to be working with a selected event
     this.dragging.setIgnoreMove(
     this.dragging.setIgnoreMove(
       !this.component.isValidSegDownEl(ev.origEvent.target as HTMLElement) ||
       !this.component.isValidSegDownEl(ev.origEvent.target as HTMLElement) ||
-      (ev.isTouch && this.component.eventSelection !== eventInstance.instanceId)
+      (ev.isTouch && this.component.eventSelection !== eventRange.eventInstance!.instanceId)
     )
     )
   }
   }
 
 
   handleDragStart = (ev: PointerDragEvent) => {
   handleDragStart = (ev: PointerDragEvent) => {
     let calendar = this.component.getCalendar()
     let calendar = this.component.getCalendar()
+    let eventRange = this.eventRange!
 
 
     this.relatedEvents = getRelatedEvents(
     this.relatedEvents = getRelatedEvents(
       calendar.state.eventStore,
       calendar.state.eventStore,
-      this.eventInstance!.instanceId
+      this.eventRange.eventInstance!.instanceId
     )
     )
 
 
     this.draggingSeg = this.querySeg(ev)
     this.draggingSeg = this.querySeg(ev)
+
+    calendar.publiclyTrigger('eventResizeStart', [
+      {
+        el: this.draggingSeg.el,
+        event: new EventApi(calendar, eventRange.eventDef, eventRange.eventInstance),
+        jsEvent: ev.origEvent,
+        view: this.component.view
+      }
+    ])
   }
   }
 
 
   handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => {
   handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => {
     let calendar = this.component.getCalendar()
     let calendar = this.component.getCalendar()
     let relatedEvents = this.relatedEvents!
     let relatedEvents = this.relatedEvents!
     let initialHit = this.hitDragging.initialHit!
     let initialHit = this.hitDragging.initialHit!
-    let eventInstance = this.eventInstance!
+    let eventInstance = this.eventRange.eventInstance!
     let mutation: EventMutation | null = null
     let mutation: EventMutation | null = null
+    let mutatedRelatedEvents: EventStore | null = null
+    let isInvalid = false
 
 
     if (hit) {
     if (hit) {
       mutation = computeMutation(
       mutation = computeMutation(
@@ -79,13 +96,21 @@ export default class EventDragging {
     }
     }
 
 
     if (mutation) {
     if (mutation) {
-      let mutatedRelated = applyMutationToEventStore(relatedEvents, mutation, calendar)
+      mutatedRelatedEvents = applyMutationToEventStore(relatedEvents, mutation, calendar)
+
+      if (!isEventStoreValid(mutatedRelatedEvents, this.component.dateProfile)) {
+        isInvalid = true
+        mutation = null
+        mutatedRelatedEvents = null
+      }
+    }
 
 
+    if (mutatedRelatedEvents) {
       calendar.dispatch({
       calendar.dispatch({
         type: 'SET_EVENT_RESIZE',
         type: 'SET_EVENT_RESIZE',
         state: {
         state: {
           affectedEvents: relatedEvents,
           affectedEvents: relatedEvents,
-          mutatedEvents: mutatedRelated,
+          mutatedEvents: mutatedRelatedEvents,
           isEvent: true,
           isEvent: true,
           origSeg: this.draggingSeg
           origSeg: this.draggingSeg
         }
         }
@@ -94,6 +119,12 @@ export default class EventDragging {
       calendar.dispatch({ type: 'UNSET_EVENT_RESIZE' })
       calendar.dispatch({ type: 'UNSET_EVENT_RESIZE' })
     }
     }
 
 
+    if (!isInvalid || isFinal) {
+      enableCursor()
+    } else {
+      disableCursor()
+    }
+
     if (!isFinal) {
     if (!isFinal) {
 
 
       if (mutation && isHitsEqual(initialHit, hit)) {
       if (mutation && isHitsEqual(initialHit, hit)) {
@@ -101,18 +132,55 @@ export default class EventDragging {
       }
       }
 
 
       this.validMutation = mutation
       this.validMutation = mutation
+      this.mutatedRelatedEvents = mutatedRelatedEvents
     }
     }
   }
   }
 
 
   handleDragEnd = (ev: PointerDragEvent) => {
   handleDragEnd = (ev: PointerDragEvent) => {
     let calendar = this.component.getCalendar()
     let calendar = this.component.getCalendar()
+    let view = this.component.view
+    let { eventDef, eventInstance } = this.eventRange!
+    let eventApi = new EventApi(calendar, eventDef, eventInstance)
+    let relatedEvents = this.relatedEvents!
+    let mutatedRelatedEvents = this.mutatedRelatedEvents!
+
+    calendar.publiclyTrigger('eventResizeStop', [
+      {
+        el: this.draggingSeg.el,
+        event: eventApi,
+        jsEvent: ev.origEvent,
+        view
+      }
+    ])
 
 
     if (this.validMutation) {
     if (this.validMutation) {
       calendar.dispatch({
       calendar.dispatch({
-        type: 'MUTATE_EVENTS',
-        mutation: this.validMutation,
-        instanceId: this.eventInstance!.instanceId
+        type: 'ADD_EVENTS', // will merge
+        eventStore: mutatedRelatedEvents
       })
       })
+
+      calendar.publiclyTrigger('eventMutation', [
+        {
+          mutation: this.validMutation, // TODO: public API?
+          prevEvent: eventApi,
+          event: new EventApi( // the data AFTER the mutation
+            calendar,
+            mutatedRelatedEvents.defs[eventDef.defId],
+            eventInstance ? mutatedRelatedEvents.instances[eventInstance.instanceId] : null
+          ),
+          revert: function() {
+            calendar.dispatch({
+              type: 'ADD_EVENTS', // will merge
+              eventStore: relatedEvents
+            })
+          },
+          jsEvent: ev.origEvent,
+          view
+        }
+      ])
+
+    } else {
+      calendar.publiclyTrigger('_noEventResize')
     }
     }
 
 
     // reset all internal state
     // reset all internal state

+ 5 - 2
src/interactions/HitDragging.ts

@@ -5,6 +5,7 @@ import DateComponent, { DateComponentHash } from '../component/DateComponent'
 import { DateSpan, isDateSpansEqual } from '../structs/date-span'
 import { DateSpan, isDateSpansEqual } from '../structs/date-span'
 import { computeRect } from '../util/dom-geom'
 import { computeRect } from '../util/dom-geom'
 import { constrainPoint, intersectRects, getRectCenter, diffPoints, Rect, Point } from '../util/geom'
 import { constrainPoint, intersectRects, getRectCenter, diffPoints, Rect, Point } from '../util/geom'
+import { rangeContainsRange } from '../datelib/date-range'
 
 
 export interface Hit {
 export interface Hit {
   component: DateComponent
   component: DateComponent
@@ -164,9 +165,11 @@ export default class HitDragging {
     let { droppableHash } = this
     let { droppableHash } = this
 
 
     for (let id in droppableHash) {
     for (let id in droppableHash) {
-      let hit = droppableHash[id].queryHit(x, y)
+      let component = droppableHash[id]
+      let hit = component.queryHit(x, y)
 
 
-      if (hit) {
+      // make sure the hit is within activeRange, meaning it's not a deal cell
+      if (hit && rangeContainsRange(component.dateProfile.activeRange, hit.dateSpan.range)) {
         return hit
         return hit
       }
       }
     }
     }

+ 25 - 0
src/interactions/constraint.ts

@@ -0,0 +1,25 @@
+import { DateRangeInput, rangeContainsRange } from '../datelib/date-range'
+import { BusinessHoursDef } from '../structs/business-hours'
+import { EventStore } from '../structs/event-store'
+import { DateProfile } from '../DateProfileGenerator'
+
+export type ConstraintInput = DateRangeInput | BusinessHoursDef | 'businessHours'
+
+export function isEventStoreValid(eventStore: EventStore, dateProfile: DateProfile) {
+  let instanceHash = eventStore.instances
+
+  if (dateProfile) { // for Popover
+    for (let instanceId in instanceHash) {
+      if (
+        !rangeContainsRange(
+          dateProfile.validRange,
+          instanceHash[instanceId].range
+        )
+      ) {
+        return false
+      }
+    }
+  }
+
+  return true
+}

+ 2 - 0
src/list/ListView.ts

@@ -93,6 +93,8 @@ export default class ListView extends View {
     this.dayRanges = dayRanges
     this.dayRanges = dayRanges
 
 
     // all real rendering happens in EventRenderer
     // all real rendering happens in EventRenderer
+
+    super.renderDates() // important for firing viewRender
   }
   }
 
 
 
 

+ 13 - 4
src/reducers/eventSources.ts

@@ -30,6 +30,9 @@ export default function(eventSourceHash: EventSourceHash, action: Action, datePr
     case 'RECEIVE_EVENT_ERROR':
     case 'RECEIVE_EVENT_ERROR':
       return receiveResponse(eventSourceHash, action.sourceId, action.fetchId, action.fetchRange)
       return receiveResponse(eventSourceHash, action.sourceId, action.fetchId, action.fetchRange)
 
 
+    case 'REMOVE_ALL_EVENT_SOURCES':
+      return {}
+
     default:
     default:
       return eventSourceHash
       return eventSourceHash
   }
   }
@@ -67,8 +70,8 @@ function fetchDirtySources(sourceHash: EventSourceHash, dateProfile: DateProfile
     if (
     if (
       !calendar.opt('lazyFetching') ||
       !calendar.opt('lazyFetching') ||
       !eventSource.fetchRange ||
       !eventSource.fetchRange ||
-      eventSource.fetchRange.start < activeRange.start ||
-      eventSource.fetchRange.end > activeRange.end
+      activeRange.start < eventSource.fetchRange.start ||
+      activeRange.end > eventSource.fetchRange.end
     ) {
     ) {
       dirtySourceIds.push(eventSource.sourceId)
       dirtySourceIds.push(eventSource.sourceId)
     }
     }
@@ -120,7 +123,10 @@ function fetchSource(eventSource: EventSource, range: DateRange, fetchId: string
       range
       range
     },
     },
     function(rawEvents) {
     function(rawEvents) {
-      eventSource.success(rawEvents)
+
+      if (eventSource.success) {
+        eventSource.success(rawEvents)
+      }
 
 
       calendar.dispatch({
       calendar.dispatch({
         type: 'RECEIVE_EVENTS',
         type: 'RECEIVE_EVENTS',
@@ -134,7 +140,10 @@ function fetchSource(eventSource: EventSource, range: DateRange, fetchId: string
       let error = normalizeError(errorInput)
       let error = normalizeError(errorInput)
 
 
       warn(error.message, error)
       warn(error.message, error)
-      eventSource.failure(error)
+
+      if (eventSource.failure) {
+        eventSource.failure(error)
+      }
 
 
       calendar.dispatch({
       calendar.dispatch({
         type: 'RECEIVE_EVENT_ERROR',
         type: 'RECEIVE_EVENT_ERROR',

+ 47 - 13
src/reducers/eventStore.ts

@@ -2,7 +2,7 @@ import Calendar from '../Calendar'
 import { filterHash } from '../util/object'
 import { filterHash } 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 { EventStore, parseEventStore, mergeEventStores, getRelatedEvents } from '../structs/event-store'
+import { EventStore, parseEventStore, mergeEventStores, getRelatedEvents, createEmptyEventStore } from '../structs/event-store'
 import { Action } from './types'
 import { Action } from './types'
 import { EventSourceHash, EventSource } from '../structs/event-source'
 import { EventSourceHash, EventSource } from '../structs/event-source'
 import { DateRange } from '../datelib/date-range'
 import { DateRange } from '../datelib/date-range'
@@ -30,7 +30,17 @@ export default function(eventStore: EventStore, action: Action, sourceHash: Even
       return excludeInstances(eventStore, action.instances)
       return excludeInstances(eventStore, action.instances)
 
 
     case 'REMOVE_EVENT_SOURCE':
     case 'REMOVE_EVENT_SOURCE':
-      return excludeSource(eventStore, action.sourceId)
+      return filterDefs(eventStore, function(eventDef: EventDef) {
+        return eventDef.sourceId !== action.sourceId
+      })
+
+    case 'REMOVE_ALL_EVENT_SOURCES':
+      return filterDefs(eventStore, function(eventDef: EventDef) {
+        return !eventDef.sourceId
+      })
+
+    case 'REMOVE_ALL_EVENTS':
+      return createEmptyEventStore()
 
 
     default:
     default:
       return eventStore
       return eventStore
@@ -45,21 +55,25 @@ function receiveEvents(
   rawEvents: EventInput[],
   rawEvents: EventInput[],
   calendar: Calendar
   calendar: Calendar
 ): EventStore {
 ): EventStore {
-  if (fetchId === eventSource.latestFetchId) { // TODO: wish this logic was always in event-sources
 
 
-    rawEvents = rawEvents.map(function(rawEvent: EventInput) {
-      return eventSource.eventDataTransform(rawEvent) || rawEvent
-    })
+  if (
+    eventSource && // not already removed
+    fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources
+  ) {
+
+    rawEvents = runEventDataTransform(rawEvents, eventSource.eventDataTransform)
+    rawEvents = runEventDataTransform(rawEvents, calendar.opt('eventDataTransform'))
 
 
     return parseEventStore(
     return parseEventStore(
       rawEvents,
       rawEvents,
       eventSource.sourceId,
       eventSource.sourceId,
       fetchRange,
       fetchRange,
       calendar,
       calendar,
-      excludeSource( // dest
+      filterDefs( // dest
         eventStore,
         eventStore,
-        eventSource.sourceId,
-        true // also remove events with isTemporary:true
+        function(eventDef: EventDef) {
+          return eventDef.sourceId !== eventSource.sourceId && !eventDef.isTemporary
+        }
       )
       )
     )
     )
   }
   }
@@ -67,6 +81,28 @@ function receiveEvents(
   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)
+
+      if (refinedEvent) {
+        refinedEvents.push(refinedEvent)
+      } else if (refinedEvent == null) {
+        refinedEvents.push(rawEvent)
+      } // if a different falsy value, do nothing
+    }
+  }
+
+  return refinedEvents
+}
+
 function excludeInstances(eventStore: EventStore, removals: EventInstanceHash): EventStore {
 function excludeInstances(eventStore: EventStore, removals: EventInstanceHash): EventStore {
   return {
   return {
     defs: eventStore.defs,
     defs: eventStore.defs,
@@ -77,10 +113,8 @@ function excludeInstances(eventStore: EventStore, removals: EventInstanceHash):
 }
 }
 
 
 // has extra bonus feature of removing temporary events
 // has extra bonus feature of removing temporary events
-function excludeSource(eventStore: EventStore, sourceId: string, temporaryMatch?: boolean): EventStore {
-  let defs = filterHash(eventStore.defs, function(def: EventDef) {
-    return def.sourceId !== sourceId || def.isTemporary === temporaryMatch
-  })
+function filterDefs(eventStore: EventStore, filterFunc: (eventDef: EventDef) => boolean): EventStore {
+  let defs = filterHash(eventStore.defs, filterFunc)
   let instances = filterHash(eventStore.instances, function(instance: EventInstance) {
   let instances = filterHash(eventStore.instances, function(instance: EventInstance) {
     return defs[instance.defId] // still exists?
     return defs[instance.defId] // still exists?
   })
   })

+ 7 - 3
src/reducers/main.ts

@@ -1,14 +1,14 @@
 import Calendar from '../Calendar'
 import Calendar from '../Calendar'
 import reduceEventSources from './eventSources'
 import reduceEventSources from './eventSources'
 import reduceEventStore from './eventStore'
 import reduceEventStore from './eventStore'
-import { DateProfile } from '../DateProfileGenerator'
+import { DateProfile, isDateProfilesEqual } from '../DateProfileGenerator'
 import { DateSpan } from '../structs/date-span'
 import { DateSpan } from '../structs/date-span'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { CalendarState, Action } from './types'
 import { CalendarState, Action } from './types'
 import { EventSourceHash } from '../structs/event-source'
 import { EventSourceHash } from '../structs/event-source'
 
 
 export default function(state: CalendarState, action: Action, calendar: Calendar): CalendarState {
 export default function(state: CalendarState, action: Action, calendar: Calendar): CalendarState {
-  calendar.trigger(action.type, action) // for testing hooks
+  calendar.publiclyTrigger(action.type, action) // for testing hooks
 
 
   let dateProfile = reduceDateProfile(state.dateProfile, action)
   let dateProfile = reduceDateProfile(state.dateProfile, action)
   let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendar)
   let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendar)
@@ -30,7 +30,9 @@ export default function(state: CalendarState, action: Action, calendar: Calendar
 function reduceDateProfile(currentDateProfile: DateProfile | null, action: Action) {
 function reduceDateProfile(currentDateProfile: DateProfile | null, action: Action) {
   switch (action.type) {
   switch (action.type) {
     case 'SET_DATE_PROFILE':
     case 'SET_DATE_PROFILE':
-      return action.dateProfile
+      return (currentDateProfile && isDateProfilesEqual(currentDateProfile, action.dateProfile)) ?
+        currentDateProfile : // if same, reuse the same object, better for rerenders
+        action.dateProfile
     default:
     default:
       return currentDateProfile
       return currentDateProfile
   }
   }
@@ -82,6 +84,8 @@ function reduceEventResize(currentEventResize: EventInteractionState | null, act
 
 
 function reduceEventSourceLoadingLevel(level: number, action: Action, eventSources: EventSourceHash): number {
 function reduceEventSourceLoadingLevel(level: number, action: Action, eventSources: EventSourceHash): number {
   switch (action.type) {
   switch (action.type) {
+    case 'REMOVE_ALL_EVENT_SOURCES':
+      return 0
     case 'FETCH_EVENT_SOURCES':
     case 'FETCH_EVENT_SOURCES':
       return level + (action.sourceIds ? action.sourceIds.length : Object.keys(eventSources).length)
       return level + (action.sourceIds ? action.sourceIds.length : Object.keys(eventSources).length)
     case 'RECEIVE_EVENTS':
     case 'RECEIVE_EVENTS':

+ 3 - 1
src/reducers/types.ts

@@ -37,10 +37,12 @@ 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: 'FETCH_EVENT_SOURCES', sourceIds?: string[] } | // if no sourceIds, fetch all
+  { type: 'REMOVE_ALL_EVENT_SOURCES' } |
 
 
   { type: 'RECEIVE_EVENTS', sourceId: string, fetchId: string, fetchRange: DateRange, rawEvents: EventInput[] } |
   { type: 'RECEIVE_EVENTS', sourceId: string, fetchId: string, fetchRange: DateRange, rawEvents: EventInput[] } |
   { type: 'RECEIVE_EVENT_ERROR', sourceId: string, fetchId: string, fetchRange: DateRange, error: SimpleError } |
   { type: 'RECEIVE_EVENT_ERROR', sourceId: string, fetchId: string, fetchRange: DateRange, error: SimpleError } |
 
 
   { type: 'ADD_EVENTS', eventStore: EventStore } |
   { type: 'ADD_EVENTS', eventStore: EventStore } |
   { type: 'MUTATE_EVENTS', instanceId: string, mutation: EventMutation } |
   { type: 'MUTATE_EVENTS', instanceId: string, mutation: EventMutation } |
-  { type: 'REMOVE_EVENT_INSTANCES', instances: EventInstanceHash }
+  { type: 'REMOVE_EVENT_INSTANCES', instances: EventInstanceHash } |
+  { type: 'REMOVE_ALL_EVENTS' }

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

@@ -16,7 +16,8 @@ const DEF_DEFAULTS = {
   endTime: '17:00',
   endTime: '17:00',
   daysOfWeek: [ 1, 2, 3, 4, 5 ], // monday - friday
   daysOfWeek: [ 1, 2, 3, 4, 5 ], // monday - friday
   rendering: 'inverse-background',
   rendering: 'inverse-background',
-  className: 'fc-nonbusiness'
+  className: 'fc-nonbusiness',
+  groupId: '_businessHours' // so multiple defs get grouped
 }
 }
 
 
 export function buildBusinessHours(
 export function buildBusinessHours(

+ 20 - 7
src/structs/date-span.ts

@@ -1,6 +1,7 @@
 import { DateRange, rangesEqual } from '../datelib/date-range'
 import { DateRange, rangesEqual } from '../datelib/date-range'
 import { DateInput, DateEnv } from '../datelib/env'
 import { DateInput, DateEnv } from '../datelib/env'
 import { refineProps } from '../util/misc'
 import { refineProps } from '../util/misc'
+import { Duration } from '../datelib/duration'
 
 
 /*
 /*
 A data-structure for a date-range that will be visually displayed.
 A data-structure for a date-range that will be visually displayed.
@@ -26,24 +27,36 @@ const STANDARD_PROPS = {
   isAllDay: Boolean
   isAllDay: Boolean
 }
 }
 
 
-export function parseDateSpan(raw: DateSpanInput, dateEnv: DateEnv): DateSpan | null {
+export function parseDateSpan(raw: DateSpanInput, dateEnv: DateEnv, defaultDuration?: Duration): DateSpan | null {
   let leftovers = {} as DateSpan
   let leftovers = {} as DateSpan
   let standardProps = refineProps(raw, STANDARD_PROPS, {}, leftovers)
   let standardProps = refineProps(raw, STANDARD_PROPS, {}, leftovers)
   let startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null
   let startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null
+  let startMarker
   let endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null
   let endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null
+  let endMarker
   let isAllDay = standardProps.isAllDay
   let isAllDay = standardProps.isAllDay
 
 
-  if (startMeta && endMeta) {
+  if (startMeta) {
+    startMarker = startMeta.marker
 
 
     if (isAllDay == null) {
     if (isAllDay == null) {
-      isAllDay = startMeta.isTimeUnspecified && endMeta.isTimeUnspecified
+      isAllDay = startMeta.isTimeUnspecified && (!endMeta || endMeta.isTimeUnspecified)
     }
     }
 
 
-    // use this leftover object as the selection object
-    leftovers.range = { start: startMeta.marker, end: endMeta.marker }
-    leftovers.isAllDay = isAllDay
+    if (endMeta) {
+      endMarker = endMeta.marker
+    } else if (defaultDuration) {
+      endMarker = dateEnv.add(startMarker, defaultDuration)
+    }
+
+    if (endMarker) {
 
 
-    return leftovers
+      // use this leftover object as the selection object
+      leftovers.range = { start: startMarker, end: endMarker }
+      leftovers.isAllDay = isAllDay
+
+      return leftovers
+    }
   }
   }
 
 
   return null
   return null

+ 4 - 4
src/structs/event-source.ts

@@ -72,8 +72,8 @@ export interface EventSource {
   backgroundColor: string
   backgroundColor: string
   borderColor: string
   borderColor: string
   textColor: string
   textColor: string
-  success: EventSourceSuccessHandler
-  failure: EventSourceFailureHandler
+  success: EventSourceSuccessHandler | null
+  failure: EventSourceFailureHandler | null
 }
 }
 
 
 export type EventSourceHash = { [sourceId: string]: EventSource }
 export type EventSourceHash = { [sourceId: string]: EventSource }
@@ -163,10 +163,10 @@ function parseEventSourceProps(raw: ExtendedEventSourceInput, meta: object, sour
 
 
   // TODO: consolidate with event struct
   // TODO: consolidate with event struct
   if ('color' in raw) {
   if ('color' in raw) {
-    if (props.backgroundColor === null) {
+    if (!props.backgroundColor) {
       props.backgroundColor = raw.color
       props.backgroundColor = raw.color
     }
     }
-    if (props.borderColor === null) {
+    if (!props.borderColor) {
       props.borderColor = raw.color
       props.borderColor = raw.color
     }
     }
   }
   }

+ 25 - 11
src/structs/event.ts

@@ -4,6 +4,7 @@ import { DateInput } from '../datelib/env'
 import Calendar from '../Calendar'
 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'
 
 
 /*
 /*
 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.
@@ -41,13 +42,8 @@ export interface EventDateInput {
 
 
 export type EventInput = EventNonDateInput & EventDateInput
 export type EventInput = EventNonDateInput & EventDateInput
 
 
-export interface EventDef {
-  defId: string
-  sourceId: string
-  publicId: string
+export interface EventDefAttrs { // mirrors NON_DATE_PROPS. can be used elsewhere?
   groupId: string
   groupId: string
-  hasEnd: boolean
-  isAllDay: boolean
   title: string
   title: string
   url: string
   url: string
   startEditable: boolean | null
   startEditable: boolean | null
@@ -59,6 +55,14 @@ export interface EventDef {
   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
   extendedProps: object
   extendedProps: object
   isTemporary?: boolean // if true, will disappear upon navigation
   isTemporary?: boolean // if true, will disappear upon navigation
 }
 }
@@ -88,6 +92,7 @@ const NON_DATE_PROPS = {
   groupId: String,
   groupId: String,
   title: String,
   title: String,
   url: String,
   url: String,
+  editable: Boolean,
   startEditable: Boolean,
   startEditable: Boolean,
   durationEditable: Boolean,
   durationEditable: Boolean,
   constraint: null,
   constraint: null,
@@ -135,10 +140,10 @@ export function parseEventDef(raw: EventNonDateInput, sourceId: string, isAllDay
   }
   }
 
 
   if ('color' in leftovers) {
   if ('color' in leftovers) {
-    if (props.backgroundColor === null) {
+    if (!props.backgroundColor) {
       props.backgroundColor = leftovers.color
       props.backgroundColor = leftovers.color
     }
     }
-    if (props.borderColor === null) {
+    if (!props.borderColor) {
       props.borderColor = leftovers.color
       props.borderColor = leftovers.color
     }
     }
     delete leftovers.color
     delete leftovers.color
@@ -168,6 +173,7 @@ export function parseEventDateSpan(
   let dateProps = refineProps(raw, DATE_PROPS, {}, leftovers)
   let dateProps = refineProps(raw, DATE_PROPS, {}, leftovers)
   let rawStart = dateProps.start
   let rawStart = dateProps.start
   let startMeta
   let startMeta
+  let startMarker
   let hasEnd = false
   let hasEnd = false
   let endMeta = null
   let endMeta = null
   let endMarker = null
   let endMarker = null
@@ -199,11 +205,19 @@ export function parseEventDateSpan(
     isAllDay = startMeta.isTimeUnspecified && (!endMeta || endMeta.isTimeUnspecified)
     isAllDay = startMeta.isTimeUnspecified && (!endMeta || endMeta.isTimeUnspecified)
   }
   }
 
 
+  startMarker = startMeta.marker
+
+  if (isAllDay) {
+    startMarker = startOfDay(startMarker)
+  }
+
   if (endMeta) {
   if (endMeta) {
     endMarker = endMeta.marker
     endMarker = endMeta.marker
 
 
-    if (endMarker <= startMeta.marker) {
+    if (endMarker <= startMarker) {
       endMarker = null
       endMarker = null
+    } else if (isAllDay) {
+      endMarker = startOfDay(endMarker)
     }
     }
   }
   }
 
 
@@ -213,7 +227,7 @@ export function parseEventDateSpan(
     hasEnd = calendar.opt('forceEventDuration') || false
     hasEnd = calendar.opt('forceEventDuration') || false
 
 
     endMarker = calendar.dateEnv.add(
     endMarker = calendar.dateEnv.add(
-      startMeta.marker,
+      startMarker,
       isAllDay ?
       isAllDay ?
         calendar.defaultAllDayEventDuration :
         calendar.defaultAllDayEventDuration :
         calendar.defaultTimedEventDuration
         calendar.defaultTimedEventDuration
@@ -223,7 +237,7 @@ export function parseEventDateSpan(
   return {
   return {
     isAllDay,
     isAllDay,
     hasEnd,
     hasEnd,
-    range: { start: startMeta.marker, end: endMarker },
+    range: { start: startMarker, end: endMarker },
     forcedStartTzo: startMeta.forcedTzo,
     forcedStartTzo: startMeta.forcedTzo,
     forcedEndTzo: endMeta ? endMeta.forcedTzo : null
     forcedEndTzo: endMeta ? endMeta.forcedTzo : null
   }
   }

+ 3 - 6
src/types/input-types.ts

@@ -12,10 +12,7 @@ import { DateRangeInput } from '../datelib/date-range'
 import { BusinessHoursDef } from '../structs/business-hours'
 import { BusinessHoursDef } from '../structs/business-hours'
 import { EventInput } from '../structs/event'
 import { EventInput } from '../structs/event'
 import EventApi from '../api/EventApi'
 import EventApi from '../api/EventApi'
-
-
-// temporary!
-export type ConstraintInput = DateRangeInput | BusinessHoursDef | 'businessHours'
+import { ConstraintInput } from '../interactions/constraint'
 
 
 
 
 export interface ToolbarInput {
 export interface ToolbarInput {
@@ -193,10 +190,10 @@ export interface OptionsInputBase {
   eventDestroy?(arg: { event: EventApi, el: HTMLElement, view: View }): void
   eventDestroy?(arg: { event: EventApi, el: HTMLElement, view: View }): void
   eventDragStart?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: View }): void
   eventDragStart?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: View }): void
   eventDragStop?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: View }): void
   eventDragStop?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: View }): void
-  eventDrop?(arg: { el: HTMLElement, event: EventApi, delta: Duration, revertFunc: Function, jsEvent: Event, view: View }): void
+  eventDrop?(arg: { el: HTMLElement, event: EventApi, delta: Duration, revertFunc: () => void, jsEvent: Event, view: View }): void
   eventResizeStart?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
   eventResizeStart?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
   eventResizeStop?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
   eventResizeStop?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
-  eventResize?(arg: { el: HTMLElement, event: EventApi, delta: Duration, revertFunc: Function, jsEvent: Event, view: View }): void
+  eventResize?(arg: { el: HTMLElement, event: EventApi, delta: Duration, revertFunc: () => void, jsEvent: Event, view: View }): void
   drop?(arg: { date: DateInput, isAllDay: boolean, jsEvent: MouseEvent }): void
   drop?(arg: { date: DateInput, isAllDay: boolean, jsEvent: MouseEvent }): void
   eventReceive?(event: EventApi): void
   eventReceive?(event: EventApi): void
 }
 }

+ 4 - 6
src/util/misc.ts

@@ -402,8 +402,6 @@ export function debounce(func, wait) {
 
 
 export type GenericHash = { [key: string]: any }
 export type GenericHash = { [key: string]: any }
 
 
-let emptyFunc = function() { }
-
 // Number and Boolean are only types that defaults or not computed for
 // Number and Boolean are only types that defaults or not computed for
 // TODO: write more comments
 // TODO: write more comments
 export function refineProps(rawProps: GenericHash, processors: GenericHash, defaults: GenericHash = {}, leftoverProps?: GenericHash): GenericHash {
 export function refineProps(rawProps: GenericHash, processors: GenericHash, defaults: GenericHash = {}, leftoverProps?: GenericHash): GenericHash {
@@ -414,7 +412,9 @@ export function refineProps(rawProps: GenericHash, processors: GenericHash, defa
 
 
     if (rawProps[key] !== undefined) {
     if (rawProps[key] !== undefined) {
       // found
       // found
-      if (processor) { // a refining function?
+      if (processor === Function) {
+        refined[key] = typeof rawProps[key] === 'function' ? rawProps[key] : null
+      } else if (processor) { // a refining function?
         refined[key] = processor(rawProps[key])
         refined[key] = processor(rawProps[key])
       } else {
       } else {
         refined[key] = rawProps[key]
         refined[key] = rawProps[key]
@@ -426,9 +426,7 @@ export function refineProps(rawProps: GenericHash, processors: GenericHash, defa
       // must compute a default
       // must compute a default
       if (processor === String) {
       if (processor === String) {
         refined[key] = '' // empty string is default for String
         refined[key] = '' // empty string is default for String
-      } else if (processor === Function) {
-        refined[key] = emptyFunc // noop is default for Function
-      } else if (!processor || processor === Number || processor === Boolean) {
+      } else if (!processor || processor === Number || processor === Boolean || processor === Function) {
         refined[key] = null // assign null for other non-custom processor funcs
         refined[key] = null // assign null for other non-custom processor funcs
       } else {
       } else {
         refined[key] = processor(null) // run the custom processor func
         refined[key] = processor(null) // run the custom processor func