Ver código fonte

awesome Splitter/Slicer classes

Adam Shaw 7 anos atrás
pai
commit
04a67dfa0c

+ 1 - 2
src/agenda/AbstractAgendaView.ts

@@ -17,7 +17,6 @@ import { diffDays } from '../datelib/marker'
 import { ComponentContext } from '../component/Component'
 import { ViewSpec } from '../structs/view-spec'
 import DateProfileGenerator from '../DateProfileGenerator'
-import { memoizeSplitter } from '../component/event-splitting'
 import AllDaySplitter from './AllDaySplitter'
 
 const AGENDA_ALL_DAY_EVENT_LIMIT = 5
@@ -37,7 +36,7 @@ export default abstract class AgendaView extends View {
   scroller: ScrollComponent
   axisWidth: any // the width of the time axis running down the side
 
-  protected splitter = memoizeSplitter(new AllDaySplitter())
+  protected splitter = new AllDaySplitter()
 
 
   constructor(

+ 17 - 29
src/agenda/AgendaView.ts

@@ -58,13 +58,9 @@ export default class AgendaView extends AbstractAgendaView {
   render(props: ViewProps) {
     super.render(props) // for flags for updateSize
 
-    let { splitter } = this
-    let { dateProfile, dateSelection } = this.props
+    let { dateProfile, businessHours } = this.props
     let dayTable = this.buildDayTable(dateProfile, this.dateProfileGenerator)
-
-    let eventStores = splitter.splitEventStore(props.eventStore)
-    let eventDrags = splitter.splitEventDrag(props.eventDrag)
-    let eventResizes = splitter.splitEventResize(props.eventResize)
+    let splitProps = this.splitter.splitProps(props)
 
     if (this.header) {
       this.header.receiveProps({
@@ -75,32 +71,24 @@ export default class AgendaView extends AbstractAgendaView {
       })
     }
 
-    this.simpleTimeGrid.receiveProps({
-      dateProfile,
-      dayTable,
-      businessHours: props.businessHours,
-      dateSelection: dateSelection && !dateSelection.allDay ? dateSelection : null,
-      eventStore: eventStores.timed,
-      eventUiBases: props.eventUiBases,
-      eventSelection: props.eventSelection,
-      eventDrag: eventDrags.timed,
-      eventResize: eventResizes.timed
-    })
-
-    if (this.simpleDayGrid) {
-      this.simpleDayGrid.receiveProps({
+    this.simpleTimeGrid.receiveProps(
+      Object.assign({}, splitProps['timed'] || splitProps[''], {
         dateProfile,
         dayTable,
-        businessHours: props.businessHours,
-        dateSelection: dateSelection && dateSelection.allDay ? dateSelection : null,
-        eventStore: eventStores.allDay,
-        eventUiBases: props.eventUiBases,
-        eventSelection: props.eventSelection,
-        eventDrag: eventDrags.allDay,
-        eventResize: eventResizes.allDay,
-        nextDayThreshold: this.nextDayThreshold,
-        isRigid: false
+        businessHours
       })
+    )
+
+    if (this.simpleDayGrid) {
+      this.simpleDayGrid.receiveProps(
+        Object.assign({}, splitProps['allDay'] || splitProps[''], {
+          dateProfile,
+          dayTable,
+          businessHours,
+          nextDayThreshold: this.nextDayThreshold,
+          isRigid: false
+        })
+      )
     }
   }
 

+ 8 - 3
src/agenda/AllDaySplitter.ts

@@ -1,11 +1,16 @@
-import { Splitter } from '../component/event-splitting'
+import Splitter from '../component/event-splitting'
 import { hasBgRendering } from '../component/event-rendering'
 import { EventDef } from '../structs/event'
+import { DateSpan } from '../structs/date-span'
 
 export default class AllDaySplitter extends Splitter {
 
-  constructor() {
-    super([ 'allDay', 'timed' ])
+  getKeysForDateSpan(dateSpan: DateSpan): string[] {
+    if (dateSpan.allDay) {
+      return [ 'allDay' ]
+    } else {
+      return [ 'timed' ]
+    }
   }
 
   getKeysForEventDef(eventDef: EventDef): string[] {

+ 30 - 51
src/agenda/SimpleTimeGrid.ts

@@ -9,8 +9,8 @@ import reselector from '../util/reselector'
 import { intersectRanges, DateRange } from '../datelib/date-range'
 import DayTable from '../common/DayTable'
 import { DateEnv } from '../datelib/env'
-import { DateMarker, addMs } from '../datelib/marker'
-import { Slicer, memoizeSlicer } from '../common/slicing-utils'
+import { DateMarker } from '../datelib/marker'
+import Slicer from '../common/slicing-utils'
 import OffsetTracker from '../common/OffsetTracker'
 import { Hit } from '../interactions/HitDragging'
 
@@ -26,19 +26,14 @@ export interface SimpleTimeGridProps {
   eventResize: EventInteractionState | null
 }
 
-export interface SimpleTimeGridSlicerArgs {
-  component: TimeGrid // TODO: kill
-  dayRanges: DateRange[]
-}
-
 export default class SimpleTimeGrid extends DateComponent<SimpleTimeGridProps> {
 
   timeGrid: TimeGrid
-  dayRanges: DateRange[]
+  dayRanges: DateRange[] // for now indicator
   offsetTracker: OffsetTracker
 
   private buildDayRanges = reselector(buildDayRanges)
-  private slicer = memoizeSlicer(new Slicer(sliceTimeGridSegs))
+  private slicer = new TimeGridSlicer()
 
   constructor(context, timeGrid: TimeGrid) {
     super(context, timeGrid.el)
@@ -47,40 +42,20 @@ export default class SimpleTimeGrid extends DateComponent<SimpleTimeGridProps> {
   }
 
   render(props: SimpleTimeGridProps) {
-    let { slicer } = this
     let { dateProfile, dayTable } = props
-
     let dayRanges = this.dayRanges = this.buildDayRanges(dayTable, dateProfile, this.dateEnv)
-    let slicerArgs: SimpleTimeGridSlicerArgs = { dayRanges, component: this.timeGrid }
-    let segRes = slicer.eventStoreToSegs(
-      props.eventStore,
-      props.eventUiBases,
-      dateProfile,
-      null,
-      slicerArgs
-    )
 
-    this.timeGrid.receiveProps({
-      dateProfile,
-      cells: dayTable.cells[0],
-      businessHourSegs: slicer.businessHoursToSegs(props.businessHours, dateProfile, null, slicerArgs),
-      bgEventSegs: segRes.bg,
-      fgEventSegs: segRes.fg,
-      dateSelectionSegs: slicer.selectionToSegs(props.dateSelection, props.eventUiBases, slicerArgs),
-      eventSelection: props.eventSelection,
-      eventDrag: slicer.buildEventDrag(props.eventDrag, props.eventUiBases, dateProfile, null, slicerArgs),
-      eventResize: slicer.buildEventResize(props.eventResize, props.eventUiBases, dateProfile, null, slicerArgs)
-    })
+    this.timeGrid.receiveProps(
+      Object.assign({}, this.slicer.sliceProps(props, dateProfile, null, this.timeGrid, dayRanges), {
+        dateProfile,
+        cells: dayTable.cells[0]
+      })
+    )
   }
 
-  renderNowIndicator(date: DateMarker) { // TODO: user slicer???
+  renderNowIndicator(date: DateMarker) {
     this.timeGrid.renderNowIndicator(
-      // seg system might be overkill, but it handles scenario where line needs to be rendered
-      //  more than once because of columns with the same date (resources columns for example)
-      sliceTimeGridSegs({
-        start: date,
-        end: addMs(date, 1) // protect against null range
-      }, { dayRanges: this.dayRanges, component: this.timeGrid }),
+      this.slicer.sliceNowDate(date, this.timeGrid, this.dayRanges),
       date
     )
   }
@@ -140,23 +115,27 @@ export function buildDayRanges(dayTable: DayTable, dateProfile: DateProfile, dat
   return ranges
 }
 
-export function sliceTimeGridSegs(range: DateRange, slicerArgs: SimpleTimeGridSlicerArgs): TimeGridSeg[] {
-  let { dayRanges } = slicerArgs
-  let segs: TimeGridSeg[] = []
 
-  for (let col = 0; col < dayRanges.length; col++) {
-    let segRange = intersectRanges(range, dayRanges[col])
+export class TimeGridSlicer extends Slicer<TimeGridSeg, [DateRange[]]> {
 
-    if (segRange) {
-      segs.push({
-        start: segRange.start,
-        end: segRange.end,
-        isStart: segRange.start.valueOf() === range.start.valueOf(),
-        isEnd: segRange.end.valueOf() === range.end.valueOf(),
-        col
-      })
+  sliceRange(range: DateRange, dayRanges: DateRange[]): TimeGridSeg[] {
+    let segs: TimeGridSeg[] = []
+
+    for (let col = 0; col < dayRanges.length; col++) {
+      let segRange = intersectRanges(range, dayRanges[col])
+
+      if (segRange) {
+        segs.push({
+          start: segRange.start,
+          end: segRange.end,
+          isStart: segRange.start.valueOf() === range.start.valueOf(),
+          isEnd: segRange.end.valueOf() === range.end.valueOf(),
+          col
+        })
+      }
     }
+
+    return segs
   }
 
-  return segs
 }

+ 24 - 42
src/basic/SimpleDayGrid.ts

@@ -8,7 +8,7 @@ import DayTable from '../common/DayTable'
 import { Duration } from '../datelib/duration'
 import DateComponent from '../component/DateComponent'
 import { DateRange } from '../datelib/date-range'
-import { Slicer, memoizeSlicer } from '../common/slicing-utils'
+import Slicer from '../common/slicing-utils'
 import OffsetTracker from '../common/OffsetTracker'
 import { Hit } from '../interactions/HitDragging'
 
@@ -26,18 +26,12 @@ export interface SimpleDayGridProps {
   isRigid: boolean
 }
 
-export interface SimpleDayGridSlicerArgs {
-  component: DayGrid // TODO: kill
-  dayTable: DayTable
-  isRtl: boolean
-}
-
 export default class SimpleDayGrid extends DateComponent<SimpleDayGridProps> {
 
   dayGrid: DayGrid
   offsetTracker: OffsetTracker
 
-  private slicer = memoizeSlicer(new Slicer(sliceDayGridSegs))
+  private slicer = new DayGridSlicer()
 
   constructor(context, dayGrid: DayGrid) {
     super(context, dayGrid.el)
@@ -46,30 +40,16 @@ export default class SimpleDayGrid extends DateComponent<SimpleDayGridProps> {
   }
 
   render(props: SimpleDayGridProps) {
-    let { dayGrid, slicer, isRtl } = this
-    let { dateProfile, dayTable, nextDayThreshold } = props
-
-    let slicerArgs = { dayTable, isRtl, component: this.dayGrid }
-    let segRes = slicer.eventStoreToSegs(
-      props.eventStore,
-      props.eventUiBases,
-      dateProfile,
-      nextDayThreshold,
-      slicerArgs
+    let { dayGrid } = this
+    let { dateProfile, dayTable } = props
+
+    dayGrid.receiveProps(
+      Object.assign({}, this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, dayGrid, dayTable, this.isRtl), {
+        dateProfile,
+        cells: dayTable.cells,
+        isRigid: props.isRigid
+      })
     )
-
-    dayGrid.receiveProps({
-      dateProfile,
-      cells: dayTable.cells,
-      businessHourSegs: slicer.businessHoursToSegs(props.businessHours, dateProfile, nextDayThreshold, slicerArgs),
-      bgEventSegs: segRes.bg,
-      fgEventSegs: segRes.fg,
-      dateSelectionSegs: slicer.selectionToSegs(props.dateSelection, props.eventUiBases, slicerArgs),
-      eventSelection: props.eventSelection,
-      eventDrag: slicer.buildEventDrag(props.eventDrag, props.eventUiBases, dateProfile, nextDayThreshold, slicerArgs),
-      eventResize: slicer.buildEventResize(props.eventResize, props.eventUiBases, dateProfile, nextDayThreshold, slicerArgs),
-      isRigid: props.isRigid
-    })
   }
 
   prepareHits() {
@@ -114,16 +94,18 @@ export default class SimpleDayGrid extends DateComponent<SimpleDayGridProps> {
 SimpleDayGrid.prototype.isInteractable = true
 
 
-export function sliceDayGridSegs(range: DateRange, slicerArgs: SimpleDayGridSlicerArgs): DayGridSeg[] {
-  let { dayTable, isRtl } = slicerArgs
+export class DayGridSlicer extends Slicer<DayGridSeg, [DayTable, boolean]> {
+
+  sliceRange(dateRange: DateRange, dayTable: DayTable, isRtl: boolean): DayGridSeg[] {
+    return dayTable.sliceRange(dateRange).map(function(seg) {
+      return {
+        isStart: seg.isStart,
+        isEnd: seg.isEnd,
+        row: seg.row,
+        leftCol: isRtl ? (dayTable.colCnt - 1 - seg.lastCol) : seg.firstCol,
+        rightCol: isRtl ? (dayTable.colCnt - 1 - seg.firstCol) : seg.lastCol
+      }
+    })
+  }
 
-  return dayTable.sliceRange(range).map(function(seg) {
-    return {
-      isStart: seg.isStart,
-      isEnd: seg.isEnd,
-      row: seg.row,
-      leftCol: isRtl ? (dayTable.colCnt - 1 - seg.lastCol) : seg.firstCol,
-      rightCol: isRtl ? (dayTable.colCnt - 1 - seg.firstCol) : seg.lastCol
-    }
-  })
 }

+ 109 - 75
src/common/slicing-utils.ts

@@ -3,78 +3,112 @@ import { EventStore, expandRecurring } from '../structs/event-store'
 import { EventUiHash } from '../component/event-ui'
 import { sliceEventStore, EventRenderRange } from '../component/event-rendering'
 import { DateProfile } from '../DateProfileGenerator'
-import { Seg, EventSegUiInteractionState } from '../component/DateComponent'
+import { Seg, EventSegUiInteractionState } from '../component/DateComponent' // TODO: rename EventSegUiInteractionState, move here
 import { DateSpan, fabricateEventRange } from '../structs/date-span'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import DateComponent from '../component/DateComponent'
 import { Duration } from '../datelib/duration'
 import reselector from '../util/reselector'
-import { isPropsEqual } from '../util/object'
-
-export function memoizeSlicer<
-  SegType extends Seg,
-  OtherArgsType extends { component: DateComponent<any> }, // TODO: kill component requirement
->(
-  slicer: Slicer<SegType, OtherArgsType>
-) {
-  let buildInteraction = slicer.buildInteraction.bind(slicer) as typeof slicer.buildInteraction
-
-  // WARNING: important to keep these memoizer equalityfuncs up to date with the signatures of the methods below!!!
-  // YUCK
-  return {
-    businessHoursToSegs: reselector(slicer.businessHoursToSegs.bind(slicer) as typeof slicer.businessHoursToSegs, [ null, null, null, isPropsEqual ]),
-    eventStoreToSegs: reselector(slicer.eventStoreToSegs.bind(slicer) as typeof slicer.eventStoreToSegs, [ null, isPropsEqual, null, null, isPropsEqual ]),
-    selectionToSegs: reselector(slicer.dateSpanToCompleteSegs.bind(slicer) as typeof slicer.dateSpanToCompleteSegs, [ null, isPropsEqual ]),
-    buildEventDrag: reselector(buildInteraction, [ null, isPropsEqual, null, null, isPropsEqual ]),
-    buildEventResize: reselector(buildInteraction, [ null, isPropsEqual, null, null, isPropsEqual ])
-  }
+import { DateMarker, addMs } from '../datelib/marker'
+
+export interface SliceableProps {
+  dateSelection: DateSpan
+  businessHours: EventStore
+  eventStore: EventStore
+  eventDrag: EventInteractionState | null
+  eventResize: EventInteractionState | null
+  eventSelection: string
+  eventUiBases: EventUiHash
+}
+
+export interface SlicedProps<SegType extends Seg> {
+  dateSelectionSegs: SegType[]
+  businessHourSegs: SegType[]
+  fgEventSegs: SegType[]
+  bgEventSegs: SegType[]
+  eventDrag: EventSegUiInteractionState | null
+  eventResize: EventSegUiInteractionState | null
+  eventSelection: string
 }
 
-export class Slicer<
-  SegType extends Seg,
-  OtherArgsType extends { component: DateComponent<any> } // TODO: kill component requirement
-> {
+export default abstract class Slicer<SegType extends Seg, ExtraArgs extends any[] = []> {
+
+  private sliceBusinessHours = reselector(this._sliceBusinessHours)
+  private sliceDateSelection = reselector(this._sliceDateSpan)
+  private sliceEventStore = reselector(this._sliceEventStore)
+  private sliceEventDrag = reselector(this._sliceInteraction)
+  private sliceEventResize = reselector(this._sliceInteraction)
 
-  slice: (range: DateRange, otherArgs: OtherArgsType) => SegType[]
+  abstract sliceRange(dateRange: DateRange, ...extraArgs: ExtraArgs): SegType[]
 
-  constructor(
-    slice: (range: DateRange, otherArgs: OtherArgsType) => SegType[]
-  ) {
-    this.slice = slice
+  sliceProps(
+    props: SliceableProps,
+    dateProfile: DateProfile,
+    nextDayThreshold: Duration | null,
+    component: DateComponent<any>, // TODO: kill
+    ...extraArgs: ExtraArgs
+  ): SlicedProps<SegType> {
+    let eventSegs = this.sliceEventStore(props.eventStore, props.eventUiBases, dateProfile, nextDayThreshold, component, ...extraArgs)
+
+    return {
+      dateSelectionSegs: this.sliceDateSelection(props.dateSelection, props.eventUiBases, component, ...extraArgs),
+      businessHourSegs: this.sliceBusinessHours(props.businessHours, dateProfile, nextDayThreshold, component, ...extraArgs),
+      fgEventSegs: eventSegs.fg,
+      bgEventSegs: eventSegs.bg,
+      eventDrag: this.sliceEventDrag(props.eventDrag, props.eventUiBases, dateProfile, nextDayThreshold, component, ...extraArgs),
+      eventResize: this.sliceEventResize(props.eventResize, props.eventUiBases, dateProfile, nextDayThreshold, component, ...extraArgs),
+      eventSelection: props.eventSelection
+    }
+  }
+
+  sliceNowDate( // does not memoize
+    date: DateMarker,
+    component: DateComponent<any>, // TODO: kill
+    ...extraArgs: ExtraArgs
+  ): SegType[] {
+    return this._sliceDateSpan(
+      { range: { start: date, end: addMs(date, 1) }, allDay: false }, // add 1 ms, protect against null range
+      {},
+      component,
+      ...extraArgs
+    )
   }
 
-  businessHoursToSegs (
+  private _sliceBusinessHours(
     businessHours: EventStore,
     dateProfile: DateProfile,
     nextDayThreshold: Duration,
-    otherArgs: OtherArgsType
+    component: DateComponent<any>, // TODO: kill
+    ...extraArgs: ExtraArgs
   ): SegType[] {
     if (!businessHours) {
       return []
     }
 
-    return this.eventStoreToSegs(
-      expandRecurring(businessHours, dateProfile.activeRange, otherArgs.component.calendar),
+    return this._sliceEventStore(
+      expandRecurring(businessHours, dateProfile.activeRange, component.calendar),
       {},
       dateProfile,
       nextDayThreshold,
-      otherArgs
+      component,
+      ...extraArgs
     ).bg
   }
 
-  eventStoreToSegs(
+  private _sliceEventStore(
     eventStore: EventStore,
     eventUiBases: EventUiHash,
     dateProfile: DateProfile,
     nextDayThreshold: Duration,
-    otherArgs: OtherArgsType
+    component: DateComponent<any>, // TODO: kill
+    ...extraArgs: ExtraArgs
   ): { bg: SegType[], fg: SegType[] } {
     if (eventStore) {
       let rangeRes = sliceEventStore(eventStore, eventUiBases, dateProfile.activeRange, nextDayThreshold)
 
       return {
-        bg: this.eventRangesToCompleteSegs(rangeRes.bg, otherArgs),
-        fg: this.eventRangesToCompleteSegs(rangeRes.fg, otherArgs)
+        bg: this.sliceEventRanges(rangeRes.bg, component, extraArgs),
+        fg: this.sliceEventRanges(rangeRes.fg, component, extraArgs)
       }
 
     } else {
@@ -82,33 +116,13 @@ export class Slicer<
     }
   }
 
-  dateSpanToCompleteSegs(
-    dateSpan: DateSpan,
-    eventUiBases: EventUiHash,
-    otherArgs: OtherArgsType
-  ): SegType[] {
-    if (!dateSpan) {
-      return []
-    }
-
-    let component = otherArgs.component
-    let eventRange = fabricateEventRange(dateSpan, eventUiBases, component.calendar)
-    let segs = this.dateSpanToSegs(dateSpan, otherArgs)
-
-    for (let seg of segs) {
-      seg.component = component
-      seg.eventRange = eventRange
-    }
-
-    return segs
-  }
-
-  buildInteraction(
+  private _sliceInteraction(
     interaction: EventInteractionState,
     eventUiBases: EventUiHash,
     dateProfile: DateProfile,
     nextDayThreshold: Duration,
-    otherArgs: OtherArgsType
+    component: DateComponent<any>, // TODO: kill
+    ...extraArgs: ExtraArgs
   ): EventSegUiInteractionState {
     if (!interaction) {
       return null
@@ -117,21 +131,46 @@ export class Slicer<
     let rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, dateProfile.activeRange, nextDayThreshold)
 
     return {
-      segs: this.eventRangesToCompleteSegs(rangeRes.fg, otherArgs),
+      segs: this.sliceEventRanges(rangeRes.fg, component, extraArgs),
       affectedInstances: interaction.affectedEvents.instances,
       isEvent: interaction.isEvent,
       sourceSeg: interaction.origSeg
     }
   }
 
+  private _sliceDateSpan(
+    dateSpan: DateSpan,
+    eventUiBases: EventUiHash,
+    component: DateComponent<any>, // TODO: kill
+    ...extraArgs: ExtraArgs
+  ): SegType[] {
+    if (!dateSpan) {
+      return []
+    }
+
+    let eventRange = fabricateEventRange(dateSpan, eventUiBases, component.calendar)
+    let segs = this.sliceRange(dateSpan.range, ...extraArgs)
+
+    for (let seg of segs) {
+      seg.component = component
+      seg.eventRange = eventRange
+    }
+
+    return segs
+  }
+
   /*
   "complete" seg means it has component and eventRange
   */
-  private eventRangesToCompleteSegs(eventRanges: EventRenderRange[], otherArgs: OtherArgsType): SegType[] {
+  private sliceEventRanges(
+    eventRanges: EventRenderRange[],
+    component: DateComponent<any>, // TODO: kill
+    extraArgs: ExtraArgs
+  ): SegType[] {
     let segs: SegType[] = []
 
     for (let eventRange of eventRanges) {
-      segs.push(...this.eventRangeToCompleteSegs(eventRange, otherArgs))
+      segs.push(...this.sliceEventRange(eventRange, component, extraArgs))
     }
 
     return segs
@@ -140,9 +179,12 @@ export class Slicer<
   /*
   "complete" seg means it has component and eventRange
   */
-  private eventRangeToCompleteSegs(eventRange: EventRenderRange, otherArgs: OtherArgsType): SegType[] {
-    let component = otherArgs.component
-    let segs = this.eventRangeToSegs(eventRange, otherArgs)
+  private sliceEventRange(
+    eventRange: EventRenderRange,
+    component: DateComponent<any>, // TODO: kill
+    extraArgs: ExtraArgs
+  ): SegType[] {
+    let segs = this.sliceRange(eventRange.range, ...extraArgs)
 
     for (let seg of segs) {
       seg.component = component
@@ -154,12 +196,4 @@ export class Slicer<
     return segs
   }
 
-  protected dateSpanToSegs(dateSpan: DateSpan, otherArgs: OtherArgsType): SegType[] {
-    return this.slice(dateSpan.range, otherArgs)
-  }
-
-  protected eventRangeToSegs(eventRange: EventRenderRange, otherArgs: OtherArgsType): SegType[] {
-    return this.slice(eventRange.range, otherArgs)
-  }
-
 }

+ 137 - 54
src/component/event-splitting.ts

@@ -3,73 +3,88 @@ import { EventDef } from '../structs/event'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { mapHash } from '../util/object'
 import reselector from '../util/reselector'
-
-export function memoizeSplitter(splitter: Splitter) {
-  return {
-    splitEventStore: reselector(splitter.splitEventStore),
-    splitEventDrag: reselector(splitter.splitInteraction),
-    splitEventResize: reselector(splitter.splitInteraction)
-  }
+import { EventUiHash } from './event-ui'
+import { DateSpan } from '../structs/date-span'
+
+export interface SplittableProps {
+  dateSelection: DateSpan
+  eventStore: EventStore
+  eventUiBases: EventUiHash
+  eventSelection: string
+  eventDrag: EventInteractionState | null
+  eventResize: EventInteractionState | null
 }
 
-export abstract class Splitter { // not just EVENT splitting (rename file?)
+export default abstract class Splitter<ExtraArgs extends any[] = []> {
+
+  private getKeysForEventDefs = reselector(this._getKeysForEventDefs)
+  private splitDateSelection = reselector(this._splitDateSpan)
+  private splitEventStore = reselector(this._splitEventStore)
+  private splitEventUiBases = reselector(this._splitEventUiBases)
+  private splitEventDrag = reselector(this._splitInteraction)
+  private splitEventResize = reselector(this._splitInteraction)
+
+  abstract getKeysForDateSpan(dateSpan: DateSpan, ...extraArgs: ExtraArgs): string[]
+  abstract getKeysForEventDef(eventDef: EventDef, ...extraArgs: ExtraArgs): string[]
+
+  /*
+  will always create a default '' hash
+  */
+  splitProps(props: SplittableProps, ...extraArgs: ExtraArgs): { [key: string]: SplittableProps } {
+    let dateSelections = this.splitDateSelection(props.dateSelection, ...extraArgs)
+    let keysByDefId = this.getKeysForEventDefs(props.eventStore, ...extraArgs)
+    let eventStores = this.splitEventStore(props.eventStore, keysByDefId)
+    let eventUiBases = this.splitEventUiBases(props.eventUiBases, keysByDefId)
+    let eventDrags = this.splitEventDrag(props.eventDrag, keysByDefId, ...extraArgs)
+    let eventResizes = this.splitEventResize(props.eventResize, keysByDefId, ...extraArgs)
+    let splitProps: { [key: string]: SplittableProps } = {}
+
+    let populate = function(key: string) {
+      if (!splitProps[key]) {
+        let eventStore = eventStores[key] || createEmptyEventStore()
+
+        splitProps[key] = {
+          dateSelection: dateSelections[key] || null,
+          eventStore,
+          eventUiBases: eventUiBases[key] || eventUiBases[''],
+          eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '',
+          eventDrag: eventDrags[key] || null,
+          eventResize: eventResizes[key] || null
+        }
+      }
+    }
 
-  ensuredKeys: string[]
+    for (let key in dateSelections) { populate(key) }
+    for (let key in eventStores) { populate(key) }
+    for (let key in eventUiBases) { populate(key) } // guaranteed to create the '' hash
+    for (let key in eventDrags) { populate(key) }
+    for (let key in eventResizes) { populate(key) }
 
-  constructor(ensuredKeys: string[] = []) {
-    this.ensuredKeys = ensuredKeys
+    return splitProps
   }
 
-  splitInteraction = (state: EventInteractionState | null): { [key: string]: EventInteractionState } => {
-    let splitStates: { [key: string]: EventInteractionState } = {}
-
-    for (let key of this.ensuredKeys) {
-      splitStates[key] = null
-    }
-
-    if (state) {
-      let mutatedStores = this.splitEventStorePopulated(state.mutatedEvents)
-      let affectedStores = this.splitEventStorePopulated(state.affectedEvents)
-      let populate = function(key) {
-        if (!splitStates[key]) {
-          splitStates[key] = {
-            affectedEvents: affectedStores[key] || createEmptyEventStore(),
-            mutatedEvents: mutatedStores[key] || createEmptyEventStore(),
-            isEvent: state.isEvent,
-            origSeg: state.origSeg
-          }
-        }
-      }
+  private _splitDateSpan(dateSpan: DateSpan | null, ...extraArgs: ExtraArgs) {
+    let dateSpans = {}
 
-      for (let key in affectedStores) {
-        populate(key)
-      }
+    if (dateSpan) {
+      let keys = this.getKeysForDateSpan(dateSpan, ...extraArgs)
 
-      for (let key in mutatedStores) {
-        populate(key)
+      for (let key of keys) {
+        dateSpans[key] = dateSpan
       }
     }
 
-    return splitStates
+    return dateSpans
   }
 
-  splitEventStore = (eventStore: EventStore): { [key: string]: EventStore } => {
-    let splitStores = this.splitEventStorePopulated(eventStore)
-
-    for (let key of this.ensuredKeys) {
-      if (!splitStores[key]) {
-        splitStores[key] = createEmptyEventStore()
-      }
-    }
-
-    return splitStores
+  private _getKeysForEventDefs(eventStore: EventStore, ...extraArgs: ExtraArgs) {
+    return mapHash(eventStore.defs, (eventDef: EventDef) => {
+      return this.getKeysForEventDef(eventDef, ...extraArgs)
+    })
   }
 
-  splitEventStorePopulated(eventStore: EventStore): { [key: string]: EventStore } {
+  private _splitEventStore(eventStore: EventStore, keysByDefId): { [key: string]: EventStore } {
     let { defs, instances } = eventStore
-    let keysByDefId = mapHash(eventStore.defs, (eventDef: EventDef, defId: string) => {
-      return this.getKeysForEventDef(eventDef)
-    })
     let splitStores = {}
 
     for (let defId in defs) {
@@ -87,14 +102,82 @@ export abstract class Splitter { // not just EVENT splitting (rename file?)
       let instance = instances[instanceId]
 
       for (let key of keysByDefId[instance.defId]) {
-        splitStores[key].instances[instanceId] = instance
+
+        if (splitStores[key]) { // must have already been created
+          splitStores[key].instances[instanceId] = instance
+        }
       }
     }
 
     return splitStores
   }
 
-  // is allowed to return keys that aren't in the set
-  abstract getKeysForEventDef(eventDef: EventDef): string[]
+  /*
+  will always create a default '' hash
+  */
+  private _splitEventUiBases(eventUiBases: EventUiHash, keysByDefId): { [key: string]: EventUiHash } {
+    let splitHashes: { [key: string]: EventUiHash } = {}
+
+    for (let defId in eventUiBases) {
+      if (defId) { // not the '' key
+        for (let key of keysByDefId[defId]) {
+
+          if (!splitHashes[key]) {
+            splitHashes[key] = {}
+          }
+
+          splitHashes[key][defId] = eventUiBases[defId]
+        }
+      }
+    }
+
+    if (eventUiBases['']) {
+
+      // make sure each keyed hash has the fallback eventUiBase
+      for (let key in splitHashes) {
+        splitHashes[key][''] = eventUiBases['']
+      }
+
+      // ensure a default hash, which ONLY has the fallback eventUiBase
+      splitHashes[''] = { '': eventUiBases[''] }
+    } else {
+      splitHashes[''] = {}
+    }
+
+    return splitHashes
+  }
+
+  private _splitInteraction(interaction: EventInteractionState | null, keysByDefId, ...extraArgs: ExtraArgs): { [key: string]: EventInteractionState } {
+    let splitStates: { [key: string]: EventInteractionState } = {}
+
+    if (interaction) {
+      let affectedStores = this._splitEventStore(interaction.affectedEvents, keysByDefId)
+
+      // can't rely on keysByDefId because event data is mutated
+      let mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents, ...extraArgs)
+      let mutatedStores = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId)
+
+      let populate = function(key) {
+        if (!splitStates[key]) {
+          splitStates[key] = {
+            affectedEvents: affectedStores[key] || createEmptyEventStore(),
+            mutatedEvents: mutatedStores[key] || createEmptyEventStore(),
+            isEvent: interaction.isEvent,
+            origSeg: interaction.origSeg
+          }
+        }
+      }
+
+      for (let key in affectedStores) {
+        populate(key)
+      }
+
+      for (let key in mutatedStores) {
+        populate(key)
+      }
+    }
+
+    return splitStates
+  }
 
 }

+ 4 - 4
src/exports.ts

@@ -63,7 +63,7 @@ export {
 export { EventStore, filterEventStoreDefs, createEmptyEventStore } from './structs/event-store'
 export { hasBgRendering } from './component/event-rendering'
 export { EventUiHash, EventUi, processScopedUiProps, ScopedEventUiInput, combineEventUis } from './component/event-ui'
-export { Splitter, memoizeSplitter } from './component/event-splitting'
+export { default as Splitter } from './component/event-splitting'
 export { buildGotoAnchorHtml, getAllDayHtml, getDayClasses } from './component/date-rendering'
 
 export {
@@ -98,8 +98,8 @@ export { default as AgendaView, buildDayTable as buildAgendaDayTable } from './a
 export { default as AbstractAgendaView} from './agenda/AbstractAgendaView'
 export { default as AbstractBasicView} from './basic/AbstractBasicView'
 export { default as TimeGrid, TimeGridSeg } from './agenda/TimeGrid'
-export { buildDayRanges, sliceTimeGridSegs, SimpleTimeGridSlicerArgs } from './agenda/SimpleTimeGrid'
-export { sliceDayGridSegs, SimpleDayGridSlicerArgs } from './basic/SimpleDayGrid'
+export { TimeGridSlicer } from './agenda/SimpleTimeGrid'
+export { DayGridSlicer } from './basic/SimpleDayGrid'
 export { default as DayGrid, DayGridSeg } from './basic/DayGrid'
 export { default as BasicView, buildDayTable as buildBasicDayTable } from './basic/BasicView'
 export { default as ListView } from './list/ListView'
@@ -158,6 +158,6 @@ export { EventRenderRange, sliceEventStore } from './component/event-rendering'
 
 export { default as DayTable, DayTableSeg, DayTableCell } from './common/DayTable'
 
-export { Slicer, memoizeSlicer } from './common/slicing-utils'
+export { default as Slicer } from './common/slicing-utils'
 
 export { EventMutation } from './structs/event-mutation'