Ver Fonte

lots of refactoring. sizing. scrolling

Adam Shaw há 6 anos atrás
pai
commit
fb807014aa
33 ficheiros alterados com 990 adições e 1101 exclusões
  1. 1 1
      packages-premium
  2. 3 3
      packages/core/src/Calendar.tsx
  3. 58 0
      packages/core/src/ScrollResponder.ts
  4. 0 17
      packages/core/src/View.ts
  5. 4 38
      packages/core/src/common/PositionCache.ts
  6. 6 1
      packages/core/src/component/ComponentContext.ts
  7. 0 4
      packages/core/src/component/DateComponent.ts
  8. 0 35
      packages/core/src/component/renderers/FgEventRenderer.ts
  9. 0 31
      packages/core/src/component/renderers/FillRenderer.ts
  10. 3 1
      packages/core/src/main.ts
  11. 45 64
      packages/core/src/scrollgrid/SimpleScrollGrid.tsx
  12. 34 32
      packages/core/src/scrollgrid/util.tsx
  13. 5 0
      packages/core/src/util/array.ts
  14. 9 5
      packages/core/src/util/memoize.ts
  15. 7 39
      packages/daygrid/src/DayBgRow.tsx
  16. 2 9
      packages/daygrid/src/DayTable.tsx
  17. 57 75
      packages/daygrid/src/Table.tsx
  18. 10 41
      packages/daygrid/src/TableSkeleton.tsx
  19. 0 2
      packages/interaction/src/interactions/HitDragging.ts
  20. 1 16
      packages/list/src/ListView.tsx
  21. 9 14
      packages/timegrid/src/DayTimeCols.tsx
  22. 5 18
      packages/timegrid/src/DayTimeColsView.tsx
  23. 128 410
      packages/timegrid/src/TimeCols.tsx
  24. 77 0
      packages/timegrid/src/TimeColsBg.tsx
  25. 275 0
      packages/timegrid/src/TimeColsContent.tsx
  26. 0 98
      packages/timegrid/src/TimeColsContentSkeleton.tsx
  27. 29 13
      packages/timegrid/src/TimeColsEvents.ts
  28. 10 13
      packages/timegrid/src/TimeColsFills.ts
  29. 4 4
      packages/timegrid/src/TimeColsMirrorEvents.ts
  30. 94 35
      packages/timegrid/src/TimeColsSlats.tsx
  31. 105 0
      packages/timegrid/src/TimeColsSlatsCoords.ts
  32. 8 82
      packages/timegrid/src/TimeColsView.tsx
  33. 1 0
      packages/timegrid/src/main.ts

+ 1 - 1
packages-premium

@@ -1 +1 @@
-Subproject commit 2675c463b35f6629751d1cd1be898db0a615a998
+Subproject commit 69d9563a66a68786e7ee307de77336853fd1537f

+ 3 - 3
packages/core/src/Calendar.tsx

@@ -1218,10 +1218,10 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
   scrollToTime(timeInput: DurationInput) {
-    let duration = createDuration(timeInput)
+    let time = createDuration(timeInput)
 
-    if (duration) {
-      this.component.view.scrollToTime(duration)
+    if (time) {
+      this.trigger('scrollRequest', { time })
     }
   }
 

+ 58 - 0
packages/core/src/ScrollResponder.ts

@@ -0,0 +1,58 @@
+import { Duration, createDuration } from './datelib/duration'
+import Calendar from './Calendar'
+import { __assign } from 'tslib'
+
+
+export interface ScrollRequest {
+  time?: Duration
+  [otherProp: string]: any
+}
+
+export type ScrollRequestHandler = (request: ScrollRequest) => boolean
+
+
+export default class ScrollResponder {
+
+  queuedRequest: ScrollRequest
+
+
+  constructor(public calendar: Calendar, public execFunc: ScrollRequestHandler) {
+    calendar.on('scrollRequest', this.handleScrollRequest)
+    this.fireInitialScroll()
+  }
+
+
+  detach() {
+    this.calendar.off('scrollRequest', this.handleScrollRequest)
+  }
+
+
+  update(isDatesNew: boolean) {
+    if (isDatesNew) {
+      this.fireInitialScroll() // will drain
+    } else {
+      this.drain()
+    }
+  }
+
+
+  private fireInitialScroll() {
+    this.handleScrollRequest({
+      time: createDuration(this.calendar.viewOpt('scrollTime'))
+    })
+  }
+
+
+  private handleScrollRequest = (request: ScrollRequest) => {
+    this.queuedRequest = __assign({}, this.queuedRequest || {}, request)
+    this.drain()
+  }
+
+
+  private drain() {
+    if (this.queuedRequest && this.execFunc(this.queuedRequest)) {
+      this.queuedRequest = null
+    }
+  }
+
+}

+ 0 - 17
packages/core/src/View.ts

@@ -1,5 +1,4 @@
 import DateProfileGenerator, { DateProfile } from './DateProfileGenerator'
-import { createDuration, Duration } from './datelib/duration'
 import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 import { ViewSpec } from './structs/view-spec'
 import DateComponent from './component/DateComponent'
@@ -56,22 +55,6 @@ export default abstract class View<State={}> extends DateComponent<ViewProps, St
     ).fg
   }
 
-
-  // Scroller
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  scrollToInitialTime() {
-    let duration = createDuration(this.context.options.scrollTime)
-
-    this.scrollToTime(duration)
-  }
-
-
-  scrollToTime(duration: Duration) {
-    // subclasses can implement
-  }
-
 }
 
 EmitterMixin.mixInto(View)

+ 4 - 38
packages/core/src/common/PositionCache.ts

@@ -6,11 +6,8 @@ Provides methods for querying the cache by position.
 */
 export default class PositionCache {
 
-  originClientRect: ClientRect
   els: HTMLElement[] // assumed to be siblings
-  originEl: HTMLElement // options can override the natural originEl
-  isHorizontal: boolean // whether to query for left/right/width
-  isVertical: boolean // whether to query for top/bottom/height
+  originClientRect: ClientRect
 
   // arrays of coordinates (from topleft of originEl)
   // caller can access these directly
@@ -21,51 +18,20 @@ export default class PositionCache {
 
 
   constructor(originEl: HTMLElement, els: HTMLElement[], isHorizontal: boolean, isVertical: boolean) {
-    this.originEl = originEl
     this.els = els
-    this.isHorizontal = isHorizontal
-    this.isVertical = isVertical
-  }
-
 
-  // Queries the els for coordinates and stores them.
-  // Call this method before using and of the get* methods below.
-  build() {
-    let originEl = this.originEl
-    let originClientRect = this.originClientRect =
-      originEl.getBoundingClientRect() // relative to viewport top-left
+    let originClientRect = this.originClientRect = originEl.getBoundingClientRect() // relative to viewport top-left
 
-    if (this.isHorizontal) {
+    if (isHorizontal) {
       this.buildElHorizontals(originClientRect.left)
     }
 
-    if (this.isVertical) {
+    if (isVertical) {
       this.buildElVerticals(originClientRect.top)
     }
   }
 
 
-  // HACK for when DOM isn't ready yet but we coords for intermediate rendering
-  buildZeros() {
-    let len = this.els.length
-    let zeros = []
-
-    for (let i = 0; i < len; i++) {
-      zeros.push(0)
-    }
-
-    if (this.isHorizontal) {
-      this.lefts = zeros
-      this.rights = zeros
-    }
-
-    if (this.isVertical) {
-      this.tops = zeros
-      this.bottoms = zeros
-    }
-  }
-
-
   // Populates the left/right internal coordinate arrays
   buildElHorizontals(originClientLeft: number) {
     let lefts = []

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

@@ -8,6 +8,7 @@ import { createDuration, Duration } from '../datelib/duration'
 import { PluginHooks } from '../plugin-system'
 import { createContext } from '../vdom'
 import { parseToolbars, ToolbarModel } from '../toolbar-parse'
+import ScrollResponder, { ScrollRequestHandler } from '../ScrollResponder'
 
 
 export const ComponentContextType = createContext<ComponentContext>({} as any) // for Components
@@ -28,6 +29,7 @@ export default interface ComponentContext {
   viewsWithButtons: string[]
   addResizeHandler: (handler: ResizeHandler) => void
   removeResizeHandler: (handler: ResizeHandler) => void
+  createScrollResponder: (execFunc: ScrollRequestHandler) => ScrollResponder
 }
 
 
@@ -48,7 +50,10 @@ export function buildContext(
     options,
     ...computeContextProps(options, theme, calendar),
     addResizeHandler: calendar.addResizeHandler,
-    removeResizeHandler: calendar.removeResizeHandler
+    removeResizeHandler: calendar.removeResizeHandler,
+    createScrollResponder(execFunc: ScrollRequestHandler) {
+      return new ScrollResponder(calendar, execFunc)
+    }
   }
 }
 

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

@@ -55,10 +55,6 @@ export default abstract class DateComponent<Props={}, State={}> extends BaseComp
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  buildPositionCaches() {
-  }
-
-
   queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit | null {
     return null // this should be abstract
   }

+ 0 - 35
packages/core/src/component/renderers/FgEventRenderer.ts

@@ -29,10 +29,6 @@ export default abstract class FgEventRenderer<
   private renderSelectedInstance = subrenderer(renderSelectedInstance, unrenderSelectedInstance)
   private renderHiddenInstances = subrenderer(renderHiddenInstances, unrenderHiddenInstances)
 
-  // internal state
-  private segs: Seg[] = [] // for sizing funcs
-  private isSizeDirty: boolean = false // NOTE: should also flick this when attaching segs to new containers
-
   // computed options
   protected eventTimeFormat: DateFormatter
   protected displayEventTime: boolean
@@ -59,9 +55,6 @@ export default abstract class FgEventRenderer<
       hiddenInstances: props.hiddenInstances
     })
 
-    this.segs = segs
-    this.isSizeDirty = true
-
     return segs
   }
 
@@ -258,34 +251,6 @@ export default abstract class FgEventRenderer<
     }
   }
 
-
-  // Sizing
-  // ----------------------------------------------------------------------------------------------------
-
-
-  computeSizes(force: boolean, userComponent: any) {
-    if (force || this.isSizeDirty) {
-      this.computeSegSizes(this.segs, userComponent)
-    }
-  }
-
-
-  assignSizes(force: boolean, userComponent: any) {
-    if (force || this.isSizeDirty) {
-      this.assignSegSizes(this.segs, userComponent)
-    }
-
-    this.isSizeDirty = false
-  }
-
-
-  protected computeSegSizes(segs: Seg[], userComponent: any) {
-  }
-
-
-  protected assignSegSizes(segs: Seg[], userComponent: any) {
-  }
-
 }
 
 

+ 0 - 31
packages/core/src/component/renderers/FillRenderer.ts

@@ -17,10 +17,6 @@ export default abstract class FillRenderer<FillRendererProps extends BaseFillRen
 
   fillSegTag: string = 'div'
 
-  // for sizing
-  private segs: Seg[]
-  private isSizeDirty: boolean = false // NOTE: should also flick this when attaching segs to new containers
-
 
   _renderSegs({ segs, type }: BaseFillRendererProps, context: ComponentContext) {
 
@@ -31,9 +27,6 @@ export default abstract class FillRenderer<FillRendererProps extends BaseFillRen
       triggerPositionedSegs(context, segs, false) // isMirror=false
     }
 
-    this.segs = segs
-    this.isSizeDirty = true
-
     return segs
   }
 
@@ -115,28 +108,4 @@ export default abstract class FillRenderer<FillRendererProps extends BaseFillRen
       '></' + this.fillSegTag + '>'
   }
 
-
-  computeSizes(force: boolean, userComponent: any) {
-    if (force || this.isSizeDirty) {
-      this.computeSegSizes(this.segs, userComponent)
-    }
-  }
-
-
-  assignSizes(force: boolean, userComponent: any) {
-    if (force || this.isSizeDirty) {
-      this.assignSegSizes(this.segs, userComponent)
-    }
-
-    this.isSizeDirty = false
-  }
-
-
-  computeSegSizes(seg: Seg[], userComponent: any) {
-  }
-
-
-  assignSegSizes(seg: Seg[], userComponent: any) {
-  }
-
 }

+ 3 - 1
packages/core/src/main.ts

@@ -180,7 +180,8 @@ export {
   getScrollGridClassNames, getSectionClassNames, getChunkVGrow, getNeedsYScrolling, renderChunkContent, computeForceScrollbars, computeShrinkWidth,
   getChunkClassNames, ChunkContentCallbackArgs,
   computeScrollerClientWidths, computeScrollerClientHeights,
-  sanitizeShrinkWidth
+  sanitizeShrinkWidth,
+  ChunkConfigRowContent, ChunkConfigContent
 } from './scrollgrid/util'
 export { default as Scroller, ScrollerProps, OverflowValue } from './scrollgrid/Scroller'
 export { getScrollbarWidths } from './util/scrollbar-width'
@@ -188,4 +189,5 @@ export { default as RefMap } from './util/RefMap'
 export { getIsRtlScrollbarOnLeft } from './util/scrollbar-side'
 
 export { default as NowTimer, NowTimerCallback } from './NowTimer'
+export { default as ScrollResponder, ScrollRequest } from './ScrollResponder'
 export { globalPlugins } from './global-plugins'

+ 45 - 64
packages/core/src/scrollgrid/SimpleScrollGrid.tsx

@@ -7,6 +7,8 @@ import { ColProps, SectionConfig, renderMicroColGroup, computeShrinkWidth, getSc
   renderChunkContent, getChunkVGrow, computeForceScrollbars, ChunkConfig, hasShrinkWidth, CssDimValue, getChunkClassNames, computeScrollerClientWidths, computeScrollerClientHeights,
   } from './util'
 import { memoize } from '../util/memoize'
+import { isPropsEqual } from '../util/object'
+import { guid } from '../util/misc'
 
 
 export interface SimpleScrollGridProps {
@@ -15,7 +17,6 @@ export interface SimpleScrollGridProps {
   vGrow?: boolean
   forPrint?: boolean
   height?: CssDimValue // TODO: give to real ScrollGrid
-  onSized?: () => void
 }
 
 export interface SimpleScrollGridSection extends SectionConfig {
@@ -23,19 +24,11 @@ export interface SimpleScrollGridSection extends SectionConfig {
 }
 
 interface SimpleScrollGridState {
-  isSizingReady: boolean
-  shrinkWidth: null | number
-  forceYScrollbars: null | boolean
-  scrollerClientWidths: null | { [index: string]: number }
-  scrollerClientHeights: null | { [index: string]: number }
-}
-
-const INITIAL_SIZING_STATE: SimpleScrollGridState = {
-  isSizingReady: false,
-  shrinkWidth: null,
-  forceYScrollbars: null,
-  scrollerClientWidths: null,
-  scrollerClientHeights: null
+  shrinkWidth: number | null
+  forceYScrollbars: boolean
+  scrollerClientWidths: { [index: string]: number }
+  scrollerClientHeights: { [index: string]: number }
+  sizingId: string
 }
 
 
@@ -45,15 +38,12 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
   scrollerRefs = new RefMap<Scroller>()
   scrollerElRefs = new RefMap<HTMLElement, [ChunkConfig]>(this._handleScrollerEl.bind(this))
 
-  state = INITIAL_SIZING_STATE
-
-
-  static getDerivedStateFromProps(props, state: SimpleScrollGridState) {
-    if (state.isSizingReady) { // from a prop change
-      return INITIAL_SIZING_STATE
-    } else if (state.scrollerClientWidths) { // the last sizing-state was just set
-      return { isSizingReady: true }
-    }
+  state = {
+    shrinkWidth: null,
+    forceYScrollbars: false,
+    scrollerClientWidths: {},
+    scrollerClientHeights: {},
+    sizingId: ''
   }
 
 
@@ -98,7 +88,6 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
     }
 
     let { state } = this
-    let { isSizingReady } = state
 
     let needsYScrolling = getNeedsYScrolling(this.props, sectionConfig, chunkConfig) // TODO: do lazily
     let overflowY: OverflowValue = state.forceYScrollbars ? 'scroll' : (needsYScrolling ? 'auto' : 'hidden')
@@ -107,9 +96,9 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
     let content = renderChunkContent(sectionConfig, chunkConfig, {
       tableColGroupNode: microColGroupNode,
       tableMinWidth: '',
-      tableWidth: isSizingReady ? state.scrollerClientWidths[sectionI] : '',
-      tableHeight: isSizingReady ? state.scrollerClientHeights[sectionI] : '', // TODO: subtract 1 for IE?????
-      isSizingReady
+      clientWidth: state.scrollerClientWidths[sectionI] || '',
+      clientHeight: state.scrollerClientHeights[sectionI] || '',
+      rowSyncHeights: []
     })
 
     // TODO: cleaner solution
@@ -157,59 +146,45 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
 
 
   componentDidMount() {
-    if (!this.props.forPrint) {
-      this.adjustSizing()
-    }
-
-    this.context.addResizeHandler(this.handleResize)
+    this.handleSizing(true)
+    this.context.addResizeHandler(this.handleSizing)
   }
 
 
-  componentDidUpdate(prevProps: SimpleScrollGridProps, prevState: SimpleScrollGridState) {
-    if (!this.props.forPrint) { // repeat code
-      this.adjustSizing()
-    }
+  componentDidUpdate(prevProps: SimpleScrollGridProps) {
+    // TODO: need better solution when state contains non-sizing things
+    this.handleSizing(!isPropsEqual(this.props, prevProps))
   }
 
 
   componentWillUnmount() {
-    this.context.removeResizeHandler(this.handleResize)
+    this.context.removeResizeHandler(this.handleSizing)
   }
 
 
-  adjustSizing() {
-    let { state } = this
-
-    if (state.shrinkWidth == null) {
+  handleSizing = (isExternalChange: boolean) => {
+    if (isExternalChange && !this.props.forPrint) {
+      let sizingId = guid()
       this.setState({
+        sizingId,
         shrinkWidth:
           hasShrinkWidth(this.props.cols) ? // TODO: do this optimization for ScrollGrid
             computeShrinkWidth(this.scrollerElRefs.getAll())
             : 0
+      }, () => {
+        if (sizingId === this.state.sizingId) {
+          this.setState({
+            forceYScrollbars: computeForceScrollbars(this.scrollerRefs.getAll(), 'Y')
+          }, () => {
+            if (sizingId === this.state.sizingId) {
+              this.setState({
+                scrollerClientWidths: computeScrollerClientWidths(this.scrollerElRefs),
+                scrollerClientHeights: computeScrollerClientHeights(this.scrollerElRefs)
+              })
+            }
+          })
+        }
       })
-
-    } else if (state.forceYScrollbars == null) {
-      this.setState({
-        forceYScrollbars: computeForceScrollbars(this.scrollerRefs.getAll(), 'Y')
-      })
-
-    } else if (!state.scrollerClientWidths) {
-      this.setState({
-        scrollerClientWidths: computeScrollerClientWidths(this.scrollerElRefs),
-        scrollerClientHeights: computeScrollerClientHeights(this.scrollerElRefs)
-      })
-
-    } else {
-      if (this.props.onSized) {
-        this.props.onSized()
-      }
-    }
-  }
-
-
-  handleResize = () => {
-    if (!this.props.forPrint) {
-      this.forceUpdate() // getDerivedStateFromProps will clear the sizing state
     }
   }
 
@@ -217,3 +192,9 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
   // TODO: can do a really simple print-view. dont need to join rows
 
 }
+
+SimpleScrollGrid.addStateEquality({
+  scrollerClientWidths: isPropsEqual,
+  scrollerClientHeights: isPropsEqual,
+  sizingId: true // never update base on this
+})

+ 34 - 32
packages/core/src/scrollgrid/util.tsx

@@ -24,22 +24,25 @@ export interface SectionConfig {
   vGrowRows?: boolean // TODO: how to get a bottom rule?
 }
 
+export type ChunkConfigContent = (contentProps: ChunkContentCallbackArgs) => VNode
+export type ChunkConfigRowContent = VNode | ChunkConfigContent
+
 export interface ChunkConfig {
   outerContent?: VNode
-  content?: (contentProps: ChunkContentCallbackArgs) => VNode
-  rowContent?: VNode
+  content?: ChunkConfigContent
+  rowContent?: ChunkConfigRowContent
   vGrowRows?: boolean
   scrollerElRef?: Ref<HTMLDivElement>
   elRef?: Ref<HTMLTableCellElement>
   className?: string // on the <td>
 }
 
-export interface ChunkContentCallbackArgs {
+export interface ChunkContentCallbackArgs { // TODO: util for wrapping tables!?
   tableColGroupNode: VNode
   tableMinWidth: CssDimValue
-  tableWidth: CssDimValue
-  tableHeight: CssDimValue
-  isSizingReady: boolean
+  clientWidth: CssDimValue
+  clientHeight: CssDimValue
+  rowSyncHeights: number[]
 }
 
 
@@ -89,52 +92,51 @@ export function getNeedsYScrolling(props: { vGrow?: boolean }, sectionConfig: Se
 }
 
 
-export function renderChunkContent(sectionConfig: SectionConfig, chunkConfig: ChunkConfig, arg: {
-  tableColGroupNode: VNode,
-  tableMinWidth: CssDimValue,
-  tableWidth: CssDimValue,
-  tableHeight: CssDimValue,
-  isSizingReady: boolean
-}) {
-  let tableHeight = (sectionConfig.vGrowRows || chunkConfig.vGrowRows) ? arg.tableHeight : ''
+export function renderChunkContent(sectionConfig: SectionConfig, chunkConfig: ChunkConfig, arg: ChunkContentCallbackArgs) {
+  let vGrowRows = sectionConfig.vGrowRows || chunkConfig.vGrowRows
 
   let content: VNode = typeof chunkConfig.content === 'function' ?
-    chunkConfig.content({
-      tableColGroupNode: arg.tableColGroupNode,
-      tableMinWidth: arg.tableMinWidth,
-      tableWidth: arg.tableWidth,
-      tableHeight,
-      isSizingReady: arg.isSizingReady,
-    }) :
+    chunkConfig.content(arg) :
     h('table', {
       style: {
         minWidth: arg.tableMinWidth, // because colMinWidths arent enough
-        width: arg.tableWidth,
-        height: tableHeight // css `height` on a <table> serves as a min-height
+        width: arg.clientWidth,
+        height: vGrowRows ? arg.clientHeight : '' // css `height` on a <table> serves as a min-height
       }
     }, [
       arg.tableColGroupNode,
-      h('tbody', {}, chunkConfig.rowContent)
+      h('tbody', {}, typeof chunkConfig.rowContent === 'function' ? chunkConfig.rowContent(arg) : chunkConfig.rowContent)
     ])
 
   return content
 }
 
 
-export function renderMicroColGroup(cols: ColProps[], shrinkWidth?: number) { // TODO: make this SuperColumn-only!???
-  return (
-    <colgroup>
-      {cols.map((colProps) => (
+// TODO: make this SuperColumn-only?
+export function renderMicroColGroup(cols: ColProps[], shrinkWidth?: number) {
+  let colNodes: VNode[] = []
+
+  /*
+  for ColProps with spans, it would have been great to make a single <col span="">
+  HOWEVER, Chrome was getting messing up distributing the width to <td>/<th> elements with colspans.
+  SOLUTION: making individual <col> elements makes Chrome behave.
+  */
+  for (let colProps of cols) {
+    let span = colProps.span || 1
+
+    for (let i = 0; i < span; i++) {
+      colNodes.push(
         <col
-          span={colProps.span || 1}
           style={{
             width: colProps.width === 'shrink' ? sanitizeShrinkWidth(shrinkWidth) : (colProps.width || ''),
             minWidth: colProps.minWidth || ''
           }}
         />
-      ))}
-    </colgroup>
-  )
+      )
+    }
+  }
+
+  return (<colgroup>{colNodes}</colgroup>)
 }
 
 

+ 5 - 0
packages/core/src/util/array.ts

@@ -38,6 +38,11 @@ export function removeExact(array, exactVal) {
 }
 
 export function isArraysEqual(a0, a1) {
+
+  if (a0 === a1) {
+    return true
+  }
+
   let len = a0.length
   let i
 

+ 9 - 5
packages/core/src/util/memoize.ts

@@ -23,11 +23,15 @@ function isArgsEqual(args0, args1, equality?) {
   }
 
   for (let i = 0; i < len; i++) {
-    if (
-      (equality && equality[i]) ?
-        !equality[i](args0[i], args1[i]) :
-        args0[i] !== args1[i]
-    ) {
+    let eq = equality && equality[i]
+
+    if (eq === true) {
+      ;
+    } else if (eq) {
+      if (!eq(args0[i], args1[i])) {
+        return false
+      }
+    } else if (args0[i] !== args1[i]) {
       return false
     }
   }

+ 7 - 39
packages/daygrid/src/DayBgRow.tsx

@@ -9,22 +9,20 @@ import {
 import DayBgCell from './DayBgCell'
 
 
-export interface DayBgCellModel {
-  date: DateMarker
-  htmlAttrs?: object
-}
-
 export interface DayBgRowProps {
   cells: DayBgCellModel[]
   dateProfile: DateProfile
+  cellElRefs: RefMap<HTMLTableCellElement>
   renderIntro?: () => VNode[]
-  onReceiveCellEls?: (cellEls: HTMLElement[] | null) => void
 }
 
+export interface DayBgCellModel {
+  date: DateMarker
+  htmlAttrs?: object
+}
 
-export default class DayBgRow extends BaseComponent<DayBgRowProps> {
 
-  cellElRefs = new RefMap<HTMLTableCellElement>()
+export default class DayBgRow extends BaseComponent<DayBgRowProps> {
 
 
   render(props: DayBgRowProps, state: {}, context: ComponentContext) {
@@ -43,7 +41,7 @@ export default class DayBgRow extends BaseComponent<DayBgRowProps> {
           date={cell.date}
           dateProfile={props.dateProfile}
           otherAttrs={cell.htmlAttrs}
-          elRef={this.cellElRefs.createRef(i)}
+          elRef={props.cellElRefs.createRef(i)}
         />
       )
     }
@@ -57,34 +55,4 @@ export default class DayBgRow extends BaseComponent<DayBgRowProps> {
     return (<tr>{parts}</tr>)
   }
 
-
-  componentDidMount() {
-    this.sendDom()
-  }
-
-
-  componentDidUpdate() {
-    this.sendDom()
-  }
-
-
-  componentWillUnmount() {
-    let { onReceiveCellEls } = this.props
-    if (onReceiveCellEls) {
-      onReceiveCellEls(null)
-    }
-  }
-
-
-  sendDom() {
-    let { onReceiveCellEls } = this.props
-    if (onReceiveCellEls) {
-      onReceiveCellEls(this.cellElRefs.collect())
-    }
-  }
-
 }
-
-DayBgRow.addPropsEquality({
-  onReceiveCellEls: true
-})

+ 2 - 9
packages/daygrid/src/DayTable.tsx

@@ -43,8 +43,6 @@ export default class DayTable extends DateComponent<DayTableProps, ComponentCont
   private slicer = new DayTableSlicer()
   private tableRef = createRef<Table>()
 
-  get table() { return this.tableRef.current }
-
 
   render(props: DayTableProps, state: {}, context: ComponentContext) {
     let { dateProfile, dayTableModel } = props
@@ -52,7 +50,7 @@ export default class DayTable extends DateComponent<DayTableProps, ComponentCont
     return (
       <Table
         ref={this.tableRef}
-        elRef={this.handleRootEl}
+        rootElRef={this.handleRootEl}
         { ...this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, context.calendar, dayTableModel) }
         dateProfile={dateProfile}
         cells={dayTableModel.cells}
@@ -81,13 +79,8 @@ export default class DayTable extends DateComponent<DayTableProps, ComponentCont
   }
 
 
-  buildPositionCaches() {
-    this.table.buildPositionCaches()
-  }
-
-
   queryHit(positionLeft: number, positionTop: number): Hit {
-    let rawHit = this.table.positionToHit(positionLeft, positionTop)
+    let rawHit = this.tableRef.current.positionToHit(positionLeft, positionTop)
 
     if (rawHit) {
       return {

+ 57 - 75
packages/daygrid/src/Table.tsx

@@ -9,24 +9,26 @@ import {
   Seg,
   intersectRanges,
   EventRenderRange,
-  BaseComponent,
   ComponentContext,
   subrenderer,
   createFormatter,
-  VNode
+  VNode,
+  RefMap,
+  DateComponent,
+  setRef
 } from '@fullcalendar/core'
 import TableEvents from './TableEvents'
 import TableMirrorEvents from './TableMirrorEvents'
 import TableFills from './TableFills'
 import Popover from './Popover'
 import DayTile from './DayTile'
-import TableSkeleton, { TableSkeletonProps } from './TableSkeleton'
+import TableSkeleton, { TableBaseProps } from './TableSkeleton'
 
 
 /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
 ----------------------------------------------------------------------------------------------------------------------*/
 
-export interface TableProps extends TableSkeletonProps {
+export interface TableProps extends TableBaseProps {
   businessHourSegs: TableSeg[]
   bgEventSegs: TableSeg[]
   fgEventSegs: TableSeg[]
@@ -58,7 +60,7 @@ interface SegPopoverState {
 }
 
 
-export default class Table extends BaseComponent<TableProps, TableState> {
+export default class Table extends DateComponent<TableProps, TableState> {
 
   private renderFgEvents = subrenderer(TableEvents)
   private renderMirrorEvents = subrenderer(TableMirrorEvents)
@@ -66,21 +68,32 @@ export default class Table extends BaseComponent<TableProps, TableState> {
   private renderBusinessHours = subrenderer(TableFills)
   private renderHighlight = subrenderer(TableFills)
   private popoverRef = createRef<Popover>()
+  private rootEl: HTMLElement
+  private rowElRefs = new RefMap<HTMLTableRowElement>()
+  private cellElRefs: RefMap<HTMLTableCellElement>[] = []
 
-  rowEls: HTMLElement[] // set of fake row elements
-  cellEls: HTMLElement[][] // set of whole-day elements comprising the row's background
   rowStructs: any
-
-  isCellSizesDirty: boolean = false
   rowPositions: PositionCache
   colPositions: PositionCache
-  bottomCoordPadding: number = 0 // hack for extending the hit area for the last row of the coordinate grid
 
 
   render(props: TableProps) {
+
+    // CRAZINESS. TODO: refactor RefMap system
+    let rowCnt = props.cells.length
+    if (rowCnt < this.cellElRefs.length) {
+      this.cellElRefs = this.cellElRefs.slice(0, props.cells.length)
+    }
+    for (let i = 0; i < rowCnt; i++) {
+      if (!this.cellElRefs[i]) {
+        this.cellElRefs[i] = new RefMap()
+      }
+    }
+
     return (
       <Fragment>
         <TableSkeleton
+          rootElRef={this.handleRootEl}
           dateProfile={props.dateProfile}
           cells={props.cells}
           isRigid={props.isRigid}
@@ -90,8 +103,8 @@ export default class Table extends BaseComponent<TableProps, TableState> {
           colWeekNumbersVisible={props.colWeekNumbersVisible}
           cellWeekNumbersVisible={props.cellWeekNumbersVisible}
           colGroupNode={props.colGroupNode}
-          elRef={props.elRef}
-          onReceiveEls={this.handleSkeletonEls}
+          rowElRefs={this.rowElRefs}
+          cellElRefs={this.cellElRefs}
           vGrow={props.vGrow}
         />
         {this.renderPopover()}
@@ -128,46 +141,29 @@ export default class Table extends BaseComponent<TableProps, TableState> {
   }
 
 
-  handleSkeletonEls = (rowEls: HTMLElement[] | null, cellEls: HTMLElement[][] | null) => {
-    let rootEl: HTMLElement = null
+  handleRootEl = (rootEl: HTMLElement | null) => {
+    this.rootEl = rootEl
 
-    if (!rowEls) {
+    if (!rootEl) {
       this.subrenderDestroy()
+    }
 
-    } else {
-      rootEl = rowEls[0].parentNode as HTMLElement
-
-      this.rowPositions = new PositionCache(
-        rootEl,
-        rowEls,
-        false,
-        true // vertical
-      )
-
-      this.colPositions = new PositionCache(
-        rootEl,
-        cellEls[0], // only the first row
-        true, // horizontal
-        false
-      )
-
-      this.rowEls = rowEls
-      this.cellEls = cellEls
-      this.isCellSizesDirty = true
+    if (this.props.rootElRef) {
+      setRef(this.props.rootElRef, rootEl)
     }
   }
 
 
   componentDidMount() {
     this.subrender()
-    this.handleSizing(false)
+    this.handleSizing()
     this.context.addResizeHandler(this.handleSizing)
   }
 
 
   componentDidUpdate() {
     this.subrender()
-    this.handleSizing(false)
+    this.handleSizing()
   }
 
 
@@ -177,7 +173,8 @@ export default class Table extends BaseComponent<TableProps, TableState> {
 
 
   subrender() {
-    let { props, rowEls } = this
+    let { props } = this
+    let rowEls = this.rowElRefs.collect()
     let colCnt = props.cells[0].length
 
     if (props.eventDrag && props.eventDrag.segs.length) { // messy check
@@ -235,8 +232,8 @@ export default class Table extends BaseComponent<TableProps, TableState> {
       colCnt,
       selectedInstanceId: props.eventSelection,
       hiddenInstances: // TODO: more convenient
-        (props.eventDrag && props.eventDrag.segs.length ? props.eventDrag.affectedInstances : null) ||
-        (props.eventResize && props.eventResize.segs.length ? props.eventResize.affectedInstances : null),
+        (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
+        (props.eventResize ? props.eventResize.affectedInstances : null),
       isDragging: false,
       isResizing: false,
       isSelecting: false
@@ -271,44 +268,30 @@ export default class Table extends BaseComponent<TableProps, TableState> {
   ------------------------------------------------------------------------------------------------------------------*/
 
 
-  handleSizing = (forced: boolean) => {
-    let { calendar } = this.context
-    let popover = this.popoverRef.current
+  handleSizing = () => { // TODO: make much more optimal!!!
+    this.updateEventLimitSizing()
 
-    if (
-      forced ||
-      this.isCellSizesDirty ||
-      calendar.isEventsUpdated // hack
-    ) {
-      this.updateEventLimitSizing()
-      this.buildPositionCaches()
-      this.isCellSizesDirty = false
-    }
+    this.rowPositions = new PositionCache(
+      this.rootEl,
+      this.rowElRefs.collect(),
+      false,
+      true // vertical
+    )
+
+    this.colPositions = new PositionCache(
+      this.rootEl,
+      this.cellElRefs[0].collect(), // only the first row
+      true, // horizontal
+      false
+    )
 
+    let popover = this.popoverRef.current
     if (popover) {
       popover.updateSize()
     }
   }
 
 
-  buildPositionCaches() {
-    this.buildColPositions()
-    this.buildRowPositions()
-  }
-
-
-  buildColPositions() {
-    this.colPositions.build()
-  }
-
-
-  buildRowPositions() {
-    let rowCnt = this.props.cells.length
-
-    this.rowPositions.build()
-    this.rowPositions.bottoms[rowCnt - 1] += this.bottomCoordPadding // hack
-  }
-
 
   /* Hit System
   ------------------------------------------------------------------------------------------------------------------*/
@@ -345,15 +328,14 @@ export default class Table extends BaseComponent<TableProps, TableState> {
   // FYI: the first column is the leftmost column, regardless of date
 
 
-  getCellEl(row, col) {
-    return this.cellEls[row][col]
+  private getCellEl(row, col) {
+    return this.cellElRefs[row].currentMap[col]
   }
 
 
-  getCellRange(row, col) {
+  private getCellRange(row, col) {
     let start = this.props.cells[row][col].date
     let end = addDays(start, 1)
-
     return { start, end }
   }
 
@@ -366,7 +348,7 @@ export default class Table extends BaseComponent<TableProps, TableState> {
     let { props, rowStructs } = this
 
     if (props.vGrow) {
-      this._limitRows(props.eventLimit, this.rowEls, rowStructs, this.props.cells, this.context)
+      this._limitRows(props.eventLimit, this.rowElRefs.collect(), rowStructs, this.props.cells, this.context)
 
     } else {
       for (let row = 0; row < rowStructs.length; row++) {

+ 10 - 41
packages/daygrid/src/TableSkeleton.tsx

@@ -10,9 +10,13 @@ import DayBgRow from './DayBgRow'
 import TableSkeletonDayCell from './TableSkeletonDayCell'
 
 
-export interface TableSkeletonProps {
-  elRef?: Ref<HTMLDivElement>
-  onReceiveEls?: (rowEls: HTMLElement[] | null, cellEls: HTMLElement[][] | null) => void
+export interface TableSkeletonProps extends TableBaseProps {
+  rowElRefs?: RefMap<HTMLDivElement>
+  cellElRefs?: RefMap<HTMLTableCellElement>[]
+}
+
+export interface TableBaseProps {
+  rootElRef?: Ref<HTMLDivElement>
   dateProfile: DateProfile
   cells: CellModel[][] // cells-BY-ROW
   isRigid: boolean
@@ -35,9 +39,6 @@ export interface CellModel {
 
 export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
 
-  rowElRefs = new RefMap<HTMLDivElement>()
-  cellElRefs = new RefMap<HTMLTableCellElement[]>() // the bg cells
-
 
   render(props: TableSkeletonProps) {
     let rowCnt = this.props.cells.length
@@ -50,39 +51,13 @@ export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
     }
 
     return (
-      <div class={'fc-day-grid' + (props.vGrow ? ' vgrow' : '')} ref={props.elRef}>
+      <div class={'fc-day-grid' + (props.vGrow ? ' vgrow' : '')} ref={props.rootElRef}>
         {rowNodes}
       </div>
     )
   }
 
 
-  componentDidMount() {
-    this.sendDom()
-  }
-
-
-  componentDidUpdate() {
-    this.sendDom()
-  }
-
-
-  componentWillUnmount() {
-    let { onReceiveEls } = this.props
-    if (onReceiveEls) {
-      onReceiveEls(null, null)
-    }
-  }
-
-
-  sendDom() {
-    let { onReceiveEls } = this.props
-    if (onReceiveEls) {
-      onReceiveEls(this.rowElRefs.collect(), this.cellElRefs.collect())
-    }
-  }
-
-
   // Generates the HTML for a single row, which is a div that wraps a table.
   // `row` is the row number.
   renderDayRow(row) {
@@ -95,7 +70,7 @@ export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
     }
 
     return (
-      <div class={classes.join(' ')} ref={this.rowElRefs.createRef(row)}>
+      <div class={classes.join(' ')} ref={props.rowElRefs.createRef(row)}>
         <div class='fc-bg'>
           <table class={theme.getClass('table')}>
             {props.colGroupNode}
@@ -104,9 +79,7 @@ export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
                 cells={props.cells[row]}
                 dateProfile={props.dateProfile}
                 renderIntro={props.renderBgIntro}
-                onReceiveCellEls={(bgCellEls: HTMLTableCellElement[] | null) => {
-                  this.cellElRefs.handleValue(bgCellEls, row)
-                }}
+                cellElRefs={props.cellElRefs[row]}
               />
             </tbody>
           </table>
@@ -177,7 +150,3 @@ export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
   }
 
 }
-
-TableSkeleton.addPropsEquality({
-  onReceiveEls: true
-})

+ 0 - 2
packages/interaction/src/interactions/HitDragging.ts

@@ -140,8 +140,6 @@ export default class HitDragging {
 
   prepareHits() {
     this.offsetTrackers = mapHash(this.droppableStore, function(interactionSettings) {
-      interactionSettings.component.buildPositionCaches()
-
       return new OffsetTracker(interactionSettings.el)
     })
   }

+ 1 - 16
packages/list/src/ListView.tsx

@@ -30,7 +30,6 @@ export default class ListView extends View {
   private eventStoreToSegs = memoize(this._eventStoreToSegs)
   private renderEvents = subrenderer(ListViewEvents)
   private scrollerElRef = createRef<HTMLDivElement>()
-  private eventRenderer: ListViewEvents
 
 
   render(props: ViewProps, state: {}, context: ComponentContext) {
@@ -68,19 +67,11 @@ export default class ListView extends View {
 
   componentDidMount() {
     this.subrender()
-    this.handleSizing(false)
-    this.context.addResizeHandler(this.handleSizing)
   }
 
 
   componentDidUpdate() {
     this.subrender()
-    this.handleSizing(false)
-  }
-
-
-  componentWillUnmount() {
-    this.context.removeResizeHandler(this.handleSizing)
   }
 
 
@@ -88,7 +79,7 @@ export default class ListView extends View {
     let { props } = this
     let { dayDates, dayRanges } = this.computeDateVars(props.dateProfile)
 
-    this.eventRenderer = this.renderEvents({
+    this.renderEvents({
       segs: this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges),
       dayDates,
       contentEl: this.scrollerElRef.current,
@@ -103,12 +94,6 @@ export default class ListView extends View {
   }
 
 
-  handleSizing = (forced: boolean) => {
-    this.eventRenderer.computeSizes(forced, this)
-    this.eventRenderer.assignSizes(forced, this)
-  }
-
-
   _eventStoreToSegs(eventStore: EventStore, eventUiBases: EventUiHash, dayRanges: DateRange[]): Seg[] {
     return this.eventRangesToSegs(
       sliceEventStore(

+ 9 - 14
packages/timegrid/src/DayTimeCols.tsx

@@ -32,12 +32,13 @@ export interface DayTimeColsProps {
   eventDrag: EventInteractionState | null
   eventResize: EventInteractionState | null
   tableColGroupNode: VNode
-  tableWidth: CssDimValue
-  tableHeight: CssDimValue
+  tableMinWidth: CssDimValue
+  clientWidth: CssDimValue
+  clientHeight: CssDimValue
   renderBgIntro: () => VNode[]
   renderIntro: () => VNode[]
+  onScrollTop?: (scrollTop: number) => void
   forPrint: boolean
-  allowSizing: boolean
 }
 
 interface DayTimeColsState {
@@ -53,8 +54,6 @@ export default class DayTimeCols extends DateComponent<DayTimeColsProps, DayTime
   private slicer = new DayTimeColsSlicer()
   private timeColsRef = createRef<TimeCols>()
 
-  get timeCols() { return this.timeColsRef.current } // used for view's computeDateScroll :(
-
 
   render(props: DayTimeColsProps, state: DayTimeColsState, context: ComponentContext) {
     let { dateEnv } = context
@@ -70,14 +69,15 @@ export default class DayTimeCols extends DateComponent<DayTimeColsProps, DayTime
         dateProfile={dateProfile}
         cells={dayTableModel.cells[0]}
         tableColGroupNode={props.tableColGroupNode}
-        tableWidth={props.tableWidth}
-        tableHeight={props.tableHeight}
+        tableMinWidth={props.tableMinWidth}
+        clientWidth={props.clientWidth}
+        clientHeight={props.clientHeight}
         renderBgIntro={props.renderBgIntro}
         renderIntro={props.renderIntro}
         nowIndicatorDate={state.nowIndicatorDate}
         nowIndicatorSegs={state.nowIndicatorDate && this.slicer.sliceNowDate(state.nowIndicatorDate, this.context.calendar, this.dayRanges)}
+        onScrollTop={props.onScrollTop}
         forPrint={props.forPrint}
-        allowSizing={props.allowSizing}
       />
     )
   }
@@ -125,13 +125,8 @@ export default class DayTimeCols extends DateComponent<DayTimeColsProps, DayTime
   }
 
 
-  buildPositionCaches() {
-    this.timeCols.buildPositionCaches()
-  }
-
-
   queryHit(positionLeft: number, positionTop: number): Hit {
-    let rawHit = this.timeCols.positionToHit(positionLeft, positionTop)
+    let rawHit = this.timeColsRef.current.positionToHit(positionLeft, positionTop)
 
     if (rawHit) {
       return {

+ 5 - 18
packages/timegrid/src/DayTimeColsView.tsx

@@ -1,5 +1,5 @@
 import {
-  h, createRef,
+  h,
   DateProfileGenerator, DateProfile,
   ComponentContext,
   DayHeader,
@@ -17,8 +17,6 @@ import DayTimeCols from './DayTimeCols'
 export default class DayTimeColsView extends TimeColsView {
 
   private buildTimeColsModel = memoize(buildTimeColsModel)
-  private dayTableRef = createRef<DayTable>()
-  private timeColsRef = createRef<DayTimeCols>()
 
 
   render(props: ViewProps, state: {}, context: ComponentContext) {
@@ -37,7 +35,6 @@ export default class DayTimeColsView extends TimeColsView {
         />,
       options.allDaySlot && ((contentArg: ChunkContentCallbackArgs) => (
         <DayTable
-          ref={this.dayTableRef}
           {...splitProps['allDay']}
           dateProfile={dateProfile}
           dayTableModel={dayTableModel}
@@ -55,7 +52,6 @@ export default class DayTimeColsView extends TimeColsView {
       )),
       (contentArg: ChunkContentCallbackArgs) => (
         <DayTimeCols
-          ref={this.timeColsRef}
           {...splitProps['timed']}
           dateProfile={dateProfile}
           dayTableModel={dayTableModel}
@@ -63,24 +59,15 @@ export default class DayTimeColsView extends TimeColsView {
           renderIntro={this.renderTimeColsIntro}
           forPrint={props.forPrint}
           tableColGroupNode={contentArg.tableColGroupNode}
-          tableWidth={contentArg.tableWidth}
-          tableHeight={contentArg.tableHeight}
-          allowSizing={contentArg.isSizingReady}
+          tableMinWidth={contentArg.tableMinWidth}
+          clientWidth={contentArg.clientWidth}
+          clientHeight={contentArg.clientHeight}
+          onScrollTop={this.handleScrollTop}
         />
       )
     )
   }
 
-
-  getAllDayTableObj() {
-    return this.dayTableRef.current
-  }
-
-
-  getTimeColsObj() {
-    return this.timeColsRef.current
-  }
-
 }
 
 

+ 128 - 410
packages/timegrid/src/TimeCols.tsx

@@ -1,15 +1,10 @@
 import {
   h, VNode, Ref,
   removeElement,
-  applyStyle,
-  PositionCache,
-  Duration,
   createDuration,
   addDurations,
   multiplyDuration,
   wholeDivideDurations,
-  asRoughMs,
-  startOfDay,
   DateMarker,
   ComponentContext,
   BaseComponent,
@@ -18,25 +13,20 @@ import {
   DateProfile,
   sortEventSegs,
   memoize,
-  subrenderer,
-  setRef,
-  CssDimValue
+  CssDimValue,
+  PositionCache,
+  Duration,
+  ScrollResponder,
+  ScrollRequest
 } from '@fullcalendar/core'
-import { DayBgRow, DayBgCellModel } from '@fullcalendar/daygrid'
-import TimeColsEvents from './TimeColsEvents'
-import TimeColsMirrorEvents from './TimeColsMirrorEvents'
-import TimeColsFills from './TimeColsFills'
+import { DayBgCellModel } from '@fullcalendar/daygrid'
 import TimeColsSlats from './TimeColsSlats'
-import TimeColsContentSkeleton, { TimeColsContentSkeletonContainers } from './TimeColsContentSkeleton'
+import TimeColsContent from './TimeColsContent'
+import TimeColsBg from './TimeColsBg'
 import { __assign } from 'tslib'
+import TimeColsSlatsCoords from './TimeColsSlatsCoords'
 
 
-export interface TimeColsSeg extends Seg {
-  col: number
-  start: DateMarker
-  end: DateMarker
-}
-
 export interface TimeColsProps {
   dateProfile: DateProfile
   cells: DayBgCellModel[]
@@ -49,449 +39,159 @@ export interface TimeColsProps {
   eventResize: EventSegUiInteractionState | null
   rootElRef?: Ref<HTMLDivElement>
   tableColGroupNode: VNode
-  tableWidth: CssDimValue
-  tableHeight: CssDimValue
+  tableMinWidth: CssDimValue
+  clientWidth: CssDimValue
+  clientHeight: CssDimValue
   renderBgIntro: () => VNode[]
   renderIntro: () => VNode[]
   nowIndicatorDate: DateMarker
   nowIndicatorSegs: TimeColsSeg[]
+  onScrollTop?: (scrollTop: number) => void
   forPrint: boolean
-  allowSizing: boolean
+}
+
+export interface TimeColsSeg extends Seg {
+  col: number
+  start: DateMarker
+  end: DateMarker
 }
 
 export const TIME_COLS_NOW_INDICATOR_UNIT = 'minute'
 
+interface TimeColsState {
+  slatCoords?: TimeColsSlatsCoords
+  colCoords?: PositionCache
+}
+
 
 /* A component that renders one or more columns of vertical time slots
 ----------------------------------------------------------------------------------------------------------------------*/
 
-export default class TimeCols extends BaseComponent<TimeColsProps> {
-
-  private processOptions = memoize(this._processOptions)
-  private renderMirrorEvents = subrenderer(TimeColsMirrorEvents)
-  private renderFgEvents = subrenderer(TimeColsEvents)
-  private renderBgEvents = subrenderer(TimeColsFills)
-  private renderBusinessHours = subrenderer(TimeColsFills)
-  private renderDateSelection = subrenderer(TimeColsFills)
-  private renderNowIndicator = subrenderer(this._renderNowIndicator, this._unrenderNowIndicator)
-
-  // computed options
-  private slotDuration: Duration // duration of a "slot", a distinct time segment on given day, visualized by lines
-  private snapDuration: Duration // granularity of time for dragging and selecting
-  private snapsPerSlot: any
-
-  private rootEl: HTMLElement
-  private colContainerEls: HTMLElement[] // containers for each column
-  private businessContainerEls: HTMLElement[]
-  private highlightContainerEls: HTMLElement[]
-  private bgContainerEls: HTMLElement[]
-  private fgContainerEls: HTMLElement[]
-  private mirrorContainerEls: HTMLElement[]
-  public colEls: HTMLElement[] // cells elements in the day-row background
-  private slatRootEl: HTMLElement // div that wraps all the slat rows
-  private slatEls: HTMLElement[] // elements running horizontally across all columns
-
-  private colPositions: PositionCache
-  private slatPositions: PositionCache
-  private segRenderers: (TimeColsEvents | TimeColsFills | null)[]
-
+export default class TimeCols extends BaseComponent<TimeColsProps, TimeColsState> {
 
-  /* Rendering
-  ------------------------------------------------------------------------------------------------------------------*/
+  private processSlotOptions = memoize(processSlotOptions)
+  private slotDuration: Duration
+  private snapDuration: Duration
+  private snapsPerSlot: number
+  private scrollResponder: ScrollResponder
 
 
-  render(props: TimeColsProps, state: {}, context: ComponentContext) {
-    let { theme } = context
-
-    this.processOptions(context.options)
-
-    return (
-      <div class='fc-time-grid' ref={this.handleRootEl}>
-        <div class='fc-bg'>
-          <table class={theme.getClass('table')}>
-            {props.tableColGroupNode}
-            <DayBgRow
-              dateProfile={props.dateProfile}
-              cells={props.cells}
-              renderIntro={props.renderBgIntro}
-              onReceiveCellEls={this.handleBgCellEls}
-            />
-          </table>
-        </div>
-        <div class='fc-slats'>
-          <table
-            class={theme.getClass('table') + ' vgrow' /* why not use rowsGrow like resource view? */}
-            style={{
-              // TODO: add minWidth
-              width: props.tableWidth,
-              height: props.tableHeight
-            }}
-          >
-            {props.tableColGroupNode /* relies on there only being a single <col> for the axis */}
-            <TimeColsSlats
-              dateProfile={props.dateProfile}
-              slotDuration={this.slotDuration}
-              onReceiveSlatEls={this.handleSlatEls}
-            />
-          </table>
-        </div>
-        <div class='fc-content-skeleton'>
-          <table>
-            {props.tableColGroupNode}
-            <TimeColsContentSkeleton
-              colCnt={props.cells.length}
-              renderIntro={props.renderIntro}
-              onReceiveContainerEls={this.handleContainerEls /* expensive, but TimeColsContentSkeleton rerender is rare */}
-            />
-          </table>
-        </div>
-      </div>
-    )
-  }
-
-
-  // Parses various options into properties of this object
-  // MUST have context already set
-  _processOptions(options) {
-    let { slotDuration, snapDuration } = options
-    let snapsPerSlot
-
-    slotDuration = createDuration(slotDuration)
-    snapDuration = snapDuration ? createDuration(snapDuration) : slotDuration
-    snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration)
-
-    if (snapsPerSlot === null) {
-      snapDuration = slotDuration
-      snapsPerSlot = 1
-      // TODO: say warning?
-    }
+  render(props: TimeColsProps, state: TimeColsState, context: ComponentContext) {
+    let { dateProfile } = props
 
+    let { slotDuration, snapDuration, snapsPerSlot } = this.processSlotOptions(context.options)
     this.slotDuration = slotDuration
     this.snapDuration = snapDuration
     this.snapsPerSlot = snapsPerSlot
-  }
-
-
-  handleRootEl = (rootEl: HTMLElement | null) => {
-    this.rootEl = rootEl
-    setRef(this.props.rootElRef, rootEl)
-  }
 
-
-  handleBgCellEls = (colEls: HTMLElement[] | null) => {
-    if (colEls) {
-      this.colEls = colEls
-      this.colPositions = new PositionCache(
-        colEls[0].parentNode as HTMLElement,
-        colEls,
-        true, // horizontal
-        false
-      )
-    }
-  }
-
-
-  handleSlatEls = (slatEls: HTMLElement[] | null) => {
-    if (slatEls) {
-      let slatRootEl = this.slatRootEl = slatEls[0].parentNode as HTMLElement
-      this.slatEls = slatEls
-      this.slatPositions = new PositionCache(
-        slatRootEl,
-        slatEls,
-        false,
-        true // vertical
-      )
-    }
-  }
-
-
-  handleContainerEls = (containers: TimeColsContentSkeletonContainers | null) => {
-    if (!containers) {
-      this.subrenderDestroy()
-    } else {
-      __assign(this, containers)
-    }
+    return (
+      <div class='fc-time-grid' ref={props.rootElRef}>
+        <TimeColsBg
+          dateProfile={dateProfile}
+          cells={props.cells}
+          clientWidth={props.clientWidth}
+          tableMinWidth={props.tableMinWidth}
+          tableColGroupNode={props.tableColGroupNode}
+          renderIntro={props.renderBgIntro}
+          onCoords={this.handlColCoords}
+        />
+        <TimeColsSlats
+          dateProfile={dateProfile}
+          slotDuration={slotDuration}
+          clientWidth={props.clientWidth}
+          clientHeight={props.clientHeight}
+          tableMinWidth={props.tableMinWidth}
+          tableColGroupNode={props.tableColGroupNode}
+          onCoords={this.handleSlatCoords}
+        />
+        <TimeColsContent
+          colCnt={props.cells.length}
+          renderIntro={props.renderIntro}
+          businessHourSegs={props.businessHourSegs}
+          bgEventSegs={props.bgEventSegs}
+          fgEventSegs={props.fgEventSegs}
+          dateSelectionSegs={props.dateSelectionSegs}
+          eventSelection={props.eventSelection}
+          eventDrag={props.eventDrag}
+          eventResize={props.eventResize}
+          nowIndicatorDate={props.nowIndicatorDate}
+          nowIndicatorSegs={props.nowIndicatorSegs}
+          clientWidth={props.clientWidth}
+          tableMinWidth={props.tableMinWidth}
+          tableColGroupNode={props.tableColGroupNode}
+          coords={state.slatCoords}
+          forPrint={props.forPrint}
+        />
+      </div>
+    )
   }
 
 
   componentDidMount() {
-    this.subrender()
-    this.handleSizing()
-    this.context.addResizeHandler(this.handleSizing)
+    this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest)
   }
 
 
-  componentDidUpdate() {
-    this.subrender()
-    this.handleSizing()
+  componentDidUpdate(prevProps: TimeColsProps) {
+    this.scrollResponder.update(this.props.dateProfile !== prevProps.dateProfile)
   }
 
 
   componentWillUnmount() {
-    this.context.removeResizeHandler(this.handleSizing)
+    this.scrollResponder.detach()
   }
 
 
-  subrender() {
-    let { props } = this
-    let { options } = this.context
-
-    this.segRenderers = [
-      this.renderBusinessHours({
-        type: 'businessHours',
-        containerEls: this.businessContainerEls,
-        segs: props.businessHourSegs,
-      }),
-      this.renderDateSelection({
-        type: 'highlight',
-        containerEls: this.highlightContainerEls,
-        segs: options.selectMirror ? [] : props.dateSelectionSegs // do highlight if NO mirror
-      }),
-      this.renderBgEvents({
-        type: 'bgEvent',
-        containerEls: this.bgContainerEls,
-        segs: props.bgEventSegs
-      }),
-      this.renderFgEvents({
-        containerEls: this.fgContainerEls,
-        segs: props.fgEventSegs,
-        selectedInstanceId: props.eventSelection,
-        hiddenInstances: // TODO: more convenient
-          (props.eventDrag && props.eventDrag.segs.length ? props.eventDrag.affectedInstances : null) ||
-          (props.eventResize && props.eventResize.segs.length ? props.eventResize.affectedInstances : null),
-        isDragging: false,
-        isResizing: false,
-        isSelecting: false,
-        forPrint: props.forPrint
-      }),
-      this.subrenderMirror(props, this.mirrorContainerEls, options)
-    ]
-  }
-
-
-  subrenderMirror(props: TimeColsProps, mirrorContainerEls: HTMLElement[], options): TimeColsEvents | null {
-    if (props.eventDrag && props.eventDrag.segs.length) { // messy check
-      return this.renderMirrorEvents({
-        containerEls: mirrorContainerEls,
-        segs: props.eventDrag.segs,
-        isDragging: true,
-        isResizing: false,
-        isSelecting: false,
-        interactingSeg: props.eventDrag.interactingSeg,
-        forPrint: props.forPrint
-      })
-
-    } else if (props.eventResize && props.eventResize.segs.length) {
-      return this.renderMirrorEvents({
-        containerEls: mirrorContainerEls,
-        segs: props.eventResize.segs,
-        isDragging: true,
-        isResizing: false,
-        isSelecting: false,
-        interactingSeg: props.eventResize.interactingSeg,
-        forPrint: props.forPrint
-      })
-
-    } else if (options.selectMirror) {
-      return this.renderMirrorEvents({
-        containerEls: mirrorContainerEls,
-        segs: props.dateSelectionSegs,
-        isDragging: false,
-        isResizing: false,
-        isSelecting: true,
-        forPrint: props.forPrint
-      })
-
-    } else {
-      return this.renderMirrorEvents(false)
-    }
-  }
+  handleScrollRequest = (request: ScrollRequest) => {
+    let { onScrollTop } = this.props
+    let { slatCoords } = this.state
 
+    if (onScrollTop && slatCoords) {
 
-  handleSizing = () => {
-    let { segRenderers } = this
+      if (request.time) {
+        let top = slatCoords.computeTimeTop(request.time)
+        top = Math.ceil(top) // zoom can give weird floating-point values. rather scroll a little bit further
+        if (top) { top++ } // to overcome top border that slots beyond the first have. looks better
 
-    if (this.props.allowSizing) {
-      this.slatPositions.build()
-      this.colPositions.build()
-    } else {
-      this.slatPositions.buildZeros()
-      this.colPositions.buildZeros()
-    }
-
-    for (let segRenderer of segRenderers) {
-      if (segRenderer) {
-        segRenderer.computeSizes(true, this) // TODO: always forced!?
-      }
-    }
-
-    for (let segRenderer of segRenderers) {
-      if (segRenderer) {
-        segRenderer.assignSizes(true, this) // TODO: always forced!?
+        onScrollTop(top)
       }
-    }
-
-    this.renderNowIndicator({ // why in sizing???
-      date: this.props.nowIndicatorDate,
-      segs: this.props.nowIndicatorSegs
-    })
-  }
-
-
-  /* Now Indicator
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  _renderNowIndicator({ date, segs }: { date: DateMarker, segs: TimeColsSeg[] }) {
-
-    if (!date) {
-      return []
-    }
-
-    let top = this.computeDateTop(date)
-    let nodes: HTMLElement[] = []
-    let i
-
-    // render lines within the columns
-    for (i = 0; i < segs.length; i++) {
-      let lineEl = document.createElement('div')
-      lineEl.className = 'fc-now-indicator fc-now-indicator-line'
-      lineEl.style.top = top + 'px'
-      this.colContainerEls[segs[i].col].appendChild(lineEl)
-      nodes.push(lineEl)
-    }
-
-    // render an arrow over the axis
-    if (segs.length > 0) { // is the current time in view?
-      let arrowEl = document.createElement('div')
-      arrowEl.className = 'fc-now-indicator fc-now-indicator-arrow'
-      arrowEl.style.top = top + 'px'
-      this.rootEl.appendChild(arrowEl)
-      nodes.push(arrowEl)
-    }
-
-    return nodes
-  }
-
-
-  _unrenderNowIndicator(nodes: HTMLElement[]) {
-    nodes.forEach(removeElement)
-  }
-
-
-  /* Coordinates
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  getTotalSlatHeight() {
-    return this.slatRootEl.getBoundingClientRect().height
-  }
-
 
-  // Computes the top coordinate, relative to the bounds of the grid, of the given date.
-  // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
-  computeDateTop(when: DateMarker, startOfDayDate?: DateMarker) {
-    if (!startOfDayDate) {
-      startOfDayDate = startOfDay(when)
+      return true
     }
-    return this.computeTimeTop(createDuration(when.valueOf() - startOfDayDate.valueOf()))
   }
 
 
-  // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
-  computeTimeTop(duration: Duration) {
-    let len = this.slatEls.length
-    let dateProfile = this.props.dateProfile
-    let slatCoverage = (duration.milliseconds - asRoughMs(dateProfile.minTime)) / asRoughMs(this.slotDuration) // floating-point value of # of slots covered
-    let slatIndex
-    let slatRemainder
-
-    // compute a floating-point number for how many slats should be progressed through.
-    // from 0 to number of slats (inclusive)
-    // constrained because minTime/maxTime might be customized.
-    slatCoverage = Math.max(0, slatCoverage)
-    slatCoverage = Math.min(len, slatCoverage)
-
-    // an integer index of the furthest whole slat
-    // from 0 to number slats (*exclusive*, so len-1)
-    slatIndex = Math.floor(slatCoverage)
-    slatIndex = Math.min(slatIndex, len - 1)
-
-    // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
-    // could be 1.0 if slatCoverage is covering *all* the slots
-    slatRemainder = slatCoverage - slatIndex
-
-    return this.slatPositions.tops[slatIndex] +
-      this.slatPositions.getHeight(slatIndex) * slatRemainder
+  handlColCoords = (colCoords: PositionCache | null) => {
+    this.setState({ colCoords })
   }
 
 
-  // For each segment in an array, computes and assigns its top and bottom properties
-  computeSegVerticals(segs: Seg[]) {
-    let { options } = this.context
-    let eventMinHeight = options.timeGridEventMinHeight
-    let i
-    let seg
-    let dayDate
-
-    for (i = 0; i < segs.length; i++) {
-      seg = segs[i]
-      dayDate = this.props.cells[seg.col].date
-
-      seg.top = this.computeDateTop(seg.start, dayDate)
-      seg.bottom = Math.max(
-        seg.top + eventMinHeight,
-        this.computeDateTop(seg.end, dayDate)
-      )
-    }
-  }
-
-
-  // Given segments that already have their top/bottom properties computed, applies those values to
-  // the segments' elements.
-  assignSegVerticals(segs) {
-    let i
-    let seg
-
-    for (i = 0; i < segs.length; i++) {
-      seg = segs[i]
-      applyStyle(seg.el, this.generateSegVerticalCss(seg))
-    }
-  }
-
-
-  // Generates an object with CSS properties for the top/bottom coordinates of a segment element
-  generateSegVerticalCss(seg) {
-    return {
-      top: seg.top,
-      bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
-    }
-  }
-
-
-  /* Sizing
-  ------------------------------------------------------------------------------------------------------------------*/
+  handleSlatCoords = (coords: PositionCache | null) => {
+    let { props } = this
 
+    let slatCoords = coords ? new TimeColsSlatsCoords(
+      coords,
+      props.dateProfile,
+      this.slotDuration,
+      props.cells,
+      this.context
+    ) : null
 
-  buildPositionCaches() {
-    this.colPositions.build()
-    this.slatPositions.build()
+    this.setState({ slatCoords })
   }
 
 
-  /* Hit System
-  ------------------------------------------------------------------------------------------------------------------*/
-
   positionToHit(positionLeft, positionTop) {
     let { dateEnv } = this.context
-    let { snapsPerSlot, slatPositions, colPositions } = this
+    let { colCoords, slatCoords } = this.state
+    let { snapsPerSlot, snapDuration } = this
 
-    let colIndex = colPositions.leftToIndex(positionLeft)
-    let slatIndex = slatPositions.topToIndex(positionTop)
+    let colIndex = colCoords.leftToIndex(positionLeft)
+    let slatIndex = slatCoords.positions.topToIndex(positionTop)
 
     if (colIndex != null && slatIndex != null) {
-      let slatTop = slatPositions.tops[slatIndex]
-      let slatHeight = slatPositions.getHeight(slatIndex)
+      let slatTop = slatCoords.positions.tops[slatIndex]
+      let slatHeight = slatCoords.positions.getHeight(slatIndex)
       let partial = (positionTop - slatTop) / slatHeight // floating point number between 0 and 1
       let localSnapIndex = Math.floor(partial * snapsPerSlot) // the snap # relative to start of slat
       let snapIndex = slatIndex * snapsPerSlot + localSnapIndex
@@ -499,11 +199,11 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
       let dayDate = this.props.cells[colIndex].date
       let time = addDurations(
         this.props.dateProfile.minTime,
-        multiplyDuration(this.snapDuration, snapIndex)
+        multiplyDuration(snapDuration, snapIndex)
       )
 
       let start = dateEnv.add(dayDate, time)
-      let end = dateEnv.add(start, this.snapDuration)
+      let end = dateEnv.add(start, snapDuration)
 
       return {
         col: colIndex,
@@ -511,10 +211,10 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
           range: { start, end },
           allDay: false
         },
-        dayEl: this.colEls[colIndex],
+        dayEl: colCoords.els[colIndex],
         relativeRect: {
-          left: colPositions.lefts[colIndex],
-          right: colPositions.rights[colIndex],
+          left: colCoords.lefts[colIndex],
+          right: colCoords.rights[colIndex],
           top: slatTop,
           bottom: slatTop + slatHeight
         }
@@ -569,3 +269,21 @@ function groupSegsByCol(segs, colCnt) {
 
   return segsByCol
 }
+
+
+function processSlotOptions(options) {
+  let { slotDuration, snapDuration } = options
+  let snapsPerSlot
+
+  slotDuration = createDuration(slotDuration)
+  snapDuration = snapDuration ? createDuration(snapDuration) : slotDuration
+  snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration)
+
+  if (snapsPerSlot === null) {
+    snapDuration = slotDuration
+    snapsPerSlot = 1
+    // TODO: say warning?
+  }
+
+  return { slotDuration, snapDuration, snapsPerSlot }
+}

+ 77 - 0
packages/timegrid/src/TimeColsBg.tsx

@@ -0,0 +1,77 @@
+import { h, BaseComponent, ComponentContext, VNode, CssDimValue, DateProfile, RefMap, PositionCache, createRef } from '@fullcalendar/core'
+import { DayBgRow, DayBgCellModel } from '@fullcalendar/daygrid'
+
+
+export interface TimeColsBgProps {
+  dateProfile: DateProfile
+  cells: DayBgCellModel[]
+  clientWidth: CssDimValue
+  tableMinWidth: CssDimValue
+  tableColGroupNode: VNode
+  renderIntro: () => VNode[]
+  onCoords?: (coords: PositionCache | null) => void
+}
+
+
+export default class TimeColsBg extends BaseComponent<TimeColsBgProps> {
+
+  rootElRef = createRef<HTMLDivElement>()
+  cellElRefs = new RefMap<HTMLTableCellElement>()
+
+
+  render(props: TimeColsBgProps, state: {}, context: ComponentContext) {
+    return (
+      <div class='fc-bg' ref={this.rootElRef}>
+        <table class={context.theme.getClass('table')} style={{
+          minWidth: props.tableMinWidth,
+          width: props.clientWidth
+        }}>
+          {props.tableColGroupNode}
+          <DayBgRow
+            dateProfile={props.dateProfile}
+            cells={props.cells}
+            renderIntro={props.renderIntro}
+            cellElRefs={this.cellElRefs}
+          />
+        </table>
+      </div>
+    )
+  }
+
+
+  componentDidMount() {
+    this.handleSizing()
+    this.context.addResizeHandler(this.handleSizing)
+  }
+
+
+  componentDidUpdate() {
+    this.handleSizing()
+  }
+
+
+  componentWillUnmount() {
+    this.context.removeResizeHandler(this.handleSizing)
+
+    if (this.props.onCoords) {
+      this.props.onCoords(null)
+    }
+  }
+
+
+  handleSizing = () => {
+    let { props } = this
+
+    if (props.onCoords && props.tableColGroupNode && props.clientWidth) {
+      props.onCoords(
+        new PositionCache(
+          this.rootElRef.current,
+          this.cellElRefs.collect(),
+          true, // horizontal
+          false
+        )
+      )
+    }
+  }
+
+}

+ 275 - 0
packages/timegrid/src/TimeColsContent.tsx

@@ -0,0 +1,275 @@
+import {
+  h, VNode,
+  BaseComponent,
+  findElements,
+  subrenderer,
+  removeElement,
+  EventSegUiInteractionState,
+  CssDimValue,
+  DateMarker,
+} from '@fullcalendar/core'
+import TimeColsMirrorEvents from './TimeColsMirrorEvents'
+import TimeColsEvents from './TimeColsEvents'
+import TimeColsFills from './TimeColsFills'
+import { TimeColsSeg } from './TimeCols'
+import TimeColsSlatsCoords from './TimeColsSlatsCoords'
+
+
+export interface TimeColsContentProps extends TimeColsContentBaseProps {
+  clientWidth: CssDimValue
+  tableMinWidth: CssDimValue
+  tableColGroupNode: VNode
+  nowIndicatorDate: DateMarker | null
+  coords: TimeColsSlatsCoords
+}
+
+
+export default class TimeColsContent extends BaseComponent<TimeColsContentProps> {
+
+  render(props: TimeColsContentProps) {
+    let nowIndicatorTop = props.coords && props.coords.safeComputeTop(props.nowIndicatorDate)
+
+    return (
+      <div class='fc-content-skeleton'>
+        <table style={{
+          minWidth: props.tableMinWidth,
+          width: props.clientWidth
+        }}>
+          {props.tableColGroupNode}
+          <TimeColsContentBody
+            colCnt={props.colCnt}
+            businessHourSegs={props.businessHourSegs}
+            bgEventSegs={props.bgEventSegs}
+            fgEventSegs={props.fgEventSegs}
+            dateSelectionSegs={props.dateSelectionSegs}
+            eventSelection={props.eventSelection}
+            eventDrag={props.eventDrag}
+            eventResize={props.eventResize}
+            nowIndicatorTop={nowIndicatorTop}
+            nowIndicatorSegs={props.nowIndicatorSegs}
+            coords={props.coords}
+            forPrint={props.forPrint}
+            renderIntro={props.renderIntro}
+          />
+        </table>
+        {nowIndicatorTop != null &&
+          <div
+            class='fc-now-indicator fc-now-indicator-arrow'
+            style={{ top: nowIndicatorTop }}
+          />
+        }
+      </div>
+    )
+  }
+
+}
+
+
+interface TimeColsContentBodyProps extends TimeColsContentBaseProps {
+  nowIndicatorTop: number
+}
+
+interface TimeColsContentBaseProps {
+  colCnt: number
+  businessHourSegs: TimeColsSeg[]
+  bgEventSegs: TimeColsSeg[]
+  fgEventSegs: TimeColsSeg[]
+  dateSelectionSegs: TimeColsSeg[]
+  eventSelection: string
+  eventDrag: EventSegUiInteractionState | null
+  eventResize: EventSegUiInteractionState | null
+  nowIndicatorSegs: TimeColsSeg[]
+  coords: TimeColsSlatsCoords
+  forPrint: boolean
+  renderIntro: () => VNode[]
+}
+
+
+class TimeColsContentBody extends BaseComponent<TimeColsContentBodyProps> {
+
+  private renderMirrorEvents = subrenderer(TimeColsMirrorEvents)
+  private renderFgEvents = subrenderer(TimeColsEvents)
+  private renderBgEvents = subrenderer(TimeColsFills)
+  private renderBusinessHours = subrenderer(TimeColsFills)
+  private renderDateSelection = subrenderer(TimeColsFills)
+  private renderNowIndicator = subrenderer(this._renderNowIndicator, this._unrenderNowIndicator)
+
+  private colContainerEls: HTMLElement[]
+  private mirrorContainerEls: HTMLElement[]
+  private fgContainerEls: HTMLElement[]
+  private bgContainerEls: HTMLElement[]
+  private highlightContainerEls: HTMLElement[]
+  private businessContainerEls: HTMLElement[]
+
+
+  render(props: TimeColsContentBodyProps) {
+    let cellNodes: VNode[] = props.renderIntro()
+
+    for (let i = 0; i < props.colCnt; i++) {
+      cellNodes.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>
+      )
+    }
+
+    return (
+      <tbody ref={this.handleRootEl}>
+        <tr>{cellNodes}</tr>
+      </tbody>
+    )
+  }
+
+
+  handleRootEl = (rootEl: HTMLElement) => {
+    if (rootEl) {
+      this.colContainerEls = findElements(rootEl, '.fc-content-col')
+      this.mirrorContainerEls = findElements(rootEl, '.fc-mirror-container')
+      this.fgContainerEls = findElements(rootEl, '.fc-event-container:not(.fc-mirror-container)')
+      this.bgContainerEls = findElements(rootEl, '.fc-bgevent-container')
+      this.highlightContainerEls = findElements(rootEl, '.fc-highlight-container')
+      this.businessContainerEls = findElements(rootEl, '.fc-business-container')
+    }
+  }
+
+
+  componentDidMount() {
+    this.subrender()
+  }
+
+
+  componentDidUpdate() {
+    this.subrender()
+  }
+
+
+  componentWillMount() {
+    this.subrenderDestroy()
+  }
+
+
+  subrender() {
+    let { props } = this
+    let { options } = this.context
+
+    this.renderBusinessHours({
+      type: 'businessHours',
+      containerEls: this.businessContainerEls,
+      segs: props.businessHourSegs,
+      coords: props.coords
+    })
+
+    this.renderDateSelection({
+      type: 'highlight',
+      containerEls: this.highlightContainerEls,
+      segs: options.selectMirror ? [] : props.dateSelectionSegs, // do highlight if NO mirror
+      coords: props.coords
+    })
+
+    this.renderBgEvents({
+      type: 'bgEvent',
+      containerEls: this.bgContainerEls,
+      segs: props.bgEventSegs,
+      coords: props.coords
+    })
+
+    this.renderFgEvents({
+      containerEls: this.fgContainerEls,
+      segs: props.fgEventSegs,
+      selectedInstanceId: props.eventSelection,
+      hiddenInstances: // TODO: more convenient
+        (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
+        (props.eventResize ? props.eventResize.affectedInstances : null),
+      isDragging: false,
+      isResizing: false,
+      isSelecting: false,
+      forPrint: props.forPrint,
+      coords: props.coords
+    })
+
+    this.subrenderMirror(this.mirrorContainerEls, options)
+
+    this.renderNowIndicator({
+      nowIndicatorTop: props.nowIndicatorTop,
+      segs: props.nowIndicatorSegs
+    })
+  }
+
+
+  subrenderMirror(mirrorContainerEls: HTMLElement[], options): TimeColsEvents | null {
+    let { props } = this
+
+    if (props.eventDrag && props.eventDrag.segs.length) { // messy check
+      return this.renderMirrorEvents({
+        containerEls: mirrorContainerEls,
+        segs: props.eventDrag.segs,
+        isDragging: true,
+        isResizing: false,
+        isSelecting: false,
+        interactingSeg: props.eventDrag.interactingSeg,
+        forPrint: props.forPrint,
+        coords: props.coords
+      })
+
+    } else if (props.eventResize && props.eventResize.segs.length) {
+      return this.renderMirrorEvents({
+        containerEls: mirrorContainerEls,
+        segs: props.eventResize.segs,
+        isDragging: true,
+        isResizing: false,
+        isSelecting: false,
+        interactingSeg: props.eventResize.interactingSeg,
+        forPrint: props.forPrint,
+        coords: props.coords
+      })
+
+    } else if (options.selectMirror) {
+      return this.renderMirrorEvents({
+        containerEls: mirrorContainerEls,
+        segs: props.dateSelectionSegs,
+        isDragging: false,
+        isResizing: false,
+        isSelecting: true,
+        forPrint: props.forPrint,
+        coords: props.coords
+      })
+
+    } else {
+      return this.renderMirrorEvents(false)
+    }
+  }
+
+
+  _renderNowIndicator({ nowIndicatorTop, segs }: { nowIndicatorTop: number | null, segs: TimeColsSeg[] }) {
+
+    if (nowIndicatorTop == null) {
+      return []
+    }
+
+    let nodes: HTMLElement[] = []
+    let i
+
+    // render lines within the columns
+    for (i = 0; i < segs.length; i++) {
+      let lineEl = document.createElement('div')
+      lineEl.className = 'fc-now-indicator fc-now-indicator-line'
+      lineEl.style.top = nowIndicatorTop + 'px'
+      this.colContainerEls[segs[i].col].appendChild(lineEl)
+      nodes.push(lineEl)
+    }
+
+    return nodes
+  }
+
+
+  _unrenderNowIndicator(nodes: HTMLElement[]) {
+    nodes.forEach(removeElement)
+  }
+
+}

+ 0 - 98
packages/timegrid/src/TimeColsContentSkeleton.tsx

@@ -1,98 +0,0 @@
-import {
-  h, VNode,
-  BaseComponent,
-  findElements,
-  createRef,
-} from '@fullcalendar/core'
-
-
-export interface TimeColsContentSkeletonProps {
-  colCnt: number
-  renderIntro: () => VNode[]
-  onReceiveContainerEls?: (containers: TimeColsContentSkeletonContainers | null) => void
-}
-
-export interface TimeColsContentSkeletonContainers {
-  colContainerEls: HTMLElement[]
-  mirrorContainerEls: HTMLElement[]
-  fgContainerEls: HTMLElement[]
-  bgContainerEls: HTMLElement[]
-  highlightContainerEls: HTMLElement[]
-  businessContainerEls: HTMLElement[]
-}
-
-
-export default class TimeColsContentSkeleton extends BaseComponent<TimeColsContentSkeletonProps> {
-
-  rootElRef = createRef<HTMLTableRowElement>()
-
-
-  render(props: TimeColsContentSkeletonProps) {
-    let cellNodes: VNode[] = props.renderIntro()
-
-    for (let i = 0; i < props.colCnt; i++) {
-      cellNodes.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>
-      )
-    }
-
-    return (
-      <tr ref={this.rootElRef}>{cellNodes}</tr>
-    )
-  }
-
-
-  componentDidMount() {
-    this.sendDom()
-  }
-
-
-  componentDidUpdate() {
-    this.sendDom()
-  }
-
-
-  componentWillUnmount() {
-    let { onReceiveContainerEls } = this.props
-    if (onReceiveContainerEls) {
-      onReceiveContainerEls(null)
-    }
-  }
-
-
-  sendDom() {
-    let { onReceiveContainerEls } = this.props
-
-    if (onReceiveContainerEls) {
-      let rootEl = this.rootElRef.current
-      let colContainerEls = findElements(rootEl, '.fc-content-col')
-      let mirrorContainerEls = findElements(rootEl, '.fc-mirror-container')
-      let fgContainerEls = findElements(rootEl, '.fc-event-container:not(.fc-mirror-container)')
-      let bgContainerEls = findElements(rootEl, '.fc-bgevent-container')
-      let highlightContainerEls = findElements(rootEl, '.fc-highlight-container')
-      let businessContainerEls = findElements(rootEl, '.fc-business-container')
-
-      onReceiveContainerEls({
-        colContainerEls,
-        mirrorContainerEls,
-        fgContainerEls,
-        bgContainerEls,
-        highlightContainerEls,
-        businessContainerEls
-      })
-    }
-  }
-
-}
-
-TimeColsContentSkeleton.addPropsEquality({
-  onReceiveContainerEls: true
-})

+ 29 - 13
packages/timegrid/src/TimeColsEvents.ts

@@ -6,11 +6,13 @@ import {
   Seg, isMultiDayRange, compareByFieldSpecs,
   computeEventDraggable, computeEventStartResizable, computeEventEndResizable, ComponentContext, BaseFgEventRendererProps, subrenderer
 } from '@fullcalendar/core'
-import TimeCols, { attachSegs, detachSegs } from './TimeCols'
+import { attachSegs, detachSegs } from './TimeCols'
+import TimeColsSlatsCoords from './TimeColsSlatsCoords'
 
 export interface TimeColsEventsProps extends BaseFgEventRendererProps {
   containerEls: HTMLElement[]
   forPrint: boolean
+  coords: TimeColsSlatsCoords
 }
 
 /*
@@ -27,6 +29,7 @@ export default class TimeColsEvents extends FgEventRenderer<TimeColsEventsProps>
 
 
   render(props: TimeColsEventsProps, context: ComponentContext) {
+    let { coords } = props
     this.updateFormatter(context.options)
 
     let segs = this.renderSegs({
@@ -43,6 +46,11 @@ export default class TimeColsEvents extends FgEventRenderer<TimeColsEventsProps>
       segs,
       containerEls: props.containerEls
     })
+
+    if (coords) {
+      this.computeSegSizes(segs, coords)
+      this.assignSegSizes(segs, coords)
+    }
   }
 
 
@@ -55,11 +63,11 @@ export default class TimeColsEvents extends FgEventRenderer<TimeColsEventsProps>
   }
 
 
-  computeSegSizes(allSegs: Seg[], timeCols: TimeCols) {
+  computeSegSizes(allSegs: Seg[], slatCoords: TimeColsSlatsCoords) {
     let { segsByCol } = this
-    let colCnt = timeCols.props.cells.length
+    let colCnt = slatCoords.cells.length
 
-    timeCols.computeSegVerticals(allSegs) // horizontals relies on this
+    slatCoords.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
@@ -67,24 +75,24 @@ export default class TimeColsEvents extends FgEventRenderer<TimeColsEventsProps>
   }
 
 
-  assignSegSizes(allSegs: Seg[], timeCols: TimeCols) {
+  assignSegSizes(allSegs: Seg[], slatCoords: TimeColsSlatsCoords) {
     let { segsByCol } = this
-    let colCnt = timeCols.props.cells.length
+    let colCnt = slatCoords.cells.length
 
-    timeCols.assignSegVerticals(allSegs) // horizontals relies on this
+    slatCoords.assignSegVerticals(allSegs) // horizontals relies on this
 
     for (let col = 0; col < colCnt; col++) {
-      this.assignSegCss(segsByCol[col], timeCols)
+      this.assignSegCss(segsByCol[col], slatCoords)
     }
   }
 
 
   // Given foreground event segments that have already had their position coordinates computed,
   // assigns position-related CSS values to their elements.
-  assignSegCss(segs: Seg[], timeCols: TimeCols) {
+  assignSegCss(segs: Seg[], slatCoords: TimeColsSlatsCoords) {
 
     for (let seg of segs) {
-      applyStyle(seg.el, this.generateSegCss(seg, timeCols))
+      applyStyle(seg.el, this.generateSegCss(seg, slatCoords))
 
       if (seg.level > 0) {
         seg.el.classList.add('fc-time-grid-event-inset')
@@ -97,6 +105,8 @@ export default class TimeColsEvents extends FgEventRenderer<TimeColsEventsProps>
         seg.eventRange.def.title && seg.bottom - seg.top < 30
       ) {
         seg.el.classList.add('fc-short') // TODO: "condensed" is a better name
+      } else {
+        seg.el.classList.remove('fc-short') // ugh
       }
     }
   }
@@ -104,12 +114,12 @@ export default class TimeColsEvents extends FgEventRenderer<TimeColsEventsProps>
 
   // 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, timeCols: TimeCols) {
+  generateSegCss(seg: Seg, slatCoords: TimeColsSlatsCoords) {
     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 = timeCols.generateSegVerticalCss(seg) as any // get top/bottom first
+    let props = slatCoords.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
 
@@ -242,6 +252,12 @@ function computeSegHorizontals(segs: Seg[], context: ComponentContext) {
   let level0
   let i
 
+  // why do we need to clear!?
+  for (let seg of segs) {
+    seg.forwardCoord = null
+    seg.backwardCoord = null
+  }
+
   levels = buildSlotSegLevels(segs)
   computeForwardSlotSegs(levels)
 
@@ -270,7 +286,7 @@ function computeSegForwardBack(seg: Seg, seriesBackwardPressure, seriesBackwardC
   let forwardSegs = seg.forwardSegs
   let i
 
-  if (seg.forwardCoord === undefined) { // not already computed
+  if (seg.forwardCoord == null) { // not already computed
 
     if (!forwardSegs.length) {
 

+ 10 - 13
packages/timegrid/src/TimeColsFills.ts

@@ -1,10 +1,10 @@
-import {
-  FillRenderer, Seg, subrenderer, BaseFillRendererProps
-} from '@fullcalendar/core'
-import TimeCols, { attachSegs, detachSegs } from './TimeCols'
+import { FillRenderer, subrenderer, BaseFillRendererProps } from '@fullcalendar/core'
+import { attachSegs, detachSegs } from './TimeCols'
+import TimeColsSlatsCoords from './TimeColsSlatsCoords'
 
 export interface TimeColsFillsProps extends BaseFillRendererProps {
   containerEls: HTMLElement[]
+  coords: TimeColsSlatsCoords
 }
 
 export default class TimeColsFills extends FillRenderer<TimeColsFillsProps> {
@@ -13,6 +13,8 @@ export default class TimeColsFills extends FillRenderer<TimeColsFillsProps> {
 
 
   render(props: TimeColsFillsProps) {
+    let { coords } = props
+
     let segs = this.renderSegs({
       type: props.type,
       segs: props.segs
@@ -22,16 +24,11 @@ export default class TimeColsFills extends FillRenderer<TimeColsFillsProps> {
       segs,
       containerEls: props.containerEls
     })
-  }
-
-
-  computeSegSizes(segs: Seg[], timeGrid: TimeCols) {
-    timeGrid.computeSegVerticals(segs)
-  }
-
 
-  assignSegSizes(segs: Seg[], timeGrid: TimeCols) {
-    timeGrid.assignSegVerticals(segs)
+    if (coords) {
+      coords.computeSegVerticals(segs)
+      coords.assignSegVerticals(segs)
+    }
   }
 
 }

+ 4 - 4
packages/timegrid/src/TimeColsMirrorEvents.ts

@@ -1,17 +1,17 @@
 import { Seg } from '@fullcalendar/core'
 import TimeColsEvents from './TimeColsEvents'
-import TimeCols from './TimeCols'
+import TimeColsSlatsCoords from './TimeColsSlatsCoords'
 
 
 export default class TimeColsMirrorEvents extends TimeColsEvents {
 
 
-  generateSegCss(seg: Seg, timeGrid: TimeCols) {
-    let cssProps = super.generateSegCss(seg, timeGrid)
+  generateSegCss(seg: Seg, slatCoords: TimeColsSlatsCoords) {
+    let cssProps = super.generateSegCss(seg, slatCoords)
     let { interactingSeg } = this.props
 
     if (interactingSeg && interactingSeg.col === seg.col) {
-      let sourceSegProps = super.generateSegCss(interactingSeg, timeGrid)
+      let sourceSegProps = super.generateSegCss(interactingSeg, slatCoords)
 
       cssProps.left = sourceSegProps.left
       cssProps.right = sourceSegProps.right

+ 94 - 35
packages/timegrid/src/TimeColsSlats.tsx

@@ -12,17 +12,27 @@ import {
   Duration,
   createFormatter,
   memoize,
-  Fragment,
-  RefMap
+  RefMap,
+  CssDimValue,
+  createRef,
+  PositionCache
 } from '@fullcalendar/core'
 
 
-export interface TimeColsSlatsProps {
+export interface TimeColsSlatsProps extends TimeColsSlatsContentProps {
+  clientWidth: CssDimValue
+  clientHeight: CssDimValue
+  tableMinWidth: CssDimValue
+  tableColGroupNode: VNode
+  onCoords?: (coords: PositionCache | null) => void
+}
+
+interface TimeColsSlatsContentProps {
   dateProfile: DateProfile
   slotDuration: Duration
-  onReceiveSlatEls?: (slatEls: HTMLElement[] | null) => void
 }
 
+
 // potential nice values for the slot-duration and interval-duration
 // from largest to smallest
 const STOCK_SUB_DURATIONS = [
@@ -36,16 +46,91 @@ const STOCK_SUB_DURATIONS = [
 /*
 for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
 */
+
+
 export default class TimeColsSlats extends BaseComponent<TimeColsSlatsProps> {
 
-  private slatElRefs = new RefMap<HTMLTableRowElement>()
+  rootElRef = createRef<HTMLDivElement>()
+  slatElRefs = new RefMap<HTMLTableRowElement>()
+
+
+  render(props: TimeColsSlatsProps, state: {}, context: ComponentContext) {
+    let { theme } = context
+
+    return (
+      <div class='fc-slats' ref={this.rootElRef}>
+        <table
+          class={theme.getClass('table') + ' vgrow' /* why not use rowsGrow like resource view? */}
+          style={{
+            minWidth: props.tableMinWidth,
+            width: props.clientWidth,
+            height: props.clientHeight
+          }}
+        >
+          {props.tableColGroupNode /* relies on there only being a single <col> for the axis */}
+          <TimeColsSlatsBody
+            slatElRefs={this.slatElRefs}
+            dateProfile={props.dateProfile}
+            slotDuration={props.slotDuration}
+          />
+        </table>
+      </div>
+    )
+  }
+
+
+  componentDidMount() {
+    this.handleSizing()
+    this.context.addResizeHandler(this.handleSizing)
+  }
+
+
+  componentDidUpdate() {
+    this.handleSizing()
+  }
+
+
+  componentWillUnmount() {
+    this.context.removeResizeHandler(this.handleSizing)
+
+    if (this.props.onCoords) {
+      this.props.onCoords(null)
+    }
+  }
+
+
+  handleSizing = () => {
+    let { props } = this
+
+    if (props.onCoords && props.clientHeight) {
+      props.onCoords(
+        new PositionCache(
+          this.rootElRef.current,
+          this.slatElRefs.collect(),
+          false,
+          true // vertical
+        )
+      )
+    }
+  }
+
+}
+
+
+interface TimeColsSlatsBodyProps extends TimeColsSlatsContentProps {
+  slatElRefs: RefMap<HTMLTableRowElement>
+}
+
+
+class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> {
+
   private getLabelInterval = memoize(getLabelInterval)
   private getLabelFormat = memoize(getLabelFormat)
 
 
-  render(props: TimeColsSlatsProps, state: {}, context: ComponentContext) {
+  render(props: TimeColsSlatsBodyProps, state: {}, context: ComponentContext) {
     let { dateEnv, isRtl, options } = context
-    let { dateProfile, slotDuration } = props
+    let { dateProfile, slotDuration, slatElRefs } = props
 
     let labelInterval = this.getLabelInterval(options.slotLabelInterval, slotDuration)
     let labelFormat = this.getLabelFormat(options.slotLabelFormat)
@@ -78,7 +163,7 @@ export default class TimeColsSlats extends BaseComponent<TimeColsSlatsProps> {
 
       rowsNodes.push(
         <tr
-          ref={this.slatElRefs.createRef(i)}
+          ref={slatElRefs.createRef(i)}
           data-time={formatIsoTimeString(slotDate)}
           class={isLabeled ? '' : 'fc-minor'}
         >
@@ -93,33 +178,7 @@ export default class TimeColsSlats extends BaseComponent<TimeColsSlatsProps> {
       i++
     }
 
-    return (<Fragment>{rowsNodes}</Fragment>)
-  }
-
-
-  componentDidMount() {
-    this.sendDom()
-  }
-
-
-  componentDidUpdate() {
-    this.sendDom()
-  }
-
-
-  componentWillUnmount() {
-    let { onReceiveSlatEls } = this.props
-    if (onReceiveSlatEls) {
-      onReceiveSlatEls(null)
-    }
-  }
-
-
-  sendDom() {
-    let { onReceiveSlatEls } = this.props
-    if (onReceiveSlatEls) {
-      onReceiveSlatEls(this.slatElRefs.collect())
-    }
+    return (<tbody>{rowsNodes}</tbody>)
   }
 
 }

+ 105 - 0
packages/timegrid/src/TimeColsSlatsCoords.ts

@@ -0,0 +1,105 @@
+import { PositionCache, DateMarker, startOfDay, createDuration, asRoughMs, DateProfile, Duration, Seg, applyStyle, ComponentContext, rangeContainsMarker } from '@fullcalendar/core'
+import { DayBgCellModel } from '@fullcalendar/daygrid'
+
+
+export default class TimeColsSlatsCoords {
+
+
+  constructor(
+    public positions: PositionCache,
+    private dateProfile: DateProfile,
+    private slotDuration: Duration,
+    public cells: DayBgCellModel[],
+    private context: ComponentContext
+  ) {
+  }
+
+
+  safeComputeTop(date: DateMarker | null) {
+    if (date && rangeContainsMarker(this.dateProfile.currentRange, date)) {
+      return this.computeDateTop(date)
+    }
+  }
+
+
+  // Computes the top coordinate, relative to the bounds of the grid, of the given date.
+  // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
+  computeDateTop(when: DateMarker, startOfDayDate?: DateMarker) {
+    if (!startOfDayDate) {
+      startOfDayDate = startOfDay(when)
+    }
+    return this.computeTimeTop(createDuration(when.valueOf() - startOfDayDate.valueOf()))
+  }
+
+
+  // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
+  computeTimeTop(duration: Duration) {
+    let { positions, dateProfile } = this
+    let len = positions.els.length
+    let slatCoverage = (duration.milliseconds - asRoughMs(dateProfile.minTime)) / asRoughMs(this.slotDuration) // floating-point value of # of slots covered
+    let slatIndex
+    let slatRemainder
+
+    // compute a floating-point number for how many slats should be progressed through.
+    // from 0 to number of slats (inclusive)
+    // constrained because minTime/maxTime might be customized.
+    slatCoverage = Math.max(0, slatCoverage)
+    slatCoverage = Math.min(len, slatCoverage)
+
+    // an integer index of the furthest whole slat
+    // from 0 to number slats (*exclusive*, so len-1)
+    slatIndex = Math.floor(slatCoverage)
+    slatIndex = Math.min(slatIndex, len - 1)
+
+    // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
+    // could be 1.0 if slatCoverage is covering *all* the slots
+    slatRemainder = slatCoverage - slatIndex
+
+    return positions.tops[slatIndex] +
+      positions.getHeight(slatIndex) * slatRemainder
+  }
+
+
+  // For each segment in an array, computes and assigns its top and bottom properties
+  computeSegVerticals(segs: Seg[]) {
+    let { options } = this.context
+    let eventMinHeight = options.timeGridEventMinHeight
+    let i
+    let seg
+    let dayDate
+
+    for (i = 0; i < segs.length; i++) {
+      seg = segs[i]
+      dayDate = this.cells[seg.col].date
+
+      seg.top = this.computeDateTop(seg.start, dayDate)
+      seg.bottom = Math.max(
+        seg.top + eventMinHeight,
+        this.computeDateTop(seg.end, dayDate)
+      )
+    }
+  }
+
+
+  // Given segments that already have their top/bottom properties computed, applies those values to
+  // the segments' elements.
+  assignSegVerticals(segs) {
+    let i
+    let seg
+
+    for (i = 0; i < segs.length; i++) {
+      seg = segs[i]
+      applyStyle(seg.el, this.generateSegVerticalCss(seg))
+    }
+  }
+
+
+  // Generates an object with CSS properties for the top/bottom coordinates of a segment element
+  generateSegVerticalCss(seg) {
+    return {
+      top: seg.top,
+      bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
+    }
+  }
+
+}

+ 8 - 82
packages/timegrid/src/TimeColsView.tsx

@@ -2,17 +2,13 @@ import {
   h, createRef,
   View,
   createFormatter, diffDays,
-  Duration,
   getViewClassNames,
   GotoAnchor,
-  ViewProps,
   SimpleScrollGridSection,
   VNode,
   SimpleScrollGrid,
   ChunkContentCallbackArgs
 } from '@fullcalendar/core'
-import { Table } from '@fullcalendar/daygrid'
-import { TimeCols } from './main'
 import AllDaySplitter from './AllDaySplitter'
 
 
@@ -28,19 +24,8 @@ const AUTO_ALL_DAY_EVENT_LIMIT = 5
 export default abstract class TimeColsView extends View {
 
   protected allDaySplitter = new AllDaySplitter() // for use by subclasses
-
   private rootElRef = createRef<HTMLDivElement>()
-  private dividerElRef = createRef<HTMLTableCellElement>()
   private scrollerElRef = createRef<HTMLDivElement>()
-  private axisWidth: any // the width of the time axis running down the side
-  private needsInitialScroll = false
-
-
-  // abstract requirements
-  // ----------------------------------------------------------------------------------------------------
-
-  abstract getAllDayTableObj(): { table: Table } | null
-  abstract getTimeColsObj(): { timeCols: TimeCols }
 
 
   // rendering
@@ -76,7 +61,6 @@ export default abstract class TimeColsView extends View {
         outerContent: (
           <tr>
             <td
-              ref={this.dividerElRef}
               class={'fc-divider ' + context.theme.getClass('tableCellShaded')}
             />
           </tr>
@@ -101,37 +85,14 @@ export default abstract class TimeColsView extends View {
           vGrow={!props.isHeightAuto}
           cols={[ { width: 'shrink' } ]}
           sections={sections}
-          onSized={this.handleGridSized}
         />
       </div>
     )
   }
 
 
-  componentDidMount() {
-    let allDayTable = this.getAllDayTableObj()
-    let dividerEl = this.dividerElRef.current
-
-    if (allDayTable) {
-      allDayTable.table.bottomCoordPadding = dividerEl.getBoundingClientRect().height
-    }
-
-    this.needsInitialScroll = true
-  }
-
-
-  componentDidUpdate(prevProps: ViewProps) {
-    if (prevProps.dateProfile !== this.props.dateProfile) {
-      this.needsInitialScroll = true
-    }
-  }
-
-
-  handleGridSized = () => {
-    if (this.needsInitialScroll) {
-      this.needsInitialScroll = false
-      this.scrollToInitialTime()
-    }
+  handleScrollTop = (scrollTop: number) => {
+    this.scrollerElRef.current.scrollTop = scrollTop
   }
 
 
@@ -148,32 +109,6 @@ export default abstract class TimeColsView extends View {
   }
 
 
-  /* Scroll
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  scrollToTime(duration: Duration) {
-    let scrollTop = this.computeDateScroll(duration)
-    let scrollerEl = this.scrollerElRef.current
-
-    scrollerEl.scrollTop = scrollTop
-  }
-
-
-  // Computes the initial pre-configured scroll state prior to allowing the user to change it
-  computeDateScroll(duration: Duration) {
-    let top = this.getTimeColsObj().timeCols.computeTimeTop(duration)
-
-    // zoom can give weird floating-point values. rather scroll a little bit further
-    top = Math.ceil(top)
-
-    if (top) {
-      top++ // to overcome top border that slots beyond the first have. looks better
-    }
-
-    return top
-  }
-
 
   /* Header Render Methods
   ------------------------------------------------------------------------------------------------------------------*/
@@ -190,7 +125,7 @@ export default abstract class TimeColsView extends View {
       weekText = dateEnv.format(range.start, WEEK_HEADER_FORMAT)
 
       return [
-        <th class={'fc-axis shrink fc-week-number'} style={this.getAxisStyles()}>
+        <th class={'fc-axis shrink fc-week-number'}>
           <div data-fc-width-all={1}>
             <GotoAnchor
               navLinks={options.navLinks}
@@ -203,20 +138,11 @@ export default abstract class TimeColsView extends View {
     }
 
     return [
-      <th class='fc-axis' style={this.getAxisStyles()}></th>
+      <th class='fc-axis'></th>
     ]
   }
 
 
-  // Generates an HTML attribute string for setting the width of the axis, if it is known
-  getAxisStyles() {
-    if (this.axisWidth != null) {
-      return { width: this.axisWidth }
-    }
-    return {}
-  }
-
-
   /* TimeCols Render Methods
   ------------------------------------------------------------------------------------------------------------------*/
 
@@ -224,7 +150,7 @@ export default abstract class TimeColsView extends View {
   // Generates the HTML that goes before the bg of the TimeCols slot area. Long vertical column.
   renderTimeColsBgIntro = () => {
     return [
-      <td class='fc-axis' style={this.getAxisStyles()}></td>
+      <td class='fc-axis'></td>
     ]
   }
 
@@ -233,7 +159,7 @@ export default abstract class TimeColsView extends View {
   // Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid.
   renderTimeColsIntro = () => {
     return [
-      <td class='fc-axis' style={this.getAxisStyles()}></td>
+      <td class='fc-axis'></td>
     ]
   }
 
@@ -254,7 +180,7 @@ export default abstract class TimeColsView extends View {
     }
 
     return [
-      <td class='shrink fc-axis' style={this.getAxisStyles()}>
+      <td class='shrink fc-axis'>
         <div data-fc-width-all={1}>
           <span {...spanAttrs} data-fc-width-content={1}>
             {child}
@@ -269,7 +195,7 @@ export default abstract class TimeColsView extends View {
   // Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid.
   renderTableIntro = () => {
     return [
-      <td class='fc-axis' style={this.getAxisStyles()}></td>
+      <td class='fc-axis'></td>
     ]
   }
 

+ 1 - 0
packages/timegrid/src/main.ts

@@ -7,6 +7,7 @@ import './main.scss'
 
 export { DayTimeCols, DayTimeColsView, TimeColsView, buildTimeColsModel, buildDayRanges, DayTimeColsSlicer, TimeColsSeg }
 export { default as TimeCols, TIME_COLS_NOW_INDICATOR_UNIT } from './TimeCols'
+export { default as TimeColsSlatsCoords } from './TimeColsSlatsCoords'
 
 export default createPlugin({
   defaultView: 'timeGridWeek',