Ver código fonte

some cool stuff

Adam Shaw 6 anos atrás
pai
commit
77f7ef7629

+ 1 - 1
packages/core/src/Toolbar.ts

@@ -45,7 +45,7 @@ export default class Toolbar extends Component<ToolbarRenderProps> {
     this.renderPrev({ el, isEnabled: props.isPrevEnabled })
     this.renderPrev({ el, isEnabled: props.isPrevEnabled })
     this.renderNext({ el, isEnabled: props.isNextEnabled })
     this.renderNext({ el, isEnabled: props.isNextEnabled })
 
 
-    return [ el ]
+    return el
   }
   }
 
 
 
 

+ 25 - 181
packages/core/src/View.ts

@@ -3,18 +3,17 @@ import { DateMarker, addMs } from './datelib/marker'
 import { createDuration, Duration } from './datelib/duration'
 import { createDuration, Duration } from './datelib/duration'
 import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 import { ViewSpec } from './structs/view-spec'
 import { ViewSpec } from './structs/view-spec'
-import { createElement } from './util/dom-manip'
 import DateComponent from './component/DateComponent'
 import DateComponent from './component/DateComponent'
 import { EventStore } from './structs/event-store'
 import { EventStore } from './structs/event-store'
 import { EventUiHash } from './component/event-ui'
 import { EventUiHash } from './component/event-ui'
 import { sliceEventStore, EventRenderRange } from './component/event-rendering'
 import { sliceEventStore, EventRenderRange } from './component/event-rendering'
 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 { memoizeRendering } from './component/memoized-rendering'
 import { __assign } from 'tslib'
 import { __assign } from 'tslib'
-import ComponentContext from './component/ComponentContext'
+import { createElement } from './util/dom-manip'
 
 
 export interface ViewProps {
 export interface ViewProps {
+  viewSpec: ViewSpec
   dateProfileGenerator: DateProfileGenerator
   dateProfileGenerator: DateProfileGenerator
   dateProfile: DateProfile
   dateProfile: DateProfile
   businessHours: EventStore
   businessHours: EventStore
@@ -39,12 +38,9 @@ export default abstract class View extends DateComponent<ViewProps> {
   triggerWith: EmitterInterface['triggerWith']
   triggerWith: EmitterInterface['triggerWith']
   hasHandlers: EmitterInterface['hasHandlers']
   hasHandlers: EmitterInterface['hasHandlers']
 
 
-  viewSpec: ViewSpec
   type: string // subclass' view name (string). for the API
   type: string // subclass' view name (string). for the API
   title: string // the text that will be displayed in the header's title. SET BY CALLER for API
   title: string // the text that will be displayed in the header's title. SET BY CALLER for API
 
 
-  queuedScroll: any
-
   // now indicator
   // now indicator
   isNowIndicatorRendered: boolean
   isNowIndicatorRendered: boolean
   initialNowDate: DateMarker // result first getNow call
   initialNowDate: DateMarker // result first getNow call
@@ -52,31 +48,6 @@ export default abstract class View extends DateComponent<ViewProps> {
   nowIndicatorTimeoutID: any // for refresh timing of now indicator
   nowIndicatorTimeoutID: any // for refresh timing of now indicator
   nowIndicatorIntervalID: any // "
   nowIndicatorIntervalID: any // "
 
 
-  private renderDatesMem = memoizeRendering(this.renderDatesWrap, this.unrenderDatesWrap)
-  private renderBusinessHoursMem = memoizeRendering(this.renderBusinessHours, this.unrenderBusinessHours, [ this.renderDatesMem ])
-  private renderDateSelectionMem = memoizeRendering(this.renderDateSelectionWrap, this.unrenderDateSelectionWrap, [ this.renderDatesMem ])
-  private renderEventsMem = memoizeRendering(this.renderEvents, this.unrenderEvents, [ this.renderDatesMem ])
-  private renderEventSelectionMem = memoizeRendering(this.renderEventSelectionWrap, this.unrenderEventSelectionWrap, [ this.renderEventsMem ])
-  private renderEventDragMem = memoizeRendering(this.renderEventDragWrap, this.unrenderEventDragWrap, [ this.renderDatesMem ])
-  private renderEventResizeMem = memoizeRendering(this.renderEventResizeWrap, this.unrenderEventResizeWrap, [ this.renderDatesMem ])
-
-
-  constructor(viewSpec: ViewSpec, parentEl: HTMLElement) {
-    super(
-      createElement('div', { className: 'fc-view fc-' + viewSpec.type + '-view' })
-    )
-
-    this.viewSpec = viewSpec
-    this.type = viewSpec.type
-
-    parentEl.appendChild(this.el)
-    this.initialize()
-  }
-
-
-  initialize() { // convenient for sublcasses
-  }
-
 
 
   // Date Setting/Unsetting
   // Date Setting/Unsetting
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
@@ -99,42 +70,32 @@ export default abstract class View extends DateComponent<ViewProps> {
   }
   }
 
 
 
 
-  // General Rendering
+  // Sizing
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
 
 
 
 
-  render(props: ViewProps, context: ComponentContext) {
-    this.renderDatesMem(props.dateProfile)
-    this.renderBusinessHoursMem(props.businessHours)
-    this.renderDateSelectionMem(props.dateSelection)
-    this.renderEventsMem(props.eventStore)
-    this.renderEventSelectionMem(props.eventSelection)
-    this.renderEventDragMem(props.eventDrag)
-    this.renderEventResizeMem(props.eventResize)
+  componentDidMount() {
+    this.applyScroll({ duration: createDuration(this.context.options.scrollTime) }, false)
   }
   }
 
 
 
 
-  beforeUpdate() {
-    this.addScroll(this.queryScroll())
+  getSnapshotBeforeUpdate() {
+    return this.queryScroll()
   }
   }
 
 
 
 
-  destroy() {
-    super.destroy()
-
-    this.renderDatesMem.unrender() // should unrender everything else
+  componentDidUpdate(prevProps, prevState, snapshot) {
+    this.applyScroll(snapshot, false)
   }
   }
 
 
 
 
-  // Sizing
-  // -----------------------------------------------------------------------------------------------------------------
-
-
+  // called from CalendarComponent
   updateSize(isResize: boolean, viewHeight: number, isAuto: boolean) {
   updateSize(isResize: boolean, viewHeight: number, isAuto: boolean) {
     let { calendar } = this.context
     let { calendar } = this.context
+    let scrollState
 
 
-    if (isResize) {
-      this.addScroll(this.queryScroll()) // NOTE: same code as in beforeUpdate
+    if (isResize) { // if NOT a resize, scroll state will be maintained via getSnapshotBeforeUpdate/componentDidUpdate
+      scrollState = this.queryScroll()
     }
     }
 
 
     if (
     if (
@@ -148,7 +109,9 @@ export default abstract class View extends DateComponent<ViewProps> {
       this.updateBaseSize(isResize, viewHeight, isAuto)
       this.updateBaseSize(isResize, viewHeight, isAuto)
     }
     }
 
 
-    // NOTE: popScroll is called by CalendarComponent
+    if (isResize) {
+      this.applyScroll(scrollState, true)
+    }
   }
   }
 
 
 
 
@@ -156,56 +119,9 @@ export default abstract class View extends DateComponent<ViewProps> {
   }
   }
 
 
 
 
-  // Date Rendering
-  // -----------------------------------------------------------------------------------------------------------------
-
-  renderDatesWrap(dateProfile: DateProfile) {
-    this.renderDates(dateProfile)
-    this.addScroll({
-      duration: createDuration(this.context.options.scrollTime)
-    })
-  }
-
-  unrenderDatesWrap() {
-    this.stopNowIndicator()
-    this.unrenderDates()
-  }
-
-  renderDates(dateProfile: DateProfile) {}
-  unrenderDates() {}
-
-
-  // Business Hours
-  // -----------------------------------------------------------------------------------------------------------------
-
-  renderBusinessHours(businessHours: EventStore) {}
-  unrenderBusinessHours() {}
-
-
-  // Date Selection
-  // -----------------------------------------------------------------------------------------------------------------
-
-  renderDateSelectionWrap(selection: DateSpan) {
-    if (selection) {
-      this.renderDateSelection(selection)
-    }
-  }
-
-  unrenderDateSelectionWrap(selection: DateSpan) {
-    if (selection) {
-      this.unrenderDateSelection(selection)
-    }
-  }
-
-  renderDateSelection(selection: DateSpan) {}
-  unrenderDateSelection(selection: DateSpan) {}
-
-
   // Event Rendering
   // Event Rendering
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  renderEvents(eventStore: EventStore) {}
-  unrenderEvents() {}
 
 
   // util for subclasses
   // util for subclasses
   sliceEvents(eventStore: EventStore, allDay: boolean): EventRenderRange[] {
   sliceEvents(eventStore: EventStore, allDay: boolean): EventRenderRange[] {
@@ -220,66 +136,9 @@ export default abstract class View extends DateComponent<ViewProps> {
   }
   }
 
 
 
 
-  // Event Selection
+  // Now Indicator
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  renderEventSelectionWrap(instanceId: string) {
-    if (instanceId) {
-      this.renderEventSelection(instanceId)
-    }
-  }
-
-  unrenderEventSelectionWrap(instanceId: string) {
-    if (instanceId) {
-      this.unrenderEventSelection(instanceId)
-    }
-  }
-
-  renderEventSelection(instanceId: string) {}
-  unrenderEventSelection(instanceId: string) {}
-
-
-  // Event Drag
-  // -----------------------------------------------------------------------------------------------------------------
-
-  renderEventDragWrap(state: EventInteractionState) {
-    if (state) {
-      this.renderEventDrag(state)
-    }
-  }
-
-  unrenderEventDragWrap(state: EventInteractionState) {
-    if (state) {
-      this.unrenderEventDrag(state)
-    }
-  }
-
-  renderEventDrag(state: EventInteractionState) {}
-  unrenderEventDrag(state: EventInteractionState) {}
-
-
-  // Event Resize
-  // -----------------------------------------------------------------------------------------------------------------
-
-  renderEventResizeWrap(state: EventInteractionState) {
-    if (state) {
-      this.renderEventResize(state)
-    }
-  }
-
-  unrenderEventResizeWrap(state: EventInteractionState) {
-    if (state) {
-      this.unrenderEventResize(state)
-    }
-  }
-
-  renderEventResize(state: EventInteractionState) {}
-  unrenderEventResize(state: EventInteractionState) {}
-
-
-  /* Now Indicator
-  ------------------------------------------------------------------------------------------------------------------*/
-
 
 
   // Immediately render the current time indicator and begins re-rendering it at an interval,
   // Immediately render the current time indicator and begins re-rendering it at an interval,
   // which is defined by this.getNowIndicatorUnit().
   // which is defined by this.getNowIndicatorUnit().
@@ -380,29 +239,9 @@ export default abstract class View extends DateComponent<ViewProps> {
   }
   }
 
 
 
 
-  /* Scroller
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  addScroll(scroll, isForced?: boolean) {
-    if (isForced) {
-      scroll.isForced = isForced
-    }
-    __assign(this.queuedScroll || (this.queuedScroll = {}), scroll)
-  }
-
-
-  popScroll(isResize: boolean) {
-    this.applyQueuedScroll(isResize)
-    this.queuedScroll = null
-  }
-
-
-  applyQueuedScroll(isResize: boolean) {
-    if (this.queuedScroll) {
-      this.applyScroll(this.queuedScroll, isResize)
-    }
-  }
+  // Scroller
+  // -----------------------------------------------------------------------------------------------------------------
+  // QUESTION: do we need to have date-scroll separate?
 
 
 
 
   queryScroll() {
   queryScroll() {
@@ -459,3 +298,8 @@ EmitterMixin.mixInto(View)
 
 
 View.prototype.usesMinMaxTime = false
 View.prototype.usesMinMaxTime = false
 View.prototype.dateProfileGeneratorClass = DateProfileGenerator
 View.prototype.dateProfileGeneratorClass = DateProfileGenerator
+
+
+export function renderViewEl(type: string) {
+  return createElement('div', { className: 'fc-view fc-' + type + '-view' })
+}

+ 2 - 1
packages/core/src/common/ScrollComponent.ts

@@ -25,7 +25,8 @@ export default class ScrollComponent extends Component<ScrollComponentProps> {
 
 
   render(props: ScrollComponentProps) {
   render(props: ScrollComponentProps) {
     this.applyOverflow(props)
     this.applyOverflow(props)
-    return [ this.el ]
+
+    return this.el
   }
   }
 
 
 
 

+ 1 - 26
packages/core/src/component/ComponentContext.ts

@@ -20,35 +20,10 @@ export default interface ComponentContext {
 }
 }
 
 
 
 
-export function buildComponentContext(
-  calendar: Calendar,
-  pluginHooks: PluginHooks,
-  theme: Theme,
-  dateEnv: DateEnv,
-  options: any,
-  view?: View
-): ComponentContext {
+export function computeContextProps(options: any) {
   return {
   return {
-    calendar,
-    pluginHooks,
-    view,
-    dateEnv,
-    theme,
-    options,
     isRtl: options.dir === 'rtl',
     isRtl: options.dir === 'rtl',
     eventOrderSpecs: parseFieldSpecs(options.eventOrder),
     eventOrderSpecs: parseFieldSpecs(options.eventOrder),
     nextDayThreshold: createDuration(options.nextDayThreshold)
     nextDayThreshold: createDuration(options.nextDayThreshold)
   }
   }
 }
 }
-
-
-export function extendComponentContext(context: ComponentContext, options?: any, view?: View): ComponentContext {
-  return buildComponentContext(
-    context.calendar,
-    context.pluginHooks,
-    context.theme,
-    context.dateEnv,
-    options || context.options,
-    view || context.view
-  )
-}

+ 4 - 25
packages/core/src/component/DateComponent.ts

@@ -1,13 +1,11 @@
-import Component from './Component'
+import { Component } from '../view-framework'
 import { EventRenderRange } from './event-rendering'
 import { EventRenderRange } from './event-rendering'
 import { DateSpan } from '../structs/date-span'
 import { DateSpan } from '../structs/date-span'
 import { EventInstanceHash } from '../structs/event'
 import { EventInstanceHash } from '../structs/event'
 import { rangeContainsRange } from '../datelib/date-range'
 import { rangeContainsRange } from '../datelib/date-range'
 import { Hit } from '../interactions/hit'
 import { Hit } from '../interactions/hit'
-import { elementClosest, removeElement } from '../util/dom-manip'
+import { elementClosest } from '../util/dom-manip'
 import { isDateSelectionValid, isInteractionValid } from '../validation'
 import { isDateSelectionValid, isInteractionValid } from '../validation'
-import FgEventRenderer from './renderers/FgEventRenderer'
-import FillRenderer from './renderers/FillRenderer'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 import { EventInteractionState } from '../interactions/event-interaction-state'
 
 
 export type DateComponentHash = { [uid: string]: DateComponent<any> }
 export type DateComponentHash = { [uid: string]: DateComponent<any> }
@@ -35,7 +33,7 @@ PURPOSES:
 - hook up to fg, fill, and mirror renderers
 - hook up to fg, fill, and mirror renderers
 - interface for dragging and hits
 - interface for dragging and hits
 */
 */
-export default class DateComponent<PropsType> extends Component<PropsType> {
+export default abstract class DateComponent<PropsType> extends Component<PropsType> {
 
 
   // self-config, overridable by subclasses. must set on prototype
   // self-config, overridable by subclasses. must set on prototype
   fgSegSelector: string // lets eventRender produce elements without fc-event class
   fgSegSelector: string // lets eventRender produce elements without fc-event class
@@ -47,25 +45,6 @@ export default class DateComponent<PropsType> extends Component<PropsType> {
   // TODO: port isTimeScale into same system?
   // TODO: port isTimeScale into same system?
   largeUnit: any
   largeUnit: any
 
 
-  eventRenderer: FgEventRenderer
-  mirrorRenderer: FgEventRenderer
-  fillRenderer: FillRenderer
-
-  el: HTMLElement // passed in to constructor
-
-
-  constructor(el: HTMLElement) {
-    super()
-
-    this.el = el
-  }
-
-  destroy() {
-    super.destroy()
-
-    removeElement(this.el)
-  }
-
 
 
   // Hit System
   // Hit System
   // -----------------------------------------------------------------------------------------------------------------
   // -----------------------------------------------------------------------------------------------------------------
@@ -138,7 +117,7 @@ export default class DateComponent<PropsType> extends Component<PropsType> {
 
 
 
 
   isPopover() {
   isPopover() {
-    return this.el.classList.contains('fc-popover')
+    return this.mountedEls[0].classList.contains('fc-popover')
   }
   }
 
 
 
 

+ 0 - 51
packages/core/src/component/memoized-rendering.ts

@@ -1,51 +0,0 @@
-import { isArraysEqual } from '../util/array'
-
-export interface MemoizedRendering<ArgsType extends any[]> {
-  (...args: ArgsType): void
-  unrender: () => void
-  dependents: MemoizedRendering<any>[]
-}
-
-export function memoizeRendering<ArgsType extends any[]>(
-  renderFunc: (...args: ArgsType) => void,
-  unrenderFunc?: (...args: ArgsType) => void,
-  dependencies: MemoizedRendering<any>[] = []
-): MemoizedRendering<ArgsType> {
-
-  let dependents: MemoizedRendering<any>[] = []
-  let thisContext
-  let prevArgs
-
-  function unrender() {
-    if (prevArgs) {
-
-      for (let dependent of dependents) {
-        dependent.unrender()
-      }
-
-      if (unrenderFunc) {
-        unrenderFunc.apply(thisContext, prevArgs)
-      }
-
-      prevArgs = null
-    }
-  }
-
-  function res() {
-    if (!prevArgs || !isArraysEqual(prevArgs, arguments)) {
-      unrender()
-      thisContext = this
-      prevArgs = arguments
-      renderFunc.apply(this, arguments)
-    }
-  }
-
-  res.dependents = dependents
-  res.unrender = unrender
-
-  for (let dependency of dependencies) {
-    dependency.dependents.push(res)
-  }
-
-  return res
-}

+ 21 - 23
packages/core/src/component/renderers/FgEventRenderer.ts

@@ -15,7 +15,7 @@ export interface BaseFgEventRendererProps {
   segs: Seg[]
   segs: Seg[]
   mirrorInfo?: any
   mirrorInfo?: any
   selectedInstanceId?: string
   selectedInstanceId?: string
-  hiddenInstances: { [instanceId: string]: any }
+  hiddenInstances?: { [instanceId: string]: any }
 }
 }
 
 
 export default abstract class FgEventRenderer<
 export default abstract class FgEventRenderer<
@@ -37,7 +37,7 @@ export default abstract class FgEventRenderer<
   protected displayEventEnd: boolean
   protected displayEventEnd: boolean
 
 
 
 
-  renderSeg(props: FgEventRendererProps, context: ComponentContext) {
+  renderSegs(props: BaseFgEventRendererProps, context: ComponentContext) {
     this.updateComputedOptions(context.options)
     this.updateComputedOptions(context.options)
 
 
     let segs = this.segs = this.renderSegsPlain({
     let segs = this.segs = this.renderSegsPlain({
@@ -247,45 +247,30 @@ export default abstract class FgEventRenderer<
   }
   }
 
 
 
 
-  // TODO: move to outside func
-  sortEventSegs(segs): Seg[] {
-    let specs = this.context.eventOrderSpecs
-    let objs = segs.map(buildSegCompareObj)
-
-    objs.sort(function(obj0, obj1) {
-      return compareByFieldSpecs(obj0, obj1, specs)
-    })
-
-    return objs.map(function(c) {
-      return c._seg
-    })
-  }
-
-
   // Sizing
   // Sizing
   // ----------------------------------------------------------------------------------------------------
   // ----------------------------------------------------------------------------------------------------
 
 
 
 
-  computeSizes(force: boolean) {
+  computeSizes(force: boolean, userComponent: any) {
     if (force || this.isSizeDirty) {
     if (force || this.isSizeDirty) {
-      this.computeSegSizes(this.segs)
+      this.computeSegSizes(this.segs, userComponent)
     }
     }
   }
   }
 
 
 
 
-  assignSizes(force: boolean) {
+  assignSizes(force: boolean, userComponent: any) {
     if (force || this.isSizeDirty) {
     if (force || this.isSizeDirty) {
-      this.assignSegSizes(this.segs)
+      this.assignSegSizes(this.segs, userComponent)
       this.isSizeDirty = false
       this.isSizeDirty = false
     }
     }
   }
   }
 
 
 
 
-  computeSegSizes(segs: Seg[]) {
+  computeSegSizes(segs: Seg[], userComponent: any) {
   }
   }
 
 
 
 
-  assignSegSizes(segs: Seg[]) {
+  assignSegSizes(segs: Seg[], userComponent: any) {
   }
   }
 
 
 }
 }
@@ -344,6 +329,19 @@ function unrenderSelectedInstance({ segs, instanceId }: { segs: Seg[], instanceI
 }
 }
 
 
 
 
+export function sortEventSegs(segs, eventOrderSpecs): Seg[] {
+  let objs = segs.map(buildSegCompareObj)
+
+  objs.sort(function(obj0, obj1) {
+    return compareByFieldSpecs(obj0, obj1, eventOrderSpecs)
+  })
+
+  return objs.map(function(c) {
+    return c._seg
+  })
+}
+
+
 // returns a object with all primitive props that can be compared
 // returns a object with all primitive props that can be compared
 export function buildSegCompareObj(seg: Seg) {
 export function buildSegCompareObj(seg: Seg) {
   let eventDef = seg.eventRange.def
   let eventDef = seg.eventRange.def

+ 35 - 73
packages/core/src/component/renderers/FillRenderer.ts

@@ -1,71 +1,50 @@
 import { cssToStr } from '../../util/html'
 import { cssToStr } from '../../util/html'
-import { htmlToElements, removeElement, elementMatches } from '../../util/dom-manip'
+import { htmlToElements, elementMatches } from '../../util/dom-manip'
 import { Seg } from '../DateComponent'
 import { Seg } from '../DateComponent'
 import { filterSegsViaEls, triggerRenderedSegs, triggerWillRemoveSegs } from '../event-rendering'
 import { filterSegsViaEls, triggerRenderedSegs, triggerWillRemoveSegs } from '../event-rendering'
 import ComponentContext from '../ComponentContext'
 import ComponentContext from '../ComponentContext'
+import { Component, renderer} from '../../view-framework'
 
 
-/*
-TODO: when refactoring this class, make a new FillRenderer instance for each `type`
-*/
-export default abstract class FillRenderer { // use for highlight, background events, business hours
-
-  context: ComponentContext
-  fillSegTag: string = 'div'
-  containerElsByType: any // a hash of element sets used for rendering each fill. Keyed by fill name.
-  segsByType: any
-  dirtySizeFlags: any = {}
-
-
-  constructor() {
-    this.containerElsByType = {}
-    this.segsByType = {}
-  }
+export interface BaseFillRendererProps {
+  segs: Seg[]
+  type: string
+}
 
 
+// use for highlight, background events, business hours
+export default abstract class FillRenderer<FillRendererProps extends BaseFillRendererProps> extends Component<FillRendererProps> {
 
 
-  getSegsByType(type: string) {
-    return this.segsByType[type] || []
-  }
+  renderSegs = renderer(this._renderSegs, this._unrenderSegs)
 
 
+  fillSegTag: string = 'div'
 
 
-  renderSegs(type: string, context: ComponentContext, segs: Seg[]) {
-    this.context = context
-
-    let renderedSegs = this.renderSegEls(type, segs) // assignes `.el` to each seg. returns successfully rendered segs
-    let containerEls = this.attachSegs(type, renderedSegs)
+  // for sizing
+  private segs: Seg[]
+  private isSizeDirty = false
 
 
-    if (containerEls) {
-      (this.containerElsByType[type] || (this.containerElsByType[type] = []))
-        .push(...containerEls)
-    }
 
 
-    this.segsByType[type] = renderedSegs
+  _renderSegs(props: BaseFillRendererProps, context: ComponentContext) {
+    let segs = this.segs = this.renderSegEls(props.segs, props.type) // assignes `.el` to each seg. returns successfully rendered segs
 
 
-    if (type === 'bgEvent') {
-      triggerRenderedSegs(context, renderedSegs, false) // isMirror=false
+    if (props.type === 'bgEvent') {
+      triggerRenderedSegs(context, segs, false) // isMirror=false
     }
     }
 
 
-    this.dirtySizeFlags[type] = true
+    this.isSizeDirty = true
+    return segs
   }
   }
 
 
 
 
   // Unrenders a specific type of fill that is currently rendered on the grid
   // Unrenders a specific type of fill that is currently rendered on the grid
-  unrender(type: string, context: ComponentContext) {
-    let segs = this.segsByType[type]
-
-    if (segs) {
-
-      if (type === 'bgEvent') {
-        triggerWillRemoveSegs(context, segs, false) // isMirror=false
-      }
-
-      this.detachSegs(type, segs)
+  _unrenderSegs(props: BaseFillRendererProps, context: ComponentContext, segs: Seg[]) {
+    if (props.type === 'bgEvent') {
+      triggerWillRemoveSegs(context, segs, false) // isMirror=false. will use publiclyTriggerAfterSizing so will fire after
     }
     }
   }
   }
 
 
 
 
   // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
   // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
   // Only returns segments that successfully rendered.
   // Only returns segments that successfully rendered.
-  renderSegEls(type, segs: Seg[]) {
+  renderSegEls(segs: Seg[], type: string) {
     let html = ''
     let html = ''
     let i
     let i
 
 
@@ -73,7 +52,7 @@ export default abstract class FillRenderer { // use for highlight, background ev
 
 
       // build a large concatenation of segment HTML
       // build a large concatenation of segment HTML
       for (i = 0; i < segs.length; i++) {
       for (i = 0; i < segs.length; i++) {
-        html += this.renderSegHtml(type, segs[i])
+        html += this.renderSegHtml(segs[i])
       }
       }
 
 
       // Grab individual elements from the combined HTML string. Use each as the default rendering.
       // Grab individual elements from the combined HTML string. Use each as the default rendering.
@@ -105,7 +84,8 @@ export default abstract class FillRenderer { // use for highlight, background ev
 
 
 
 
   // Builds the HTML needed for one fill segment. Generic enough to work with different types.
   // Builds the HTML needed for one fill segment. Generic enough to work with different types.
-  renderSegHtml(type, seg: Seg) {
+  renderSegHtml(seg: Seg) {
+    let { type } = this.props
     let css = null
     let css = null
     let classNames = []
     let classNames = []
 
 
@@ -132,44 +112,26 @@ export default abstract class FillRenderer { // use for highlight, background ev
   }
   }
 
 
 
 
-  abstract attachSegs(type, segs: Seg[]): HTMLElement[] | void
-
-
-  detachSegs(type, segs: Seg[]) {
-    let containerEls = this.containerElsByType[type]
-
-    if (containerEls) {
-      containerEls.forEach(removeElement)
-      delete this.containerElsByType[type]
+  computeSizes(force: boolean, userComponent: any) {
+    if (force || this.isSizeDirty) {
+      this.computeSegSizes(this.segs, userComponent)
     }
     }
   }
   }
 
 
 
 
-  computeSizes(force: boolean) {
-    for (let type in this.segsByType) {
-      if (force || this.dirtySizeFlags[type]) {
-        this.computeSegSizes(this.segsByType[type])
-      }
+  assignSizes(force: boolean, userComponent: any) {
+    if (force || this.isSizeDirty) {
+      this.assignSegSizes(this.segs, userComponent)
+      this.isSizeDirty = false
     }
     }
   }
   }
 
 
 
 
-  assignSizes(force: boolean) {
-    for (let type in this.segsByType) {
-      if (force || this.dirtySizeFlags[type]) {
-        this.assignSegSizes(this.segsByType[type])
-      }
-    }
-
-    this.dirtySizeFlags = {}
-  }
-
-
-  computeSegSizes(segs: Seg[]) {
+  computeSegSizes(seg: Seg[], userComponent: any) {
   }
   }
 
 
 
 
-  assignSegSizes(segs: Seg[]) {
+  assignSegSizes(seg: Seg[], userComponent: any) {
   }
   }
 
 
 }
 }

+ 2 - 3
packages/core/src/structs/view-config.ts

@@ -1,7 +1,7 @@
 import View from '../View'
 import View from '../View'
-import { ViewSpec } from './view-spec'
 import { refineProps } from '../util/misc'
 import { refineProps } from '../util/misc'
 import { mapHash } from '../util/object'
 import { mapHash } from '../util/object'
+import { RenderEngine } from '../view-framework'
 
 
 /*
 /*
 A view-config represents information for either:
 A view-config represents information for either:
@@ -10,8 +10,7 @@ B) options to customize an existing view, in which case only provides options.
 */
 */
 
 
 export type ViewClass = new(
 export type ViewClass = new(
-  viewSpec: ViewSpec,
-  parentEl: HTMLElement
+  renderEngine: RenderEngine
 ) => View
 ) => View
 
 
 export interface ViewConfigObjectInput {
 export interface ViewConfigObjectInput {

+ 8 - 12
packages/daygrid/src/DayBgRow.ts

@@ -3,7 +3,9 @@ import {
   DateMarker,
   DateMarker,
   getDayClasses,
   getDayClasses,
   rangeContainsMarker,
   rangeContainsMarker,
-  DateProfile
+  DateProfile,
+  CCComponent as Component,
+  createElement
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 
 
 export interface DayBgCell {
 export interface DayBgCell {
@@ -17,15 +19,9 @@ export interface DayBgRowProps {
   renderIntroHtml?: () => string
   renderIntroHtml?: () => string
 }
 }
 
 
-export default class DayBgRow {
+export default class DayBgRow extends Component<DayBgRowProps> {
 
 
-  context: ComponentContext
-
-  constructor(context: ComponentContext) {
-    this.context = context
-  }
-
-  renderHtml(props: DayBgRowProps) {
+  render(props: DayBgRowProps, context: ComponentContext) {
     let parts = []
     let parts = []
 
 
     if (props.renderIntroHtml) {
     if (props.renderIntroHtml) {
@@ -37,21 +33,21 @@ export default class DayBgRow {
         renderCellHtml(
         renderCellHtml(
           cell.date,
           cell.date,
           props.dateProfile,
           props.dateProfile,
-          this.context,
+          context,
           cell.htmlAttrs
           cell.htmlAttrs
         )
         )
       )
       )
     }
     }
 
 
     if (!props.cells.length) {
     if (!props.cells.length) {
-      parts.push('<td class="fc-day ' + this.context.theme.getClass('widgetContent') + '"></td>')
+      parts.push('<td class="fc-day ' + context.theme.getClass('widgetContent') + '"></td>')
     }
     }
 
 
     if (this.context.options.dir === 'rtl') {
     if (this.context.options.dir === 'rtl') {
       parts.reverse()
       parts.reverse()
     }
     }
 
 
-    return '<tr>' + parts.join('') + '</tr>'
+    return createElement('tr', {}, parts.join(''))
   }
   }
 
 
 }
 }

+ 4 - 6
packages/list/src/ListView.ts

@@ -45,11 +45,9 @@ export default class ListView extends View {
       viewSpec: props.viewSpec
       viewSpec: props.viewSpec
     })
     })
 
 
-    this.scroller = this.renderScroller({
+    this.scroller = this.renderScroller(rootEl, {
       overflowX: 'hidden',
       overflowX: 'hidden',
       overflowY: 'auto'
       overflowY: 'auto'
-    }, {
-      parent: rootEl
     })
     })
 
 
     this.eventRenderer = this.renderEvents({
     this.eventRenderer = this.renderEvents({
@@ -58,11 +56,11 @@ export default class ListView extends View {
       contentEl: this.scroller.el,
       contentEl: this.scroller.el,
       selectedInstanceId: props.eventSelection, // TODO: rename
       selectedInstanceId: props.eventSelection, // TODO: rename
       hiddenInstances: // TODO: more convenient
       hiddenInstances: // TODO: more convenient
-        (props.eventDrag ? props.eventDrag.affectedEvents : null) ||
-        (props.eventResize ? props.eventResize.affectedEvents : null)
+        (props.eventDrag ? props.eventDrag.affectedEvents.instances : null) ||
+        (props.eventResize ? props.eventResize.affectedEvents.instances : null)
     })
     })
 
 
-    return [ rootEl ]
+    return rootEl
   }
   }
 
 
 
 

+ 276 - 325
packages/timegrid/src/TimeGrid.ts

@@ -22,10 +22,9 @@ import {
   Seg,
   Seg,
   EventSegUiInteractionState,
   EventSegUiInteractionState,
   DateProfile,
   DateProfile,
-  memoizeRendering,
-  MemoizedRendering,
-  Theme,
-  memoize
+  sortEventSegs,
+  memoize,
+  renderer,
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import { DayBgRow } from '@fullcalendar/daygrid'
 import { DayBgRow } from '@fullcalendar/daygrid'
 import TimeGridEventRenderer from './TimeGridEventRenderer'
 import TimeGridEventRenderer from './TimeGridEventRenderer'
@@ -63,6 +62,7 @@ export interface TimeGridCell {
 }
 }
 
 
 export interface TimeGridProps {
 export interface TimeGridProps {
+  renderProps: RenderProps
   dateProfile: DateProfile
   dateProfile: DateProfile
   cells: TimeGridCell[]
   cells: TimeGridCell[]
   businessHourSegs: TimeGridSeg[]
   businessHourSegs: TimeGridSeg[]
@@ -76,15 +76,13 @@ export interface TimeGridProps {
 
 
 export default class TimeGrid extends DateComponent<TimeGridProps> {
 export default class TimeGrid extends DateComponent<TimeGridProps> {
 
 
-  renderProps: RenderProps
-
+  // computed options
   slotDuration: Duration // duration of a "slot", a distinct time segment on given day, visualized by lines
   slotDuration: Duration // duration of a "slot", a distinct time segment on given day, visualized by lines
   snapDuration: Duration // granularity of time for dragging and selecting
   snapDuration: Duration // granularity of time for dragging and selecting
   snapsPerSlot: any
   snapsPerSlot: any
   labelFormat: DateFormatter // formatting string for times running along vertical axis
   labelFormat: DateFormatter // formatting string for times running along vertical axis
   labelInterval: Duration // duration of how often a label should be displayed for a slot
   labelInterval: Duration // duration of how often a label should be displayed for a slot
 
 
-  colCnt: number
   colEls: HTMLElement[] // cells elements in the day-row background
   colEls: HTMLElement[] // cells elements in the day-row background
   slatContainerEl: HTMLElement // div that wraps all the slat rows
   slatContainerEl: HTMLElement // div that wraps all the slat rows
   slatEls: HTMLElement[] // elements running horizontally across all columns
   slatEls: HTMLElement[] // elements running horizontally across all columns
@@ -95,83 +93,23 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
   isSlatSizesDirty: boolean = false
   isSlatSizesDirty: boolean = false
   isColSizesDirty: boolean = false
   isColSizesDirty: boolean = false
 
 
-  rootBgContainerEl: HTMLElement
-  bottomRuleEl: HTMLElement // hidden by default
+  bottomRuleEl: HTMLElement // hidden by default. controlled by parent components!?
   contentSkeletonEl: HTMLElement
   contentSkeletonEl: HTMLElement
   colContainerEls: HTMLElement[] // containers for each column
   colContainerEls: HTMLElement[] // containers for each column
 
 
-  // inner-containers for each column where different types of segs live
-  fgContainerEls: HTMLElement[]
-  bgContainerEls: HTMLElement[]
-  mirrorContainerEls: HTMLElement[]
-  highlightContainerEls: HTMLElement[]
-  businessContainerEls: HTMLElement[]
-
-  private processOptions = memoize(this._processOptions)
-  private renderSkeleton = memoizeRendering(this._renderSkeleton)
-  private renderSlats = memoizeRendering(this._renderSlats, null, [ this.renderSkeleton ])
-  private renderColumns = memoizeRendering(this._renderColumns, this._unrenderColumns, [ this.renderSkeleton ])
-  private renderBusinessHours: MemoizedRendering<[ComponentContext, TimeGridSeg[]]>
-  private renderDateSelection: MemoizedRendering<[TimeGridSeg[]]>
-  private renderBgEvents: MemoizedRendering<[ComponentContext, TimeGridSeg[]]>
-  private renderFgEvents: MemoizedRendering<[ComponentContext, TimeGridSeg[]]>
-  private renderEventSelection: MemoizedRendering<[string]>
-  private renderEventDrag: MemoizedRendering<[EventSegUiInteractionState]>
-  private renderEventResize: MemoizedRendering<[EventSegUiInteractionState]>
-
-
-  constructor(el: HTMLElement, renderProps: RenderProps) {
-    super(el)
-
-    this.renderProps = renderProps
-
-    let { renderColumns } = this
-    let eventRenderer = this.eventRenderer = new TimeGridEventRenderer(this)
-    let fillRenderer = this.fillRenderer = new TimeGridFillRenderer(this)
-    this.mirrorRenderer = new TimeGridMirrorRenderer(this)
-
-    this.renderBusinessHours = memoizeRendering(
-      fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'),
-      fillRenderer.unrender.bind(fillRenderer, 'businessHours'),
-      [ renderColumns ]
-    )
+  processOptions = memoize(this._processOptions)
+  renderSkeleton = renderer(renderSkeleton)
+  renderSlats = renderer(this._renderSlats)
+  renderBgColumns = renderer(this._renderBgColumns)
+  renderDayBgRow = renderer(DayBgRow)
+  renderContentSkeleton = renderer(renderContentSkeleton)
+  renderMirrorEvents = renderer(TimeGridMirrorRenderer)
+  renderFgEvents = renderer(TimeGridEventRenderer)
+  renderBgEvents = renderer(TimeGridFillRenderer)
+  renderBusinessHours = renderer(TimeGridFillRenderer)
+  renderDateSelection = renderer(TimeGridFillRenderer)
 
 
-    this.renderDateSelection = memoizeRendering(
-      this._renderDateSelection,
-      this._unrenderDateSelection,
-      [ renderColumns ]
-    )
-
-    this.renderFgEvents = memoizeRendering(
-      eventRenderer.renderSegs.bind(eventRenderer),
-      eventRenderer.unrender.bind(eventRenderer),
-      [ renderColumns ]
-    )
-
-    this.renderBgEvents = memoizeRendering(
-      fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'),
-      fillRenderer.unrender.bind(fillRenderer, 'bgEvent'),
-      [ renderColumns ]
-    )
-
-    this.renderEventSelection = memoizeRendering(
-      eventRenderer.selectByInstanceId.bind(eventRenderer),
-      eventRenderer.unselectByInstanceId.bind(eventRenderer),
-      [ this.renderFgEvents ]
-    )
-
-    this.renderEventDrag = memoizeRendering(
-      this._renderEventDrag,
-      this._unrenderEventDrag,
-      [ renderColumns ]
-    )
-
-    this.renderEventResize = memoizeRendering(
-      this._renderEventResize,
-      this._unrenderEventResize,
-      [ renderColumns ]
-    )
-  }
+  segRenderers: (TimeGridEventRenderer | TimeGridFillRenderer)[]
 
 
 
 
   /* Options
   /* Options
@@ -216,26 +154,7 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
     input = options.slotLabelInterval
     input = options.slotLabelInterval
     this.labelInterval = input ?
     this.labelInterval = input ?
       createDuration(input) :
       createDuration(input) :
-      this.computeLabelInterval(slotDuration)
-  }
-
-
-  // Computes an automatic value for slotLabelInterval
-  computeLabelInterval(slotDuration) {
-    let i
-    let labelInterval
-    let slotsPerLabel
-
-    // find the smallest stock label interval that results in more than one slots-per-label
-    for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
-      labelInterval = createDuration(AGENDA_STOCK_SUB_DURATIONS[i])
-      slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration)
-      if (slotsPerLabel !== null && slotsPerLabel > 1) {
-        return labelInterval
-      }
-    }
-
-    return slotDuration // fall back
+      computeLabelInterval(slotDuration)
   }
   }
 
 
 
 
@@ -244,36 +163,112 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
 
 
 
 
   render(props: TimeGridProps, context: ComponentContext) {
   render(props: TimeGridProps, context: ComponentContext) {
-    this.processOptions(context.options)
-
-    let cells = props.cells
-    this.colCnt = cells.length
-
-    this.renderSkeleton(context.theme)
-    this.renderSlats(props.dateProfile)
-    this.renderColumns(props.cells, props.dateProfile)
-    this.renderBusinessHours(context, props.businessHourSegs)
-    this.renderDateSelection(props.dateSelectionSegs)
-    this.renderFgEvents(context, props.fgEventSegs)
-    this.renderBgEvents(context, props.bgEventSegs)
-    this.renderEventSelection(props.eventSelection)
-    this.renderEventDrag(props.eventDrag)
-    this.renderEventResize(props.eventResize)
+    let { options } = context
+    this.processOptions(options)
+
+    let {
+      rootEl,
+      rootBgContainerEl,
+      contentSkeletonEl,
+      bottomRuleEl,
+      slatContainerEl
+    } = this.renderSkeleton()
+
+    this.renderBgColumns(rootBgContainerEl, {
+      rootEl,
+      cells: props.cells,
+      dateProfile: props.dateProfile,
+      renderProps: props.renderProps
+    })
+
+    this.renderSlats(slatContainerEl, {
+      rootEl,
+      dateProfile: props.dateProfile
+    })
+
+    let {
+      colContainerEls,
+      businessContainerEls,
+      bgContainerEls,
+      fgContainerEls,
+      highlightContainerEls,
+      mirrorContainerEls
+    } = this.renderContentSkeleton(contentSkeletonEl, {
+      colCnt: props.cells.length,
+      renderProps: props.renderProps
+    })
+
+    let segRenderers = [
+      this.renderBusinessHours({
+        type: 'businessHours',
+        containerEls: businessContainerEls,
+        segs: props.businessHourSegs,
+      }),
+      this.renderDateSelection({
+        type: 'highlight',
+        containerEls: highlightContainerEls,
+        segs: options.selectMirror ? null : props.dateSelectionSegs
+      }),
+      this.renderBgEvents({
+        type: 'bgEvent',
+        containerEls: bgContainerEls,
+        segs: props.bgEventSegs
+      }),
+      this.renderFgEvents({
+        containerEls: fgContainerEls,
+        segs: props.fgEventSegs,
+        selectedInstanceId: props.eventSelection, // TODO: rename
+        hiddenInstances: // TODO: more convenient
+          (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
+          (props.eventResize ? props.eventResize.affectedInstances : null)
+      })
+    ]
+
+    let mirrorRenderer = this.handleMirror(props, mirrorContainerEls, options)
+    if (mirrorRenderer) {
+      segRenderers.push(mirrorRenderer)
+    }
+
+    this.segRenderers = segRenderers
+    this.contentSkeletonEl = contentSkeletonEl
+    this.bottomRuleEl = bottomRuleEl
+    this.colContainerEls = colContainerEls
+
+    return rootEl
   }
   }
 
 
 
 
-  destroy() {
-    super.destroy()
+  handleMirror(props: TimeGridProps, mirrorContainerEls: HTMLElement[], options): TimeGridEventRenderer | null {
+
+    if (props.dateSelectionSegs && options.selectMirror) { // can use non-existent like this!?
+      return this.renderMirrorEvents({
+        containerEls: mirrorContainerEls,
+        segs: props.dateSelectionSegs,
+        mirrorInfo: { isSelecting: true }
+      })
 
 
-    // should unrender everything else too
-    this.renderSlats.unrender()
-    this.renderColumns.unrender()
-    this.renderSkeleton.unrender()
+    } else if (props.eventDrag) {
+      return this.renderMirrorEvents({
+        containerEls: mirrorContainerEls,
+        segs: props.eventDrag.segs,
+        mirrorInfo: { isDragging: true, sourceSeg: props.eventDrag.sourceSeg }
+      })
+
+    } else if (props.eventResize) {
+      return this.renderMirrorEvents({
+        containerEls: mirrorContainerEls,
+        segs: props.eventResize.segs,
+        mirrorInfo: { isDragging: true, sourceSeg: props.eventResize.sourceSeg }
+      })
+
+    } else {
+      return this.renderMirrorEvents(false)
+    }
   }
   }
 
 
 
 
   updateSize(isResize: boolean) {
   updateSize(isResize: boolean) {
-    let { fillRenderer, eventRenderer, mirrorRenderer } = this
+    let { segRenderers } = this
 
 
     if (isResize || this.isSlatSizesDirty) {
     if (isResize || this.isSlatSizesDirty) {
       this.buildSlatPositions()
       this.buildSlatPositions()
@@ -285,48 +280,37 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
       this.isColSizesDirty = false
       this.isColSizesDirty = false
     }
     }
 
 
-    fillRenderer.computeSizes(isResize)
-    eventRenderer.computeSizes(isResize)
-    mirrorRenderer.computeSizes(isResize)
-
-    fillRenderer.assignSizes(isResize)
-    eventRenderer.assignSizes(isResize)
-    mirrorRenderer.assignSizes(isResize)
-  }
-
-
-  _renderSkeleton(theme: Theme) {
-    let { el } = this
-
-    el.innerHTML =
-      '<div class="fc-bg"></div>' +
-      '<div class="fc-slats"></div>' +
-      '<hr class="fc-divider ' + theme.getClass('widgetHeader') + '" style="display:none" />'
+    for (let segRenderer of segRenderers) {
+      segRenderer.computeSizes(isResize, this)
+    }
 
 
-    this.rootBgContainerEl = el.querySelector('.fc-bg')
-    this.slatContainerEl = el.querySelector('.fc-slats')
-    this.bottomRuleEl = el.querySelector('.fc-divider')
+    for (let segRenderer of segRenderers) {
+      segRenderer.assignSizes(isResize, this)
+    }
   }
   }
 
 
 
 
-  _renderSlats(dateProfile: DateProfile) {
-    let { theme } = this.context
-
-    this.slatContainerEl.innerHTML =
-      '<table class="' + theme.getClass('tableGrid') + '">' +
-        this.renderSlatRowHtml(dateProfile) +
-      '</table>'
+  _renderSlats(
+    { rootEl, dateProfile }: { rootEl: HTMLElement, dateProfile: DateProfile },
+    context: ComponentContext
+  ) {
+    let tableEl = createElement(
+      'table',
+      { className: context.theme.getClass('tableGrid') },
+      this.renderSlatRowHtml(dateProfile)
+    )
 
 
-    this.slatEls = findElements(this.slatContainerEl, 'tr')
+    let slatEls = this.slatEls = findElements(tableEl, 'tr')
 
 
     this.slatPositions = new PositionCache(
     this.slatPositions = new PositionCache(
-      this.el,
-      this.slatEls,
+      rootEl,
+      slatEls,
       false,
       false,
       true // vertical
       true // vertical
     )
     )
-
     this.isSlatSizesDirty = true
     this.isSlatSizesDirty = true
+
+    return [ tableEl ]
   }
   }
 
 
 
 
@@ -373,26 +357,29 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
   }
   }
 
 
 
 
-  _renderColumns(cells: TimeGridCell[], dateProfile: DateProfile) {
-    let { calendar, view, isRtl, theme, dateEnv } = this.context
+  // goes behind the slats
+  _renderBgColumns(
+    { rootEl, cells, dateProfile, renderProps }: { rootEl: HTMLElement, cells: TimeGridCell[], dateProfile: DateProfile, renderProps: any },
+    context: ComponentContext
+  ) {
+    let { calendar, view, isRtl, theme, dateEnv } = context
 
 
-    let bgRow = new DayBgRow(this.context)
-    this.rootBgContainerEl.innerHTML =
-      '<table class="' + theme.getClass('tableGrid') + '">' +
-        bgRow.renderHtml({
-          cells,
-          dateProfile,
-          renderIntroHtml: this.renderProps.renderBgIntroHtml
-        }) +
-      '</table>'
+    let tableEl = createElement('table', { className: theme.getClass('tableGrid') }, '<tbody />')
+    let tbodyEl = tableEl.querySelector('tbody')
+
+    this.renderDayBgRow(tbodyEl, {
+      cells,
+      dateProfile,
+      renderIntroHtml: renderProps.renderBgIntroHtml
+    })
 
 
-    this.colEls = findElements(this.el, '.fc-day, .fc-disabled-day')
+    let colEls = this.colEls = findElements(tbodyEl, '.fc-day, .fc-disabled-day')
 
 
-    for (let col = 0; col < this.colCnt; col++) {
+    for (let col = 0; col < cells.length; col++) {
       calendar.publiclyTrigger('dayRender', [
       calendar.publiclyTrigger('dayRender', [
         {
         {
           date: dateEnv.toDate(cells[col].date),
           date: dateEnv.toDate(cells[col].date),
-          el: this.colEls[col],
+          el: colEls[col],
           view
           view
         }
         }
       ])
       ])
@@ -403,118 +390,14 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
     }
     }
 
 
     this.colPositions = new PositionCache(
     this.colPositions = new PositionCache(
-      this.el,
-      this.colEls,
+      rootEl,
+      colEls,
       true, // horizontal
       true, // horizontal
       false
       false
     )
     )
-
-    this.renderContentSkeleton()
     this.isColSizesDirty = true
     this.isColSizesDirty = true
-  }
 
 
-
-  _unrenderColumns() {
-    this.unrenderContentSkeleton()
-  }
-
-
-  /* Content Skeleton
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  // Renders the DOM that the view's content will live in
-  renderContentSkeleton() {
-    let { isRtl } = this.context
-    let parts = []
-    let skeletonEl: HTMLElement
-
-    parts.push(
-      this.renderProps.renderIntroHtml()
-    )
-
-    for (let i = 0; i < this.colCnt; i++) {
-      parts.push(
-        '<td>' +
-          '<div class="fc-content-col">' +
-            '<div class="fc-event-container fc-mirror-container"></div>' +
-            '<div class="fc-event-container"></div>' +
-            '<div class="fc-highlight-container"></div>' +
-            '<div class="fc-bgevent-container"></div>' +
-            '<div class="fc-business-container"></div>' +
-          '</div>' +
-        '</td>'
-      )
-    }
-
-    if (isRtl) {
-      parts.reverse()
-    }
-
-    skeletonEl = this.contentSkeletonEl = htmlToElement(
-      '<div class="fc-content-skeleton">' +
-        '<table>' +
-          '<tr>' + parts.join('') + '</tr>' +
-        '</table>' +
-      '</div>'
-    )
-
-    this.colContainerEls = findElements(skeletonEl, '.fc-content-col')
-    this.mirrorContainerEls = findElements(skeletonEl, '.fc-mirror-container')
-    this.fgContainerEls = findElements(skeletonEl, '.fc-event-container:not(.fc-mirror-container)')
-    this.bgContainerEls = findElements(skeletonEl, '.fc-bgevent-container')
-    this.highlightContainerEls = findElements(skeletonEl, '.fc-highlight-container')
-    this.businessContainerEls = findElements(skeletonEl, '.fc-business-container')
-
-    if (isRtl) {
-      this.colContainerEls.reverse()
-      this.mirrorContainerEls.reverse()
-      this.fgContainerEls.reverse()
-      this.bgContainerEls.reverse()
-      this.highlightContainerEls.reverse()
-      this.businessContainerEls.reverse()
-    }
-
-    this.el.appendChild(skeletonEl)
-  }
-
-
-  unrenderContentSkeleton() {
-    removeElement(this.contentSkeletonEl)
-  }
-
-
-  // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
-  groupSegsByCol(segs) {
-    let segsByCol = []
-    let i
-
-    for (i = 0; i < this.colCnt; i++) {
-      segsByCol.push([])
-    }
-
-    for (i = 0; i < segs.length; i++) {
-      segsByCol[segs[i].col].push(segs[i])
-    }
-
-    return segsByCol
-  }
-
-
-  // Given segments grouped by column, insert the segments' elements into a parallel array of container
-  // elements, each living within a column.
-  attachSegsByCol(segsByCol, containerEls: HTMLElement[]) {
-    let col
-    let segs
-    let i
-
-    for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
-      segs = segsByCol[col]
-
-      for (i = 0; i < segs.length; i++) {
-        containerEls[col].appendChild(segs[i].el)
-      }
-    }
+    return [ tableEl ]
   }
   }
 
 
 
 
@@ -614,7 +497,7 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
 
 
 
 
   // For each segment in an array, computes and assigns its top and bottom properties
   // For each segment in an array, computes and assigns its top and bottom properties
-  computeSegVerticals(segs) {
+  computeSegVerticals(segs: Seg[]) {
     let { options } = this.context
     let { options } = this.context
     let eventMinHeight = options.timeGridEventMinHeight
     let eventMinHeight = options.timeGridEventMinHeight
     let i
     let i
@@ -719,81 +602,149 @@ export default class TimeGrid extends DateComponent<TimeGridProps> {
     }
     }
   }
   }
 
 
-
-  /* Event Drag Visualization
-  ------------------------------------------------------------------------------------------------------------------*/
+}
 
 
 
 
-  _renderEventDrag(state: EventSegUiInteractionState) {
-    if (state) {
-      this.eventRenderer.hideByHash(state.affectedInstances)
+// Computes an automatic value for slotLabelInterval
+function computeLabelInterval(slotDuration) {
+  let i
+  let labelInterval
+  let slotsPerLabel
 
 
-      if (state.isEvent) {
-        this.mirrorRenderer.renderSegs(this.context, state.segs, { isDragging: true, sourceSeg: state.sourceSeg })
-      } else {
-        this.fillRenderer.renderSegs('highlight', this.context, state.segs)
-      }
+  // find the smallest stock label interval that results in more than one slots-per-label
+  for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
+    labelInterval = createDuration(AGENDA_STOCK_SUB_DURATIONS[i])
+    slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration)
+    if (slotsPerLabel !== null && slotsPerLabel > 1) {
+      return labelInterval
     }
     }
   }
   }
 
 
+  return slotDuration // fall back
+}
+
 
 
-  _unrenderEventDrag(state: EventSegUiInteractionState) {
-    if (state) {
-      this.eventRenderer.showByHash(state.affectedInstances)
+function renderSkeleton(props: {}, context: ComponentContext) {
+  let rootEl = createElement(
+    'div',
+    { className: 'fc-time-grid' },
+    '<div class="fc-bg"></div>' +
+    '<div class="fc-slats"></div>' +
+    '<div class="fc-content-skeleton">' +
+    '<hr class="fc-divider ' + context.theme.getClass('widgetHeader') + '" style="display:none" />'
+  )
 
 
-      if (state.isEvent) {
-        this.mirrorRenderer.unrender(this.context, state.segs, { isDragging: true, sourceSeg: state.sourceSeg })
-      } else {
-        this.fillRenderer.unrender('highlight', this.context)
-      }
-    }
+  return {
+    rootEl,
+    rootBgContainerEl: rootEl.querySelector('.fc-bg') as HTMLElement,
+    slatContainerEl: rootEl.querySelector('.fc-slats') as HTMLElement,
+    contentSkeletonEl: rootEl.querySelector('.fc-content-skeleton') as HTMLElement,
+    bottomRuleEl: rootEl.querySelector('.fc-divider') as HTMLElement
   }
   }
+}
 
 
 
 
-  /* Event Resize Visualization
-  ------------------------------------------------------------------------------------------------------------------*/
+// Renders the DOM that the view's content will live in
+// goes in front of the slats
+function renderContentSkeleton({ colCnt, renderProps }: { colCnt: number, renderProps: any  }, context: ComponentContext) {
+  let { isRtl } = context
+  let parts = []
 
 
+  parts.push(
+    renderProps.renderIntroHtml()
+  )
 
 
-  _renderEventResize(state: EventSegUiInteractionState) {
-    if (state) {
-      this.eventRenderer.hideByHash(state.affectedInstances)
-      this.mirrorRenderer.renderSegs(this.context, state.segs, { isResizing: true, sourceSeg: state.sourceSeg })
-    }
+  for (let i = 0; i < colCnt; i++) {
+    parts.push(
+      '<td>' +
+        '<div class="fc-content-col">' +
+          '<div class="fc-event-container fc-mirror-container"></div>' +
+          '<div class="fc-event-container"></div>' +
+          '<div class="fc-highlight-container"></div>' +
+          '<div class="fc-bgevent-container"></div>' +
+          '<div class="fc-business-container"></div>' +
+        '</div>' +
+      '</td>'
+    )
   }
   }
 
 
+  if (isRtl) {
+    parts.reverse()
+  }
 
 
-  _unrenderEventResize(state: EventSegUiInteractionState) {
-    if (state) {
-      this.eventRenderer.showByHash(state.affectedInstances)
-      this.mirrorRenderer.unrender(this.context, state.segs, { isResizing: true, sourceSeg: state.sourceSeg })
-    }
+  let tableEl = htmlToElement(
+    '<div class="fc-content-skeleton">' +
+      '<table>' +
+        '<tr>' + parts.join('') + '</tr>' +
+      '</table>' +
+    '</div>'
+  )
+
+  let colContainerEls = findElements(tableEl, '.fc-content-col')
+  let mirrorContainerEls = findElements(tableEl, '.fc-mirror-container')
+  let fgContainerEls = findElements(tableEl, '.fc-event-container:not(.fc-mirror-container)')
+  let bgContainerEls = findElements(tableEl, '.fc-bgevent-container')
+  let highlightContainerEls = findElements(tableEl, '.fc-highlight-container')
+  let businessContainerEls = findElements(tableEl, '.fc-business-container')
+
+  if (isRtl) {
+    colContainerEls.reverse()
+    mirrorContainerEls.reverse()
+    fgContainerEls.reverse()
+    bgContainerEls.reverse()
+    highlightContainerEls.reverse()
+    businessContainerEls.reverse()
   }
   }
 
 
+  return {
+    rootEl: tableEl,
+    colContainerEls,
+    businessContainerEls,
+    bgContainerEls,
+    fgContainerEls,
+    highlightContainerEls,
+    mirrorContainerEls
+  }
+}
 
 
-  /* Selection
-  ------------------------------------------------------------------------------------------------------------------*/
 
 
+// Given segments grouped by column, insert the segments' elements into a parallel array of container
+// elements, each living within a column.
+export function attachSegs({ segs, containerEls }: { segs: Seg[], containerEls: HTMLElement[] }, context: ComponentContext) {
+  let segsByCol = groupSegsByCol(segs, containerEls.length)
 
 
-  // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
-  _renderDateSelection(segs: Seg[]) {
-    if (segs) {
-      if (this.context.options.selectMirror) {
-        this.mirrorRenderer.renderSegs(this.context, segs, { isSelecting: true })
-      } else {
-        this.fillRenderer.renderSegs('highlight', this.context, segs)
-      }
-    }
+  for (let col = 0; col < segsByCol.length; col++) {
+    segsByCol[col] = sortEventSegs(segsByCol[col], context.eventOrderSpecs)
   }
   }
 
 
+  for (let col = 0; col < containerEls.length; col++) { // iterate each column grouping
+    let segs = segsByCol[col]
 
 
-  _unrenderDateSelection(segs: Seg[]) {
-    if (segs) {
-      if (this.context.options.selectMirror) {
-        this.mirrorRenderer.unrender(this.context, segs, { isSelecting: true })
-      } else {
-        this.fillRenderer.unrender('highlight', this.context)
-      }
+    for (let seg of segs) {
+      containerEls[col].appendChild(seg.el)
     }
     }
   }
   }
+}
+
+
+export function detachSegs({ segs, containerEls }: { segs: Seg[], containerEls: HTMLElement[] }) {
+  segs.forEach(function(seg) {
+    removeElement(seg.el)
+  })
+}
+
+
+function groupSegsByCol(segs, colCnt) {
+  let segsByCol = []
+  let i
+
+  for (i = 0; i < colCnt; i++) {
+    segsByCol.push([])
+  }
+
+  for (i = 0; i < segs.length; i++) {
+    segsByCol[segs[i].col].push(segs[i])
+  }
 
 
+  return segsByCol
 }
 }

+ 152 - 168
packages/timegrid/src/TimeGridEventRenderer.ts

@@ -1,93 +1,136 @@
 import {
 import {
   htmlEscape, cssToStr,
   htmlEscape, cssToStr,
-  removeElement, applyStyle,
+  applyStyle,
   createFormatter, DateFormatter,
   createFormatter, DateFormatter,
   FgEventRenderer, buildSegCompareObj,
   FgEventRenderer, buildSegCompareObj,
   Seg, isMultiDayRange, compareByFieldSpecs,
   Seg, isMultiDayRange, compareByFieldSpecs,
-  computeEventDraggable, computeEventStartResizable, computeEventEndResizable, ComponentContext
+  computeEventDraggable, computeEventStartResizable, computeEventEndResizable, ComponentContext, BaseFgEventRendererProps, renderer
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
-import TimeGrid from './TimeGrid'
+import TimeGrid, { attachSegs, detachSegs } from './TimeGrid'
+
+export interface TimeGridEventRendererProps extends BaseFgEventRendererProps {
+  containerEls: HTMLElement[]
+}
 
 
 /*
 /*
 Only handles foreground segs.
 Only handles foreground segs.
 Does not own rendering. Use for low-level util methods by TimeGrid.
 Does not own rendering. Use for low-level util methods by TimeGrid.
 */
 */
-export default class TimeGridEventRenderer extends FgEventRenderer {
+export default class TimeGridEventRenderer extends FgEventRenderer<TimeGridEventRendererProps> {
 
 
-  timeGrid: TimeGrid
-  segsByCol: any // within each col, events are ordered
-  fullTimeFormat: DateFormatter
+  attachSegs = renderer(attachSegs, detachSegs)
 
 
+  // computed options
+  private fullTimeFormat: DateFormatter
 
 
-  constructor(timeGrid: TimeGrid) {
-    super()
-
-    this.timeGrid = timeGrid
-  }
+  // for sizing
+  private segsByCol: any
 
 
 
 
-  renderSegs(context: ComponentContext, segs: Seg[], mirrorInfo?) {
-    super.renderSegs(context, segs, mirrorInfo)
+  _updateComputedOptions(options) {
+    super._updateComputedOptions(options)
 
 
-    // TODO: dont do every time. memoize
     this.fullTimeFormat = createFormatter({
     this.fullTimeFormat = createFormatter({
       hour: 'numeric',
       hour: 'numeric',
       minute: '2-digit',
       minute: '2-digit',
-      separator: this.context.options.defaultRangeSeparator
+      separator: options.defaultRangeSeparator
     })
     })
   }
   }
 
 
 
 
-  // Given an array of foreground segments, render a DOM element for each, computes position,
-  // and attaches to the column inner-container elements.
-  attachSegs(segs: Seg[], mirrorInfo) {
-    let segsByCol = this.timeGrid.groupSegsByCol(segs)
+  render(props: TimeGridEventRendererProps, context: ComponentContext) {
+    let segs = this.renderSegs({
+      segs: props.segs,
+      mirrorInfo: props.mirrorInfo,
+      selectedInstanceId: props.selectedInstanceId,
+      hiddenInstances: props.hiddenInstances
+    }, context)
+
+    this.segsByCol = this.attachSegs({
+      segs,
+      containerEls: props.containerEls
+    })
+  }
 
 
-    // order the segs within each column
-    // TODO: have groupSegsByCol do this?
-    for (let col = 0; col < segsByCol.length; col++) {
-      segsByCol[col] = this.sortEventSegs(segsByCol[col])
-    }
 
 
-    this.segsByCol = segsByCol
-    this.timeGrid.attachSegsByCol(segsByCol, this.timeGrid.fgContainerEls)
+  computeSegSizes(allSegs: Seg[], timeGrid: TimeGrid) {
+    let { segsByCol } = this
+    let colCnt = timeGrid.props.cells.length
+
+    timeGrid.computeSegVerticals(allSegs) // horizontals relies on this
+
+    for (let col = 0; col < colCnt; col++) {
+      computeSegHorizontals(segsByCol[col], this.context) // compute horizontal coordinates, z-index's, and reorder the array
+    }
   }
   }
 
 
 
 
-  detachSegs(segs: Seg[]) {
-    segs.forEach(function(seg) {
-      removeElement(seg.el)
-    })
+  assignSegSizes(allSegs: Seg[], timeGrid: TimeGrid) {
+    let { segsByCol } = this
+    let colCnt = timeGrid.props.cells.length
+
+    timeGrid.assignSegVerticals(allSegs) // horizontals relies on this
 
 
-    this.segsByCol = null
+    for (let col = 0; col < colCnt; col++) {
+      this.assignSegCss(segsByCol[col], timeGrid)
+    }
   }
   }
 
 
 
 
-  computeSegSizes(allSegs: Seg[]) {
-    let { timeGrid, segsByCol } = this
-    let colCnt = timeGrid.colCnt
+  // Given foreground event segments that have already had their position coordinates computed,
+  // assigns position-related CSS values to their elements.
+  assignSegCss(segs: Seg[], timeGrid: TimeGrid) {
 
 
-    timeGrid.computeSegVerticals(allSegs) // horizontals relies on this
+    for (let seg of segs) {
+      applyStyle(seg.el, this.generateSegCss(seg, timeGrid))
 
 
-    if (segsByCol) {
-      for (let col = 0; col < colCnt; col++) {
-        this.computeSegHorizontals(segsByCol[col]) // compute horizontal coordinates, z-index's, and reorder the array
+      if (seg.level > 0) {
+        seg.el.classList.add('fc-time-grid-event-inset')
+      }
+
+      // if the event is short that the title will be cut off,
+      // attach a className that condenses the title into the time area.
+      if (seg.eventRange.def.title && seg.bottom - seg.top < 30) {
+        seg.el.classList.add('fc-short') // TODO: "condensed" is a better name
       }
       }
     }
     }
   }
   }
 
 
 
 
-  assignSegSizes(allSegs: Seg[]) {
-    let { timeGrid, segsByCol } = this
-    let colCnt = timeGrid.colCnt
+  // Generates an object with CSS properties/values that should be applied to an event segment element.
+  // Contains important positioning-related properties that should be applied to any event element, customized or not.
+  generateSegCss(seg: Seg, timeGrid: TimeGrid) {
+    let { isRtl, options } = this.context
+    let shouldOverlap = options.slotEventOverlap
+    let backwardCoord = seg.backwardCoord // the left side if LTR. the right side if RTL. floating-point
+    let forwardCoord = seg.forwardCoord // the right side if LTR. the left side if RTL. floating-point
+    let props = timeGrid.generateSegVerticalCss(seg) as any // get top/bottom first
+    let left // amount of space from left edge, a fraction of the total width
+    let right // amount of space from right edge, a fraction of the total width
+
+    if (shouldOverlap) {
+      // double the width, but don't go beyond the maximum forward coordinate (1.0)
+      forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2)
+    }
+
+    if (isRtl) {
+      left = 1 - forwardCoord
+      right = backwardCoord
+    } else {
+      left = backwardCoord
+      right = 1 - forwardCoord
+    }
 
 
-    timeGrid.assignSegVerticals(allSegs) // horizontals relies on this
+    props.zIndex = seg.level + 1 // convert from 0-base to 1-based
+    props.left = left * 100 + '%'
+    props.right = right * 100 + '%'
 
 
-    if (segsByCol) {
-      for (let col = 0; col < colCnt; col++) {
-        this.assignSegCss(segsByCol[col])
-      }
+    if (shouldOverlap && seg.forwardPressure) {
+      // add padding to the edge so that forward stacked events don't cover the resizer's icon
+      props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2 // 10 is a guesstimate of the icon's width
     }
     }
+
+    return props
   }
   }
 
 
 
 
@@ -183,152 +226,93 @@ export default class TimeGridEventRenderer extends FgEventRenderer {
       '</a>'
       '</a>'
   }
   }
 
 
+}
 
 
-  // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
-  // Assumed the segs are already ordered.
-  // NOTE: Also reorders the given array by date!
-  computeSegHorizontals(segs: Seg[]) {
-    let levels
-    let level0
-    let i
 
 
-    levels = buildSlotSegLevels(segs)
-    computeForwardSlotSegs(levels)
+// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
+// Assumed the segs are already ordered.
+// NOTE: Also reorders the given array by date!
+function computeSegHorizontals(segs: Seg[], context: ComponentContext) {
+  let levels
+  let level0
+  let i
 
 
-    if ((level0 = levels[0])) {
+  levels = buildSlotSegLevels(segs)
+  computeForwardSlotSegs(levels)
 
 
-      for (i = 0; i < level0.length; i++) {
-        computeSlotSegPressures(level0[i])
-      }
+  if ((level0 = levels[0])) {
 
 
-      for (i = 0; i < level0.length; i++) {
-        this.computeSegForwardBack(level0[i], 0, 0)
-      }
+    for (i = 0; i < level0.length; i++) {
+      computeSlotSegPressures(level0[i])
     }
     }
-  }
 
 
-
-  // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
-  // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
-  // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
-  //
-  // The segment might be part of a "series", which means consecutive segments with the same pressure
-  // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
-  // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
-  // coordinate of the first segment in the series.
-  computeSegForwardBack(seg: Seg, seriesBackwardPressure, seriesBackwardCoord) {
-    let forwardSegs = seg.forwardSegs
-    let i
-
-    if (seg.forwardCoord === undefined) { // not already computed
-
-      if (!forwardSegs.length) {
-
-        // if there are no forward segments, this segment should butt up against the edge
-        seg.forwardCoord = 1
-      } else {
-
-        // sort highest pressure first
-        this.sortForwardSegs(forwardSegs)
-
-        // this segment's forwardCoord will be calculated from the backwardCoord of the
-        // highest-pressure forward segment.
-        this.computeSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord)
-        seg.forwardCoord = forwardSegs[0].backwardCoord
-      }
-
-      // calculate the backwardCoord from the forwardCoord. consider the series
-      seg.backwardCoord = seg.forwardCoord -
-        (seg.forwardCoord - seriesBackwardCoord) / // available width for series
-        (seriesBackwardPressure + 1) // # of segments in the series
-
-      // use this segment's coordinates to computed the coordinates of the less-pressurized
-      // forward segments
-      for (i = 0; i < forwardSegs.length; i++) {
-        this.computeSegForwardBack(forwardSegs[i], 0, seg.forwardCoord)
-      }
+    for (i = 0; i < level0.length; i++) {
+      computeSegForwardBack(level0[i], 0, 0, context)
     }
     }
   }
   }
+}
 
 
 
 
-  sortForwardSegs(forwardSegs: Seg[]) {
-    let objs = forwardSegs.map(buildTimeGridSegCompareObj)
-
-    let specs = [
-      // put higher-pressure first
-      { field: 'forwardPressure', order: -1 },
-      // put segments that are closer to initial edge first (and favor ones with no coords yet)
-      { field: 'backwardCoord', order: 1 }
-    ].concat(
-      this.context.eventOrderSpecs
-    )
-
-    objs.sort(function(obj0, obj1) {
-      return compareByFieldSpecs(obj0, obj1, specs)
-    })
-
-    return objs.map(function(c) {
-      return c._seg
-    })
-  }
+// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
+// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
+// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
+//
+// The segment might be part of a "series", which means consecutive segments with the same pressure
+// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
+// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
+// coordinate of the first segment in the series.
+function computeSegForwardBack(seg: Seg, seriesBackwardPressure, seriesBackwardCoord, context: ComponentContext) {
+  let forwardSegs = seg.forwardSegs
+  let i
 
 
+  if (seg.forwardCoord === undefined) { // not already computed
 
 
-  // Given foreground event segments that have already had their position coordinates computed,
-  // assigns position-related CSS values to their elements.
-  assignSegCss(segs: Seg[]) {
+    if (!forwardSegs.length) {
 
 
-    for (let seg of segs) {
-      applyStyle(seg.el, this.generateSegCss(seg))
+      // if there are no forward segments, this segment should butt up against the edge
+      seg.forwardCoord = 1
+    } else {
 
 
-      if (seg.level > 0) {
-        seg.el.classList.add('fc-time-grid-event-inset')
-      }
+      // sort highest pressure first
+      sortForwardSegs(forwardSegs, context)
 
 
-      // if the event is short that the title will be cut off,
-      // attach a className that condenses the title into the time area.
-      if (seg.eventRange.def.title && seg.bottom - seg.top < 30) {
-        seg.el.classList.add('fc-short') // TODO: "condensed" is a better name
-      }
+      // this segment's forwardCoord will be calculated from the backwardCoord of the
+      // highest-pressure forward segment.
+      computeSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord, context)
+      seg.forwardCoord = forwardSegs[0].backwardCoord
     }
     }
-  }
-
 
 
-  // Generates an object with CSS properties/values that should be applied to an event segment element.
-  // Contains important positioning-related properties that should be applied to any event element, customized or not.
-  generateSegCss(seg: Seg) {
-    let shouldOverlap = this.context.options.slotEventOverlap
-    let backwardCoord = seg.backwardCoord // the left side if LTR. the right side if RTL. floating-point
-    let forwardCoord = seg.forwardCoord // the right side if LTR. the left side if RTL. floating-point
-    let props = this.timeGrid.generateSegVerticalCss(seg) as any // get top/bottom first
-    let isRtl = this.context.isRtl
-    let left // amount of space from left edge, a fraction of the total width
-    let right // amount of space from right edge, a fraction of the total width
+    // calculate the backwardCoord from the forwardCoord. consider the series
+    seg.backwardCoord = seg.forwardCoord -
+      (seg.forwardCoord - seriesBackwardCoord) / // available width for series
+      (seriesBackwardPressure + 1) // # of segments in the series
 
 
-    if (shouldOverlap) {
-      // double the width, but don't go beyond the maximum forward coordinate (1.0)
-      forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2)
+    // use this segment's coordinates to computed the coordinates of the less-pressurized
+    // forward segments
+    for (i = 0; i < forwardSegs.length; i++) {
+      this.computeSegForwardBack(forwardSegs[i], 0, seg.forwardCoord)
     }
     }
+  }
+}
 
 
-    if (isRtl) {
-      left = 1 - forwardCoord
-      right = backwardCoord
-    } else {
-      left = backwardCoord
-      right = 1 - forwardCoord
-    }
 
 
-    props.zIndex = seg.level + 1 // convert from 0-base to 1-based
-    props.left = left * 100 + '%'
-    props.right = right * 100 + '%'
+function sortForwardSegs(forwardSegs: Seg[], eventOrderSpecs) {
+  let objs = forwardSegs.map(buildTimeGridSegCompareObj)
 
 
-    if (shouldOverlap && seg.forwardPressure) {
-      // add padding to the edge so that forward stacked events don't cover the resizer's icon
-      props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2 // 10 is a guesstimate of the icon's width
-    }
+  let specs = [
+    // put higher-pressure first
+    { field: 'forwardPressure', order: -1 },
+    // put segments that are closer to initial edge first (and favor ones with no coords yet)
+    { field: 'backwardCoord', order: 1 }
+  ].concat(eventOrderSpecs)
 
 
-    return props
-  }
+  objs.sort(function(obj0, obj1) {
+    return compareByFieldSpecs(obj0, obj1, specs)
+  })
 
 
+  return objs.map(function(c) {
+    return c._seg
+  })
 }
 }
 
 
 
 

+ 21 - 29
packages/timegrid/src/TimeGridFillRenderer.ts

@@ -1,45 +1,37 @@
 import {
 import {
-  FillRenderer, Seg
+  FillRenderer, Seg, renderer, BaseFillRendererProps
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
-import TimeGrid from './TimeGrid'
+import TimeGrid, { attachSegs, detachSegs } from './TimeGrid'
 
 
+export interface TimeGridFillRendererProps extends BaseFillRendererProps {
+  containerEls: HTMLElement[]
+}
 
 
-export default class TimeGridFillRenderer extends FillRenderer {
-
-  timeGrid: TimeGrid
-
-  constructor(timeGrid: TimeGrid) {
-    super()
-
-    this.timeGrid = timeGrid
-  }
+export default class TimeGridFillRenderer extends FillRenderer<TimeGridFillRendererProps> {
 
 
-  attachSegs(type, segs: Seg[]) {
-    let { timeGrid } = this
-    let containerEls
+  private attachSegs = renderer(attachSegs, detachSegs)
 
 
-    // TODO: more efficient lookup
-    if (type === 'bgEvent') {
-      containerEls = timeGrid.bgContainerEls
-    } else if (type === 'businessHours') {
-      containerEls = timeGrid.businessContainerEls
-    } else if (type === 'highlight') {
-      containerEls = timeGrid.highlightContainerEls
-    }
 
 
-    timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls)
+  render(props: TimeGridFillRendererProps) {
+    let segs = this.renderSegs({
+      type: props.type,
+      segs: props.segs
+    })
 
 
-    return segs.map(function(seg) {
-      return seg.el
+    this.attachSegs({
+      segs,
+      containerEls: props.containerEls
     })
     })
   }
   }
 
 
-  computeSegSizes(segs: Seg[]) {
-    this.timeGrid.computeSegVerticals(segs)
+
+  computeSegSizes(segs: Seg[], timeGrid: TimeGrid) {
+    timeGrid.computeSegVerticals(segs)
   }
   }
 
 
-  assignSegSizes(segs: Seg[]) {
-    this.timeGrid.assignSegVerticals(segs)
+
+  assignSegSizes(segs: Seg[], timeGrid: TimeGrid) {
+    timeGrid.assignSegVerticals(segs)
   }
   }
 
 
 }
 }

+ 10 - 17
packages/timegrid/src/TimeGridMirrorRenderer.ts

@@ -1,32 +1,25 @@
 import { Seg } from '@fullcalendar/core'
 import { Seg } from '@fullcalendar/core'
 import TimeGridEventRenderer from './TimeGridEventRenderer'
 import TimeGridEventRenderer from './TimeGridEventRenderer'
+import TimeGrid from './TimeGrid'
 
 
 
 
 export default class TimeGridMirrorRenderer extends TimeGridEventRenderer {
 export default class TimeGridMirrorRenderer extends TimeGridEventRenderer {
 
 
-  sourceSeg: Seg
 
 
-  attachSegs(segs: Seg[], mirrorInfo) {
-    this.segsByCol = this.timeGrid.groupSegsByCol(segs)
-    this.timeGrid.attachSegsByCol(this.segsByCol, this.timeGrid.mirrorContainerEls)
-
-    this.sourceSeg = mirrorInfo.sourceSeg
-  }
-
-  generateSegCss(seg: Seg) {
-    let props = super.generateSegCss(seg)
-    let { sourceSeg } = this
+  generateSegCss(seg: Seg, timeGrid: TimeGrid) {
+    let cssProps = super.generateSegCss(seg, timeGrid)
+    let { sourceSeg } = this.props.mirrorInfo
 
 
     if (sourceSeg && sourceSeg.col === seg.col) {
     if (sourceSeg && sourceSeg.col === seg.col) {
-      let sourceSegProps = super.generateSegCss(sourceSeg)
+      let sourceSegProps = super.generateSegCss(sourceSeg, timeGrid)
 
 
-      props.left = sourceSegProps.left
-      props.right = sourceSegProps.right
-      props.marginLeft = sourceSegProps.marginLeft
-      props.marginRight = sourceSegProps.marginRight
+      cssProps.left = sourceSegProps.left
+      cssProps.right = sourceSegProps.right
+      cssProps.marginLeft = sourceSegProps.marginLeft
+      cssProps.marginRight = sourceSegProps.marginRight
     }
     }
 
 
-    return props
+    return cssProps
   }
   }
 
 
 }
 }