Преглед изворни кода

first pass on ScrollGrid/updateSize refactor

Adam Shaw пре 6 година
родитељ
комит
007a631a2e
37 измењених фајлова са 930 додато и 850 уклоњено
  1. 1 1
      packages-premium
  2. 0 6
      packages/bootstrap/src/main.ts
  3. 1 1
      packages/core/src/Calendar.tsx
  4. 53 105
      packages/core/src/CalendarComponent.tsx
  5. 0 17
      packages/core/src/View.ts
  6. 38 0
      packages/core/src/ViewContainer.tsx
  7. 4 9
      packages/core/src/common/DayHeader.tsx
  8. 0 116
      packages/core/src/common/Scroller.tsx
  9. 2 0
      packages/core/src/main.scss
  10. 11 4
      packages/core/src/main.ts
  11. 99 0
      packages/core/src/scrollgrid/Scroller.tsx
  12. 116 0
      packages/core/src/scrollgrid/SimpleScrollGrid.tsx
  13. 87 0
      packages/core/src/scrollgrid/main.scss
  14. 211 0
      packages/core/src/scrollgrid/util.tsx
  15. 0 4
      packages/core/src/theme/StandardTheme.ts
  16. 42 0
      packages/core/src/util/RefMap.ts
  17. 7 8
      packages/core/src/util/dom-geom.ts
  18. 1 1
      packages/core/src/util/dom-manip.ts
  19. 0 146
      packages/core/src/util/misc.ts
  20. 8 15
      packages/core/src/util/scrollbar-side.ts
  21. 29 0
      packages/core/src/util/scrollbar-width.ts
  22. 3 2
      packages/core/src/vdom-util.tsx
  23. 2 5
      packages/daygrid/src/DayTable.tsx
  24. 33 44
      packages/daygrid/src/DayTableView.tsx
  25. 9 2
      packages/daygrid/src/Table.tsx
  26. 2 0
      packages/daygrid/src/TableEvents.ts
  27. 10 3
      packages/daygrid/src/TableMirrorEvents.tsx
  28. 4 1
      packages/daygrid/src/TableSkeleton.tsx
  29. 48 153
      packages/daygrid/src/TableView.tsx
  30. 11 25
      packages/list/src/ListView.tsx
  31. 2 5
      packages/timegrid/src/DayTimeCols.tsx
  32. 17 32
      packages/timegrid/src/DayTimeColsView.tsx
  33. 7 6
      packages/timegrid/src/TimeCols.tsx
  34. 2 0
      packages/timegrid/src/TimeColsBg.tsx
  35. 2 0
      packages/timegrid/src/TimeColsContentSkeleton.tsx
  36. 67 139
      packages/timegrid/src/TimeColsView.tsx
  37. 1 0
      tsconfig.json

+ 1 - 1
packages-premium

@@ -1 +1 @@
-Subproject commit 3029448fdc0024937501531ee7232b6f177eed27
+Subproject commit 67f58a5dc1cddc22e3e68b43897fed5cda3a310b

+ 0 - 6
packages/bootstrap/src/main.ts

@@ -20,12 +20,6 @@ BootstrapTheme.prototype.classes = {
   popoverHeader: 'card-header',
   popoverContent: 'card-body',
 
-  // day grid
-  // for left/right border color when border is inset from edges (all-day in timeGrid view)
-  // avoid `table` class b/c don't want margins/padding/structure. only border color.
-  headerRow: 'table-bordered',
-  dayRow: 'table-bordered',
-
   // list view
   listView: 'card card-primary'
 }

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

@@ -351,7 +351,7 @@ export default class Calendar {
     let calendarComponent = this.component
     let viewComponent = calendarComponent.view
 
-    calendarComponent.updateSize(false)
+    // calendarComponent.updateSize(false)
     this.drainAfterSizingCallbacks()
 
     if (this.isViewUpdated) {

+ 53 - 105
packages/core/src/CalendarComponent.tsx

@@ -3,12 +3,10 @@ import { ViewSpec } from './structs/view-spec'
 import View, { ViewProps } from './View'
 import Toolbar from './Toolbar'
 import DateProfileGenerator, { DateProfile } from './DateProfileGenerator'
-import { applyStyle } from './util/dom-manip'
 import { rangeContainsMarker } from './datelib/date-range'
 import { EventUiHash } from './component/event-ui'
 import { parseBusinessHours } from './structs/business-hours'
 import { memoize } from './util/memoize'
-import { computeHeightAndMargins } from './util/dom-geom'
 import { DateMarker } from './datelib/marker'
 import { CalendarState } from './reducers/types'
 import { ViewPropsTransformerClass } from './plugin-system'
@@ -18,6 +16,8 @@ import { BaseComponent, subrenderer } from './vdom-util'
 import { buildDelegationHandler } from './util/dom-event'
 import { capitaliseFirstLetter } from './util/misc'
 import { DelayedRunner } from './util/runner'
+import { applyStyleProp } from './util/dom-manip'
+import ViewContainer from './ViewContainer'
 
 
 export interface CalendarComponentProps extends CalendarState {
@@ -34,7 +34,8 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
   private parseBusinessHours = memoize((input) => parseBusinessHours(input, this.context.calendar))
   private buildViewPropTransformers = memoize(buildViewPropTransformers)
   private buildToolbarProps = memoize(buildToolbarProps)
-  private updateClassNames = subrenderer(setClassNames, unsetClassNames)
+  private updateOuterClassNames = subrenderer(setClassNames, unsetClassNames)
+  private updateOuterHeight = subrenderer(setHeight, unsetHeight)
   private handleNavLinkClick = buildDelegationHandler('a[data-goto]', this._handleNavLinkClick.bind(this))
 
   headerRef = createRef<Toolbar>()
@@ -42,10 +43,6 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
   viewRef = createRef<View>()
   viewContainerEl: HTMLElement
 
-  isSizingDirty = false
-  isHeightAuto: boolean
-  viewHeight: number
-
   get view() { return this.viewRef.current }
 
 
@@ -53,7 +50,7 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
   renders INSIDE of an outer div
   */
   render(props: CalendarComponentProps, state: {}, context: ComponentContext) {
-    let { calendar, header, footer } = context
+    let { calendar, options, header, footer } = context
 
     let toolbarProps = this.buildToolbarProps(
       props.viewSpec,
@@ -64,10 +61,23 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
       props.title
     )
 
-    this.freezeHeight() // thawed after render
-    this.isSizingDirty = true
+    let calendarHeight: string | number = ''
+    let viewHeight: string | number = ''
+    let viewAspectRatio: number | undefined
+
+    if (isHeightAuto(options)) {
+      viewHeight = 'auto'
+      viewAspectRatio = options.aspectRatio
+    } else if (options.height != null) {
+      calendarHeight = options.height
+    } else if (options.contentHeight != null) {
+      viewHeight = options.contentHeight
+    }
 
-    this.updateClassNames({ rootEl: props.rootEl })
+    // TODO: move this somewhere after real render!
+    // move to Calendar class?
+    this.updateOuterClassNames({ el: props.rootEl })
+    this.updateOuterHeight({ el: props.rootEl, height: calendarHeight })
 
     return (
       <Fragment>
@@ -77,18 +87,18 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
             extraClassName='fc-header-toolbar'
             model={header}
             { ...toolbarProps }
-            />
+          />
         }
-        <div class='fc-view-container' ref={this.setViewContainerEl} onClick={this.handleNavLinkClick}>
+        <ViewContainer height={viewHeight} aspectRatio={viewAspectRatio} elRef={this.setViewContainerEl} onClick={this.handleNavLinkClick}>
           {this.renderView(props, this.context)}
-        </div>
+        </ViewContainer>
         {footer &&
           <Toolbar
             ref={this.footerRef}
             extraClassName='fc-footer-toolbar'
             model={footer}
             { ...toolbarProps }
-            />
+          />
         }
       </Fragment>
     )
@@ -96,7 +106,7 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
 
 
   resizeRunner = new DelayedRunner(() => {
-    this.updateSize(true)
+    // TODO: call updateSize or something
     let { calendar, view } = this.context
     calendar.publiclyTrigger('windowResize', [ view ])
   })
@@ -174,7 +184,8 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
       dateSelection: props.dateSelection,
       eventSelection: props.eventSelection,
       eventDrag: props.eventDrag,
-      eventResize: props.eventResize
+      eventResize: props.eventResize,
+      isHeightAuto: isHeightAuto(options)
     }
 
     let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers)
@@ -213,93 +224,14 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
 
 
   updateSize(isResize = false) {
-    this.resizeRunner.whilePaused(() => {
-      if (isResize || this.isSizingDirty) {
-
-        if (isResize || this.isHeightAuto == null) {
-          this.computeHeightVars()
-        }
-
-        let view = this.viewRef.current
-        view.updateSize(isResize, this.viewHeight, this.isHeightAuto)
-        view.updateNowIndicator()
-
-        this.thawHeight()
-        this.isSizingDirty = true
-      }
-    })
-  }
-
-
-  computeHeightVars() {
-    let { calendar } = this.context // yuck. need to handle dynamic options
-    let heightInput = calendar.opt('height')
-    let contentHeightInput = calendar.opt('contentHeight')
-
-    this.isHeightAuto = heightInput === 'auto' || contentHeightInput === 'auto'
-
-    if (typeof contentHeightInput === 'number') { // exists and not 'auto'
-      this.viewHeight = contentHeightInput
-    } else if (typeof contentHeightInput === 'function') { // exists and is a function
-      this.viewHeight = contentHeightInput()
-    } else if (typeof heightInput === 'number') { // exists and not 'auto'
-      this.viewHeight = heightInput - this.queryToolbarsHeight()
-    } else if (typeof heightInput === 'function') { // exists and is a function
-      this.viewHeight = heightInput() - this.queryToolbarsHeight()
-    } else if (heightInput === 'parent') { // set to height of parent element
-      let parentEl = this.props.rootEl.parentNode as HTMLElement
-      this.viewHeight = parentEl.getBoundingClientRect().height - this.queryToolbarsHeight()
-    } else {
-      this.viewHeight = Math.round(
-        this.viewContainerEl.getBoundingClientRect().width /
-        Math.max(calendar.opt('aspectRatio'), .5)
-      )
-    }
-  }
-
-
-  queryToolbarsHeight() {
-    let header = this.headerRef.current
-    let footer = this.footerRef.current
-    let height = 0
-
-    if (header) {
-      height += computeHeightAndMargins(header.rootEl)
-    }
-
-    if (footer) {
-      height += computeHeightAndMargins(footer.rootEl)
-    }
-
-    return height
-  }
-
-
-  // Height "Freezing"
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  freezeHeight() {
-    let { rootEl } = this.props
-
-    applyStyle(rootEl, {
-      height: rootEl.getBoundingClientRect().height,
-      overflow: 'hidden'
-    })
-  }
-
-
-  thawHeight() {
-    let { rootEl } = this.props
-
-    applyStyle(rootEl, {
-      height: '',
-      overflow: ''
-    })
+    // TODO
+    // this.resizeRunner.whilePaused(() => {
+    // })
   }
 
 }
 
+
 function buildToolbarProps(
   viewSpec: ViewSpec,
   dateProfile: DateProfile,
@@ -322,12 +254,17 @@ function buildToolbarProps(
 }
 
 
+function isHeightAuto(options) {
+  return options.height === 'auto' || options.contentHeight === 'auto'
+}
+
+
 // Outer Div Rendering
 // -----------------------------------------------------------------------------------------------------------------
 
 
-function setClassNames({ rootEl }: { rootEl: HTMLElement }, context: ComponentContext) {
-  let classList = rootEl.classList
+function setClassNames({ el }: { el: HTMLElement }, context: ComponentContext) {
+  let classList = el.classList
   let classNames: string[] = [
     'fc',
     'fc-' + context.options.dir,
@@ -338,12 +275,12 @@ function setClassNames({ rootEl }: { rootEl: HTMLElement }, context: ComponentCo
     classList.add(className)
   }
 
-  return { rootEl, classNames }
+  return { el, classNames }
 }
 
 
-function unsetClassNames({ rootEl, classNames }: { rootEl: HTMLElement, classNames: string[] }) {
-  let classList = rootEl.classList
+function unsetClassNames({ el, classNames }: { el: HTMLElement, classNames: string[] }) {
+  let classList = el.classList
 
   for (let className of classNames) {
     classList.remove(className)
@@ -351,6 +288,17 @@ function unsetClassNames({ rootEl, classNames }: { rootEl: HTMLElement, classNam
 }
 
 
+function setHeight({ el, height }: { el: HTMLElement, height: any }) {
+  applyStyleProp(el, 'height', height)
+  return el
+}
+
+function unsetHeight(el: HTMLElement) {
+  applyStyleProp(el, 'height', '')
+}
+
+
+
 // Plugin
 // -----------------------------------------------------------------------------------------------------------------
 

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

@@ -47,23 +47,6 @@ export default abstract class View<State={}> extends DateComponent<ViewProps, St
   nowIndicatorIntervalID: any // "
 
 
-  // Sizing
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  updateSize(isResize: boolean, viewHeight: number, isAuto: boolean) {
-  }
-
-
-  isLayoutSizeDirty() {
-    let { calendar } = this.context
-
-    return calendar.isViewUpdated ||
-      calendar.isDatesUpdated ||
-      calendar.isEventsUpdated
-  }
-
-
   // Event Rendering
   // -----------------------------------------------------------------------------------------------------------------
 

+ 38 - 0
packages/core/src/ViewContainer.tsx

@@ -0,0 +1,38 @@
+import { BaseComponent } from './vdom-util'
+import { ComponentChildren, Ref, h } from './vdom'
+import { CssDimValue } from './scrollgrid/util'
+
+
+export interface ViewContainerProps {
+  height?: CssDimValue
+  aspectRatio?: number
+  onClick?: (ev: Event) => void
+  elRef?: Ref<HTMLDivElement>
+  children?: ComponentChildren
+}
+
+
+// TODO: shouldn't the the fc-view be the inner container???
+// TODO: do function component?
+// TODO: use classnames instead of css props
+export default class ViewContainer extends BaseComponent<ViewContainerProps> {
+
+  render(props: ViewContainerProps) {
+    if (props.height != null) {
+      return (
+        <div class='fc-view-container' style={{ height: props.height }} ref={props.elRef} onClick={props.onClick}>
+          {props.children}
+        </div>
+      )
+    } else {
+      return (
+        <div class='fc-view-container' style={{ paddingBottom: (props.aspectRatio * 100 + '%'), position: 'relative' }} >
+          <div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
+            {props.children}
+          </div>
+        </div>
+      )
+    }
+  }
+
+}

+ 4 - 9
packages/core/src/common/DayHeader.tsx

@@ -7,6 +7,7 @@ import { computeFallbackHeaderFormat } from './table-utils'
 import { VNode, h, createRef } from '../vdom'
 import TableDateCell from './TableDateCell'
 
+
 export interface DayHeaderProps {
   dates: DateMarker[]
   dateProfile: DateProfile
@@ -14,7 +15,8 @@ export interface DayHeaderProps {
   renderIntro?: () => VNode[]
 }
 
-export default class DayHeader extends BaseComponent<DayHeaderProps> {
+
+export default class DayHeader extends BaseComponent<DayHeaderProps> { // TODO: rename to DayHeaderTr?
 
   private rootElRef = createRef<HTMLDivElement>()
 
@@ -22,7 +24,6 @@ export default class DayHeader extends BaseComponent<DayHeaderProps> {
 
 
   render(props: DayHeaderProps, state: {}, context: ComponentContext) {
-    let { theme } = context
     let { dates, datesRepDistinctDays } = props
     let cells: VNode[] = []
 
@@ -52,13 +53,7 @@ export default class DayHeader extends BaseComponent<DayHeaderProps> {
     }
 
     return (
-      <div ref={this.rootElRef} class={'fc-row ' + theme.getClass('headerRow')}>
-        <table class={theme.getClass('tableGrid')}>
-          <thead>
-            <tr>{cells}</tr>
-          </thead>
-        </table>
-      </div>
+      <tr>{cells}</tr>
     )
   }
 

+ 0 - 116
packages/core/src/common/Scroller.tsx

@@ -1,116 +0,0 @@
-import { computeEdges } from '../util/dom-geom'
-import { ElementScrollController } from './scroll-controller'
-import { Component, h, ComponentChildren } from '../vdom'
-import { __assign } from 'tslib'
-
-export interface ScrollbarWidths {
-  left: number
-  right: number
-  bottom: number
-}
-
-export interface ScrollerProps {
-  overflowX: string
-  overflowY: string
-  children?: ComponentChildren
-  extraClassName?: string
-}
-
-/*
-Embodies a div that has potential scrollbars
-*/
-export default class Scroller extends Component<ScrollerProps> { // TODO: why not BaseComponent???
-
-  private forcedStyles = {} as any
-
-  rootEl: HTMLDivElement
-  controller: ElementScrollController
-
-
-  render(props: ScrollerProps) {
-    let { forcedStyles } = this
-
-    return (
-      <div ref={this.setRootEl} class={'fc-scroller ' + (props.extraClassName || '')} style={{
-        height: forcedStyles.height,
-        overflowX: forcedStyles.overflowX || props.overflowX,
-        overflowY: forcedStyles.overflowY || props.overflowY
-      }}>
-        {props.children}
-      </div>
-    )
-  }
-
-
-  setRootEl = (rootEl: HTMLDivElement | null) => {
-    if (rootEl) {
-      this.rootEl = rootEl
-      this.controller = rootEl ? new ElementScrollController(rootEl) : null
-    }
-  }
-
-
-  private forceStyles(forcedStyles: any) {
-    __assign(this.forcedStyles, forcedStyles)
-    __assign(this.rootEl.style, forcedStyles)
-  }
-
-
-  clear() {
-    this.forceStyles({
-      height: 'auto',
-      overflowXOverride: '',
-      overflowYOverride: ''
-    })
-  }
-
-
-  // Overflow
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
-  // Useful for preserving scrollbar widths regardless of future resizes.
-  // Can pass in scrollbarWidths for optimization.
-  lockOverflow(scrollbarWidths: ScrollbarWidths) {
-    let { controller } = this
-    let { overflowX, overflowY } = this.props
-
-    scrollbarWidths = scrollbarWidths || this.getScrollbarWidths()
-
-    if (overflowX === 'auto') {
-      overflowX = (
-          scrollbarWidths.bottom || // horizontal scrollbars?
-          controller.canScrollHorizontally() // OR scrolling pane with massless scrollbars?
-        ) ? 'scroll' : 'hidden'
-    }
-
-    if (overflowY === 'auto') {
-      overflowY = (
-          scrollbarWidths.left || scrollbarWidths.right || // horizontal scrollbars?
-          controller.canScrollVertically() // OR scrolling pane with massless scrollbars?
-        ) ? 'scroll' : 'hidden'
-    }
-
-    this.forceStyles({ overflowX, overflowY })
-  }
-
-
-  setHeight(height: number | string) {
-    this.forceStyles({
-      height: typeof height === 'number' ? height + 'px' : height // TODO: util for this
-    })
-  }
-
-
-  getScrollbarWidths(): ScrollbarWidths {
-    let edges = computeEdges(this.rootEl)
-
-    return {
-      left: edges.scrollbarLeft,
-      right: edges.scrollbarRight,
-      bottom: edges.scrollbarBottom
-    }
-  }
-
-}

+ 2 - 0
packages/core/src/main.scss

@@ -1,4 +1,6 @@
 
+@import './scrollgrid/main';
+
 @import './common/common';
 @import './common/standard';
 @import './toolbar';

+ 11 - 4
packages/core/src/main.ts

@@ -23,10 +23,7 @@ export {
   flexibleCompare,
   computeVisibleDayRange,
   refineProps,
-  matchCellWidths, uncompensateScroll, compensateScroll, subtractInnerElHeight,
   isMultiDayRange,
-  distributeHeight,
-  undistributeHeight,
   preventSelection, allowSelection, preventContextMenu, allowContextMenu,
   compareNumbers, enableCursor, disableCursor,
   diffDates,
@@ -97,7 +94,6 @@ export { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin
 export { DateRange, rangeContainsMarker, intersectRanges, rangesEqual, rangesIntersect, rangeContainsRange } from './datelib/date-range'
 export { default as Mixin } from './common/Mixin'
 export { default as PositionCache } from './common/PositionCache'
-export { default as Scroller, ScrollerProps, ScrollbarWidths } from './common/Scroller'
 export { ScrollController, ElementScrollController, WindowScrollController } from './common/scroll-controller'
 export { default as Theme } from './theme/Theme'
 export { default as ComponentContext, ComponentContextType } from './component/ComponentContext'
@@ -175,3 +171,14 @@ export { default as requestJson } from './util/requestJson'
 export * from './vdom'
 export { subrenderer, SubRenderer, BaseComponent, setRef, renderVNodes, buildMapSubRenderer } from './vdom-util'
 export { DelayedRunner } from './util/runner'
+
+export { default as SimpleScrollGrid, SimpleScrollGridSection } from './scrollgrid/SimpleScrollGrid'
+export {
+  CssDimValue, ScrollerLike, SectionConfig, ColCss, ChunkConfig, doSizingHacks, hasShrinkWidth, renderMicroColGroup,
+  getScrollGridClassNames, getSectionClassNames, getChunkVGrow, getNeedsYScrolling, renderChunkContent, getForceScrollbars, getShrinkWidth,
+  getChunkClassNames, ChunkContentCallbackArgs
+} from './scrollgrid/util'
+export { default as Scroller, ScrollerProps, OverflowValue } from './scrollgrid/Scroller'
+export { getScrollbarWidths } from './util/scrollbar-width'
+export { default as RefMap } from './util/RefMap'
+export { getIsRtlScrollbarOnLeft } from './util/scrollbar-side'

+ 99 - 0
packages/core/src/scrollgrid/Scroller.tsx

@@ -0,0 +1,99 @@
+import { h, ComponentChildren, Ref } from '../vdom'
+import { BaseComponent, setRef } from '../vdom-util'
+import { CssDimValue, ScrollerLike } from './util'
+
+
+export type OverflowValue = 'auto' | 'hidden' | 'scroll'
+
+export interface ScrollerProps {
+  overflowX: OverflowValue
+  overflowY: OverflowValue
+  vGrow?: boolean
+  maxHeight?: CssDimValue
+  style?: object // complex object, bad for purecomponent, but who cares because has children anyway
+  className?: string
+  children?: ComponentChildren
+  elRef?: Ref<HTMLElement>
+}
+
+
+export default class Scroller extends BaseComponent<ScrollerProps> implements ScrollerLike {
+
+  private el: HTMLElement
+
+
+  render(props: ScrollerProps) {
+    let className = [ 'scroller' ]
+
+    if (props.className) {
+      className = className.concat(props.className)
+    }
+
+    if (props.vGrow) {
+      className.push('vgrow')
+    }
+
+    return (
+      <div ref={this.handleEl} class={className.join(' ')} style={{
+        ...props.style || {},
+        maxHeight: props.maxHeight || '',
+        overflowX: props.overflowX,
+        overflowY: props.overflowY
+      }}>
+        {props.children}
+      </div>
+    )
+  }
+
+
+  handleEl = (el: HTMLElement) => {
+    this.el = el
+
+    setRef(this.props.elRef, el)
+  }
+
+
+  // getSnapshotBeforeUpdate() {
+  //   return {
+  //     scrollLeft: this.el.scrollLeft,
+  //     scrollTop: this.el.scrollTop
+  //   }
+  // }
+
+
+  // componentDidUpdate(prevProps, prevState, snapshot) {
+  //   this.el.scrollLeft = snapshot.scrollLeft
+  //   this.el.scrollTop = snapshot.scrollTop
+  // }
+
+
+  needsXScrolling() {
+    return this.el.scrollWidth > this.el.clientWidth + 1 || // IE shittiness
+      this.props.overflowX === 'auto' && Boolean(this.getXScrollbarWidth()) // hack safeguard
+  }
+
+
+  needsYScrolling() {
+    return this.el.scrollHeight > this.el.clientHeight + 1 || // IE shittiness
+      this.props.overflowY === 'auto' && Boolean(this.getYScrollbarWidth()) // hack safeguard
+  }
+
+
+  getXScrollbarWidth() {
+    if (this.props.overflowX === 'hidden') {
+      return 0
+    } else {
+      return this.el.offsetHeight - this.el.clientHeight // only works because we guarantee no borders
+    }
+  }
+
+
+  getYScrollbarWidth() {
+    if (this.props.overflowY === 'hidden') {
+      return 0
+    } else {
+      return this.el.offsetWidth - this.el.clientWidth // only works because we guarantee no borders
+    }
+  }
+
+}

+ 116 - 0
packages/core/src/scrollgrid/SimpleScrollGrid.tsx

@@ -0,0 +1,116 @@
+import { VNode, h } from '../vdom'
+import ComponentContext from '../component/ComponentContext'
+import { BaseComponent, setRef } from '../vdom-util'
+import Scroller, { OverflowValue } from './Scroller'
+import RefMap from '../util/RefMap'
+import { ColCss, SectionConfig, renderMicroColGroup, getShrinkWidth, getScrollGridClassNames, getSectionClassNames, getNeedsYScrolling,
+  renderChunkContent, getChunkVGrow, doSizingHacks, getForceScrollbars, ChunkConfig, hasShrinkWidth, CssDimValue, getChunkClassNames } from './util'
+
+
+export interface SimpleScrollGridProps {
+  cols: ColCss[]
+  sections: SimpleScrollGridSection[]
+  vGrow?: boolean
+  height?: CssDimValue // TODO: give to real ScrollGrid
+}
+
+export interface SimpleScrollGridSection extends SectionConfig {
+  chunk?: ChunkConfig
+}
+
+interface SimpleScrollGridState {
+  shrinkWidth?: number
+  forceYScrollbars: boolean
+}
+
+
+export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProps> {
+
+  scrollerRefs = new RefMap<Scroller>()
+  scrollerElRefs = new RefMap<HTMLElement, [ChunkConfig]>(this._handleScrollerElRef.bind(this))
+
+  state = {
+    forceYScrollbars: false
+  }
+
+
+  render(props: SimpleScrollGridProps, state: SimpleScrollGridState, context: ComponentContext) {
+    let sectionConfigs = props.sections || []
+    let microColGroupNode = renderMicroColGroup(props.cols, state.shrinkWidth)
+
+    return (
+      <table class={getScrollGridClassNames(props.vGrow, context).join(' ')} style={{ height: props.height }}>
+        {sectionConfigs.map((sectionConfig, sectionI) => this.renderSection(sectionConfig, sectionI, microColGroupNode))}
+      </table>
+    )
+  }
+
+
+  renderSection(sectionConfig: SimpleScrollGridSection, sectionI: number, microColGroupNode: VNode) {
+    return (
+      <tr class={getSectionClassNames(sectionConfig, this.props.vGrow).join(' ')}>
+        {this.renderChunkTd(sectionConfig, sectionI, microColGroupNode, sectionConfig.chunk)}
+      </tr>
+    )
+  }
+
+
+  renderChunkTd(sectionConfig: SimpleScrollGridSection, sectionI: number, microColGroupNode: VNode, chunkConfig: ChunkConfig) {
+
+    if (chunkConfig.outerContent) {
+      return chunkConfig.outerContent
+    }
+
+    let needsYScrolling = getNeedsYScrolling(this.props, sectionConfig, chunkConfig) // TODO: do lazily
+    let overflowY: OverflowValue = this.state.forceYScrollbars ? 'scroll' : (needsYScrolling ? 'auto' : 'hidden')
+    let content = renderChunkContent(sectionConfig, chunkConfig, microColGroupNode, '')
+
+    return (
+      <td class={getChunkClassNames(sectionConfig, this.context)} ref={chunkConfig.elRef}>
+        <Scroller
+          ref={this.scrollerRefs.createRef(sectionI)}
+          elRef={this.scrollerElRefs.createRef(sectionI, chunkConfig)}
+          className={chunkConfig.scrollerClassName}
+          overflowY={overflowY}
+          overflowX='hidden'
+          maxHeight={sectionConfig.maxHeight}
+          vGrow={getChunkVGrow(this.props, sectionConfig, chunkConfig)}
+        >{content}</Scroller>
+      </td>
+    )
+  }
+
+
+  componentDidMount() {
+    this.componentDidRender()
+  }
+
+
+  componentDidUpdate() {
+    this.componentDidRender()
+  }
+
+
+  componentDidRender() {
+    doSizingHacks(this.base as HTMLElement) // TODO: not every time
+
+    if (hasShrinkWidth(this.props.cols)) {
+      this.setState({
+        shrinkWidth: getShrinkWidth(this.scrollerElRefs.getCurrents())
+      })
+    }
+
+    this.setState({ // TODO: only for resizing
+      forceYScrollbars: getForceScrollbars(this.scrollerRefs.getCurrents(), 'Y')
+    })
+  }
+
+
+  _handleScrollerElRef(scrollerEl: HTMLElement | null, key: string, chunkConfig: ChunkConfig) {
+    setRef(chunkConfig.scrollerElRef, scrollerEl)
+  }
+
+
+  // TODO: can do a really simple print-view. dont need to join rows
+
+}

+ 87 - 0
packages/core/src/scrollgrid/main.scss

@@ -0,0 +1,87 @@
+
+.scrollgrid {
+  width: 100%;
+}
+
+.scrollgrid--rtl { // temporary
+  direction: rtl;
+}
+
+.scrollgrid,
+.scrollgrid table {
+  table-layout: fixed;
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+.scrollgrid--forprint {
+  table-layout: auto;
+}
+
+.scrollgrid--vgrow {
+  height: 100%;
+}
+
+.scrollgrid th,
+.scrollgrid td {
+  border: 1px solid #ddd;
+  padding: 0;
+  vertical-align: top;
+}
+
+.scrollgrid table {
+  border-style: hidden;
+}
+
+.cell-content {
+  padding: 10px;
+}
+.shrink {
+  float: left;
+  white-space: nowrap;
+}
+.scrollgrid--rtl .shrink {
+  float: right;
+}
+
+.scrollgrid__section { // a <tr>
+  height: 1px;
+}
+
+.scrollgrid__section--vgrow {
+  height: auto;
+
+  > td {
+    height: 100%;
+  }
+}
+
+
+.clippedscroller {
+  overflow: hidden;
+  position: relative; // will be override by .vgrow--absolute
+}
+
+
+// TODO: use .table--vgrow, .clippedscroller--vgrow instead?
+
+.hgrow {
+  width: 100%;
+}
+
+.vgrow {
+  height: 100%;
+}
+
+.vgrow--absolute {
+  height: auto;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.scrollgrid__section:not(.scrollgrid__section--vgrow) table.vgrow > * > tr > * { // important for IE!
+  height: 100%;
+}

+ 211 - 0
packages/core/src/scrollgrid/util.tsx

@@ -0,0 +1,211 @@
+import { VNode, h, Ref } from '../vdom'
+import { findElements } from '../util/dom-manip'
+import { computeInnerRect } from '../util/dom-geom'
+import ComponentContext from '../component/ComponentContext'
+
+
+export type CssDimValue = string | number
+
+
+export interface ColCss {
+  width?: CssDimValue
+  minWidth?: CssDimValue
+  [otherProp: string]: any
+}
+
+export interface SectionConfig {
+  outerContent?: VNode
+  type?: 'body' | 'head' | 'foot'
+  className?: string
+  maxHeight?: number
+  vGrow?: boolean
+  vGrowRows?: boolean // TODO: how to get a bottom rule?
+}
+
+export interface ChunkConfig {
+  outerContent?: VNode
+  content?: (contentProps: ChunkContentCallbackArgs) => VNode
+  rowContent?: VNode
+  vGrowRows?: boolean
+  needsSizing?: boolean
+  scrollerClassName?: string // give this to NormalScrollGrid too ... make a classname for the <td> too
+  scrollerElRef?: Ref<HTMLDivElement>
+  elRef?: Ref<HTMLTableCellElement>
+}
+
+export interface ChunkContentCallbackArgs {
+  colGroupNode: VNode
+  rowsGrow: boolean
+  type: string
+  isSizingReady: boolean
+  minWidth: CssDimValue
+}
+
+
+export function getShrinkWidth(chunkEls: HTMLElement[]) { // all in same COL!
+  let shrinkEls = findElements(chunkEls, '.shrink')
+  let largestWidth = 0
+
+  for (let shrinkEl of shrinkEls) {
+    let cellWidth = shrinkEl.getBoundingClientRect().width + 1 // HACK for simulating a border
+
+    largestWidth = Math.max(largestWidth, cellWidth)
+  }
+
+  return largestWidth
+}
+
+
+export function doSizingHacks(rootEl: HTMLElement) { // TODO: needs to run on window resize fetch!!
+
+  // // for Safari
+  // // TODO: in Scroller class?
+  // let hGrowTables = findElements(rootEl, '.scroller > table.hgrow')
+  // for (let tableEl of hGrowTables) {
+  //   if (tableEl.style.position == 'relative') {
+  //     tableEl.style.position = ''
+  //   } else {
+  //     tableEl.style.position = 'relative'
+  //   }
+  // }
+
+  // for Firefox for all cells
+  // for Safari(?) for cells with rowspans
+  let vGrowEls = findElements(rootEl, 'td > .vgrow')
+  for (let vGrowEl of vGrowEls) {
+    let cellEl = vGrowEl.parentNode as HTMLElement
+    let cellInnerRect = computeInnerRect(cellEl, true) // TODO: cache!
+    let cellInnerHeight = cellInnerRect.bottom - cellInnerRect.top
+    let vGrowHeight = vGrowEl.getBoundingClientRect().height
+    let lacking = cellInnerHeight - vGrowHeight
+
+    if (lacking > 0.5) {
+      let cellEl = vGrowEl.parentNode as HTMLElement
+      cellEl.style.position = 'relative'
+      vGrowEl.classList.add('vgrow--absolute')
+    }
+  }
+}
+
+
+export interface ScrollerLike { // have scrollers implement?
+  needsYScrolling(): boolean
+  needsXScrolling(): boolean
+}
+
+
+export function getForceScrollbars(scrollers: ScrollerLike[], axis: 'X' | 'Y') {
+  let methodName = 'needs' + axis + 'Scrolling'
+  let needsScrollbars = false
+
+  for (let scroller of scrollers) {
+    if (scroller[methodName]()) {
+      needsScrollbars = true
+      break
+    }
+  }
+
+  return needsScrollbars
+}
+
+
+export function getChunkVGrow(props: { vGrow?: boolean }, sectionConfig: SectionConfig, chunkConfig: ChunkConfig) {
+  return (props.vGrow && sectionConfig.vGrow) || chunkConfig.vGrowRows
+}
+
+
+export function getNeedsYScrolling(props: { vGrow?: boolean }, sectionConfig: SectionConfig, chunkConfig: ChunkConfig) {
+  return (sectionConfig.maxHeight != null || (props.vGrow && sectionConfig.vGrow)) && !chunkConfig.vGrowRows
+}
+
+
+export function renderChunkContent(sectionConfig: SectionConfig, chunkConfig: ChunkConfig, microColGroupNode: VNode, chunkTableMinWidth: CssDimValue) {
+  let vGrowRows = sectionConfig.vGrowRows || chunkConfig.vGrowRows
+
+  let content: VNode = typeof chunkConfig.content === 'function' ?
+    chunkConfig.content({
+      colGroupNode: microColGroupNode,
+      rowsGrow: vGrowRows,
+      type: sectionConfig.type,
+      isSizingReady: true, // TODO!!!!!!
+      minWidth: chunkTableMinWidth
+    }) :
+    h('table', {
+      class: [ 'hgrow', (vGrowRows ? 'vgrow' : '') ].join(' '),
+      style: {
+        minWidth: chunkTableMinWidth // because colMinWidths arent enough
+      }
+    }, [
+      microColGroupNode,
+      h('tbody', {}, chunkConfig.rowContent)
+    ])
+
+  return content
+}
+
+
+export function renderMicroColGroup(cols: ColCss[], shrinkWidth?: number) { // TODO: make this SuperColumn-only!???
+  return (
+    <colgroup>
+      {cols.map((colCss, i) => {
+        let className = '' // HACK
+
+        if (colCss.width === 'shrink') {
+          colCss = { ...colCss, width: shrinkWidth || 0 }
+        }
+
+        if (colCss.className !== undefined) { // gahhhhh
+          className = colCss.className
+          colCss = { ...colCss }
+          delete colCss.className
+        }
+
+        return (<col className={className} style={colCss}></col>)
+      })}
+    </colgroup>
+  )
+}
+
+
+export function hasShrinkWidth(cols: ColCss[]) {
+  for (let col of cols) {
+    if (col.width === 'shrink') {
+      return true
+    }
+  }
+
+  return false
+}
+
+
+export function getScrollGridClassNames(vGrow: boolean, context: ComponentContext) {
+  let classNames = [
+    'scrollgrid',
+    (context.isRtl ? 'scrollgrid--rtl' : 'scrollgrid--ltr'), // TODO: kill this
+    context.theme.getClass('tableGrid')
+  ]
+
+  if (vGrow) {
+    classNames.push('scrollgrid--vgrow')
+  }
+
+  return classNames
+}
+
+
+export function getSectionClassNames(sectionConfig: SectionConfig, wholeTableVGrow: boolean) {
+  let classNames = [ 'scrollgrid__section', 'scrollgrid__' + sectionConfig.type, sectionConfig.className ]
+
+  if (wholeTableVGrow && sectionConfig.vGrow && sectionConfig.maxHeight == null) {
+    classNames.push('scrollgrid__section--vgrow')
+  }
+
+  return classNames
+}
+
+
+export function getChunkClassNames(sectionConfig: SectionConfig, context: ComponentContext) {
+  return context.theme.getClass(
+    sectionConfig.type === 'body' ? 'widgetContent' : 'widgetHeader'
+  )
+}

+ 0 - 4
packages/core/src/theme/StandardTheme.ts

@@ -15,10 +15,6 @@ StandardTheme.prototype.classes = {
   popoverHeader: 'fc-widget-header',
   popoverContent: 'fc-widget-content',
 
-  // day grid
-  headerRow: 'fc-widget-header',
-  dayRow: 'fc-widget-content',
-
   // list view
   listView: 'fc-widget-content'
 }

+ 42 - 0
packages/core/src/util/RefMap.ts

@@ -0,0 +1,42 @@
+
+export default class RefMap<RefType, OtherArgs extends any[] = []> {
+
+  public currentMap: { [key: string]: RefType } = {}
+  public otherArgsMap: { [key: string]: OtherArgs } = {}
+  private callbackMap: { [key: string]: (val: RefType | null) => void } = {}
+
+
+  constructor(public masterCallback?: (val: RefType | null, key: string, ...otherArgs: OtherArgs) => void) {
+  }
+
+
+  createRef(key: string | number, ...otherArgs: OtherArgs) {
+    let refCallback = this.callbackMap[key]
+
+    if (!refCallback) {
+      refCallback = this.callbackMap[key] = (val: RefType | null) => {
+        if (val !== null) {
+          this.currentMap[key] = val
+        } else {
+          delete this.currentMap[key]
+          delete this.callbackMap[key]
+          delete this.otherArgsMap[key]
+        }
+
+        if (this.masterCallback) {
+          this.masterCallback(val, String(key), ...otherArgs)
+        }
+      }
+    }
+
+    this.otherArgsMap[key] = otherArgs
+
+    return refCallback
+  }
+
+
+  getCurrents() {
+    return Object.values(this.currentMap)
+  }
+
+}

+ 7 - 8
packages/core/src/util/dom-geom.ts

@@ -1,5 +1,6 @@
 import { Rect } from './geom'
-import { sanitizeScrollbarWidth, getIsRtlScrollbarOnLeft } from './scrollbars'
+import { getIsRtlScrollbarOnLeft } from './scrollbar-side'
+import { getScrollbarWidths } from './scrollbar-width'
 
 export interface EdgeInfo {
   borderLeft: number
@@ -16,16 +17,14 @@ export interface EdgeInfo {
 }
 
 
-export function computeEdges(el, getPadding = false): EdgeInfo {
+export function computeEdges(el: HTMLElement, getPadding = false): EdgeInfo { // cache somehow?
   let computedStyle = window.getComputedStyle(el)
   let borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0
   let borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0
   let borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0
   let borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0
-
-  // must use offset(Width|Height) because compatible with client(Width|Height)
-  let scrollbarLeftRight = sanitizeScrollbarWidth(el.offsetWidth - el.clientWidth - borderLeft - borderRight)
-  let scrollbarBottom = sanitizeScrollbarWidth(el.offsetHeight - el.clientHeight - borderTop - borderBottom)
+  let scrollbarLeftRight = el.clientHeight < el.scrollHeight ? getScrollbarWidths().y : 0
+  let scrollbarBottom = el.clientWidth < el.scrollWidth ? getScrollbarWidths().x : 0
 
   let res: EdgeInfo = {
     borderLeft,
@@ -54,8 +53,8 @@ export function computeEdges(el, getPadding = false): EdgeInfo {
 }
 
 
-export function computeInnerRect(el, goWithinPadding = false) {
-  let outerRect = computeRect(el)
+export function computeInnerRect(el, goWithinPadding = false, doFromWindowViewport?: boolean) {
+  let outerRect = doFromWindowViewport ? el.getBoundingClientRect() : computeRect(el)
   let edges = computeEdges(el, goWithinPadding)
   let res = {
     left: outerRect.left + edges.borderLeft + edges.scrollbarLeft,

+ 1 - 1
packages/core/src/util/dom-manip.ts

@@ -7,7 +7,7 @@ const containerTagHash = {
   '<td': 'tr'
 }
 
-export function htmlToElement(html: string): HTMLElement {
+export function htmlToElement(html: string): HTMLElement { // TODO: use renderVNodes instead?
   html = html.trim()
   let container = document.createElement(computeContainerTag(html))
   container.innerHTML = html

+ 0 - 146
packages/core/src/util/misc.ts

@@ -1,5 +1,3 @@
-import { applyStyle } from './dom-manip'
-import { computeVMargins } from './dom-geom'
 import { preventDefault } from './dom-event'
 import { DateMarker, startOfDay, addDays, diffDays, diffDayAndTime } from '../datelib/marker'
 import { Duration, asRoughMs, createDuration } from '../datelib/duration'
@@ -18,35 +16,6 @@ export function guid() {
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
-// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
-// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
-export function compensateScroll(rowEl: HTMLElement, scrollbarWidths) {
-  if (scrollbarWidths.left) {
-    applyStyle(rowEl, {
-      borderLeftWidth: 1,
-      marginLeft: scrollbarWidths.left - 1
-    })
-  }
-  if (scrollbarWidths.right) {
-    applyStyle(rowEl, {
-      borderRightWidth: 1,
-      marginRight: scrollbarWidths.right - 1
-    })
-  }
-}
-
-
-// Undoes compensateScroll and restores all borders/margins
-export function uncompensateScroll(rowEl: HTMLElement) {
-  applyStyle(rowEl, {
-    marginLeft: '',
-    marginRight: '',
-    borderLeftWidth: '',
-    borderRightWidth: ''
-  })
-}
-
-
 // Make the mouse cursor express that an event is not allowed in the current area
 export function disableCursor() {
   document.body.classList.add('fc-not-allowed')
@@ -59,121 +28,6 @@ export function enableCursor() {
 }
 
 
-// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
-// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
-// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
-// reduces the available height.
-export function distributeHeight(els: HTMLElement[], availableHeight, shouldRedistribute) {
-
-  // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
-  // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
-
-  let minOffset1 = Math.floor(availableHeight / els.length) // for non-last element
-  let minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)) // for last element *FLOORING NOTE*
-  let flexEls = [] // elements that are allowed to expand. array of DOM nodes
-  let flexOffsets = [] // amount of vertical space it takes up
-  let flexHeights = [] // actual css height
-  let usedHeight = 0
-
-  undistributeHeight(els) // give all elements their natural height
-
-  // find elements that are below the recommended height (expandable).
-  // important to query for heights in a single first pass (to avoid reflow oscillation).
-  els.forEach(function(el, i) {
-    let minOffset = i === els.length - 1 ? minOffset2 : minOffset1
-    let naturalHeight = el.getBoundingClientRect().height
-    let naturalOffset = naturalHeight + computeVMargins(el)
-
-    if (naturalOffset < minOffset) {
-      flexEls.push(el)
-      flexOffsets.push(naturalOffset)
-      flexHeights.push(naturalHeight)
-    } else {
-      // this element stretches past recommended height (non-expandable). mark the space as occupied.
-      usedHeight += naturalOffset
-    }
-  })
-
-  // readjust the recommended height to only consider the height available to non-maxed-out rows.
-  if (shouldRedistribute) {
-    availableHeight -= usedHeight
-    minOffset1 = Math.floor(availableHeight / flexEls.length)
-    minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)) // *FLOORING NOTE*
-  }
-
-  // assign heights to all expandable elements
-  flexEls.forEach(function(el, i) {
-    let minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1
-    let naturalOffset = flexOffsets[i]
-    let naturalHeight = flexHeights[i]
-    let newHeight = minOffset - (naturalOffset - naturalHeight) // subtract the margin/padding
-
-    if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
-      el.style.height = newHeight + 'px'
-    }
-  })
-}
-
-
-// Undoes distrubuteHeight, restoring all els to their natural height
-export function undistributeHeight(els: HTMLElement[]) {
-  els.forEach(function(el) {
-    el.style.height = ''
-  })
-}
-
-
-// Given `els`, a set of <td> cells, find the cell with the largest natural width and set the widths of all the
-// cells to be that width.
-// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
-export function matchCellWidths(els: HTMLElement[]) {
-  let maxInnerWidth = 0
-
-  els.forEach(function(el) {
-    let innerEl = el.firstChild // hopefully an element
-    if (innerEl instanceof HTMLElement) {
-      let innerWidth = innerEl.getBoundingClientRect().width
-      if (innerWidth > maxInnerWidth) {
-        maxInnerWidth = innerWidth
-      }
-    }
-  })
-
-  maxInnerWidth++ // sometimes not accurate of width the text needs to stay on one line. insurance
-
-  els.forEach(function(el) {
-    el.style.width = maxInnerWidth + 'px'
-  })
-
-  return maxInnerWidth
-}
-
-
-// Given one element that resides inside another,
-// Subtracts the height of the inner element from the outer element.
-export function subtractInnerElHeight(outerEl: HTMLElement, innerEl: HTMLElement) {
-
-  // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
-  let reflowStyleProps = {
-    position: 'relative', // cause a reflow, which will force fresh dimension recalculation
-    left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
-  }
-  applyStyle(outerEl, reflowStyleProps)
-  applyStyle(innerEl, reflowStyleProps)
-
-  let diff = // grab the dimensions
-    outerEl.getBoundingClientRect().height -
-    innerEl.getBoundingClientRect().height
-
-  // undo hack
-  let resetStyleProps = { position: '', left: '' }
-  applyStyle(outerEl, resetStyleProps)
-  applyStyle(innerEl, resetStyleProps)
-
-  return diff
-}
-
-
 /* Selection
 ----------------------------------------------------------------------------------------------------------------------*/
 

+ 8 - 15
packages/core/src/util/scrollbars.ts → packages/core/src/util/scrollbar-side.ts

@@ -1,18 +1,20 @@
-import { removeElement, applyStyle } from './dom-manip'
+import { removeElement, applyStyle } from '@fullcalendar/core'
 
 
-// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
+let _isRtlScrollbarOnLeft: boolean | null = null
 
-let isRtlScrollbarOnLeft: boolean | null = null
 
 export function getIsRtlScrollbarOnLeft() { // responsible for caching the computation
-  if (isRtlScrollbarOnLeft === null) {
-    isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft()
+  if (_isRtlScrollbarOnLeft === null) {
+    _isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft()
   }
-  return isRtlScrollbarOnLeft
+  return _isRtlScrollbarOnLeft
 }
 
+
 function computeIsRtlScrollbarOnLeft() { // creates an offscreen test element, then removes it
+
+  // TODO: use htmlToElement
   let outerEl = document.createElement('div')
   applyStyle(outerEl, {
     position: 'absolute',
@@ -32,12 +34,3 @@ function computeIsRtlScrollbarOnLeft() { // creates an offscreen test element, t
   removeElement(outerEl)
   return res
 }
-
-
-// The scrollbar width computations in computeEdges are sometimes flawed when it comes to
-// retina displays, rounding, and IE11. Massage them into a usable value.
-export function sanitizeScrollbarWidth(width: number) {
-  width = Math.max(0, width) // no negatives
-  width = Math.round(width)
-  return width
-}

+ 29 - 0
packages/core/src/util/scrollbar-width.ts

@@ -0,0 +1,29 @@
+
+export interface ScrollbarWidths {
+  x: number
+  y: number
+}
+
+let _scrollbarWidths: ScrollbarWidths | undefined
+
+
+export function getScrollbarWidths() { // TODO: way to force recompute?
+  if (!_scrollbarWidths) {
+    _scrollbarWidths = computeScrollbarWidths()
+  }
+
+  return _scrollbarWidths
+}
+
+
+function computeScrollbarWidths(): ScrollbarWidths {
+  let el = document.createElement('div')
+  el.style.overflow = 'scroll'
+  document.body.appendChild(el)
+  let res = {
+    x: el.offsetHeight - el.clientHeight,
+    y: el.offsetWidth - el.clientWidth
+  }
+  document.body.removeChild(el)
+  return res
+}

+ 3 - 2
packages/core/src/vdom-util.tsx

@@ -13,6 +13,7 @@ interface SubRendererOwner {
 }
 
 
+// TODO: make a HOC instead
 export abstract class BaseComponent<Props={}, State={}> extends Component<Props, State> implements SubRendererOwner {
 
   static addPropsEquality = addPropsEquality
@@ -140,7 +141,7 @@ function buildClassSubRenderer(subRendererClass: SubRendererClass<any>) {
     }
   }
 
-  return function(this: SubRendererOwner, props: any) {
+  return function(this: SubRendererOwner, props: any) { // what about passing in Context?
     let context = this.context
 
     if (!props) {
@@ -181,7 +182,7 @@ function buildFuncSubRenderer(renderFunc, unrenderFunc) {
     }
   }
 
-  return function(this: SubRendererOwner, props: any) {
+  return function(this: SubRendererOwner, props: any) { // what about passing in Context?
     thisContext = this
     let context = thisContext.context
 

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

@@ -28,6 +28,7 @@ export interface DayTableProps {
   eventDrag: EventInteractionState | null
   eventResize: EventInteractionState | null
   isRigid: boolean
+  colGroupNode: VNode
   renderNumberIntro: (row: number, cells: any) => VNode[]
   renderBgIntro: () => VNode[]
   renderIntro: () => VNode[]
@@ -54,6 +55,7 @@ export default class DayTable extends DateComponent<DayTableProps, ComponentCont
         dateProfile={dateProfile}
         cells={dayTableModel.cells}
         isRigid={props.isRigid}
+        colGroupNode={props.colGroupNode}
         renderNumberIntro={props.renderNumberIntro}
         renderBgIntro={props.renderBgIntro}
         renderIntro={props.renderIntro}
@@ -75,11 +77,6 @@ export default class DayTable extends DateComponent<DayTableProps, ComponentCont
   }
 
 
-  updateSize(isResize: boolean) {
-    this.table.updateSize(isResize)
-  }
-
-
   buildPositionCaches() {
     this.table.buildPositionCaches()
   }

+ 33 - 44
packages/daygrid/src/DayTableView.tsx

@@ -8,6 +8,7 @@ import {
   memoize,
   DaySeries,
   DayTableModel,
+  ChunkContentCallbackArgs,
 } from '@fullcalendar/core'
 import TableView, { hasRigidRows } from './TableView'
 import DayTable from './DayTable'
@@ -21,55 +22,43 @@ export default class DayTableView extends TableView {
 
 
   render(props: ViewProps, state: {}, context: ComponentContext) {
+    let { options } = context
     let { dateProfile } = props
     let dayTableModel = this.buildDayTableModel(dateProfile, props.dateProfileGenerator)
-    let { colWeekNumbersVisible, cellWeekNumbersVisible } = this.processOptions(context.options)
+    let { colWeekNumbersVisible, cellWeekNumbersVisible } = this.processOptions(options)
 
     return this.renderLayout(
-      <DayHeader
-        ref={this.headerRef}
-        dateProfile={dateProfile}
-        dates={dayTableModel.headerDates}
-        datesRepDistinctDays={dayTableModel.rowCnt === 1}
-        renderIntro={this.renderHeadIntro}
-      />,
-      <DayTable
-        ref={this.tableRef}
-        dateProfile={dateProfile}
-        dayTableModel={dayTableModel}
-        businessHours={props.businessHours}
-        dateSelection={props.dateSelection}
-        eventStore={props.eventStore}
-        eventUiBases={props.eventUiBases}
-        eventSelection={props.eventSelection}
-        eventDrag={props.eventDrag}
-        eventResize={props.eventResize}
-        isRigid={hasRigidRows(context.options)}
-        nextDayThreshold={context.nextDayThreshold}
-        renderNumberIntro={this.renderNumberIntro}
-        renderBgIntro={this.renderBgIntro}
-        renderIntro={this.renderIntro}
-        colWeekNumbersVisible={colWeekNumbersVisible}
-        cellWeekNumbersVisible={cellWeekNumbersVisible}
-      />
-    )
-  }
-
-
-  updateSize(isResize: boolean, viewHeight: number, isAuto: boolean) {
-    let header = this.headerRef.current
-    let table = this.tableRef.current
-
-    if (isResize || this.isLayoutSizeDirty()) {
-      this.updateLayoutHeight(
-        header ? header.rootEl : null,
-        table.table,
-        viewHeight,
-        isAuto
+      options.columnHeader &&
+        <DayHeader
+          ref={this.headerRef}
+          dateProfile={dateProfile}
+          dates={dayTableModel.headerDates}
+          datesRepDistinctDays={dayTableModel.rowCnt === 1}
+          renderIntro={this.renderHeadIntro}
+        />,
+      (contentArg: ChunkContentCallbackArgs) => (
+        <DayTable
+          ref={this.tableRef}
+          dateProfile={dateProfile}
+          dayTableModel={dayTableModel}
+          businessHours={props.businessHours}
+          dateSelection={props.dateSelection}
+          eventStore={props.eventStore}
+          eventUiBases={props.eventUiBases}
+          eventSelection={props.eventSelection}
+          eventDrag={props.eventDrag}
+          eventResize={props.eventResize}
+          isRigid={hasRigidRows(context.options)}
+          nextDayThreshold={context.nextDayThreshold}
+          colGroupNode={contentArg.colGroupNode}
+          renderNumberIntro={this.renderNumberIntro}
+          renderBgIntro={this.renderBgIntro}
+          renderIntro={this.renderIntro}
+          colWeekNumbersVisible={colWeekNumbersVisible}
+          cellWeekNumbersVisible={cellWeekNumbersVisible}
+        />
       )
-    }
-
-    table.updateSize(isResize)
+    )
   }
 
 }

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

@@ -13,7 +13,8 @@ import {
   ComponentContext,
   subrenderer,
   setRef,
-  createFormatter
+  createFormatter,
+  VNode
 } from '@fullcalendar/core'
 import TableEvents from './TableEvents'
 import TableMirrorEvents from './TableMirrorEvents'
@@ -35,6 +36,7 @@ export interface TableProps extends TableSkeletonProps {
   eventDrag: EventSegUiInteractionState | null
   eventResize: EventSegUiInteractionState | null
   rootElRef?: Ref<HTMLDivElement>
+  colGroupNode: VNode
 }
 
 export interface TableSeg extends Seg {
@@ -89,6 +91,7 @@ export default class Table extends BaseComponent<TableProps, TableState> {
           renderIntro={props.renderIntro}
           colWeekNumbersVisible={props.colWeekNumbersVisible}
           cellWeekNumbersVisible={props.cellWeekNumbersVisible}
+          colGroupNode={props.colGroupNode}
         />
         {this.renderPopover()}
       </Fragment>
@@ -157,11 +160,13 @@ export default class Table extends BaseComponent<TableProps, TableState> {
 
   componentDidMount() {
     this.subrender()
+    this.resize()
   }
 
 
   componentDidUpdate() {
     this.subrender()
+    this.resize()
   }
 
 
@@ -212,6 +217,7 @@ export default class Table extends BaseComponent<TableProps, TableState> {
     })
 
     let eventsRenderer = this.renderFgEvents({
+      colGroupNode: props.colGroupNode,
       renderIntro: props.renderIntro,
       segs: props.fgEventSegs,
       rowEls,
@@ -229,6 +235,7 @@ export default class Table extends BaseComponent<TableProps, TableState> {
 
     if (props.eventResize) {
       this.renderMirrorEvents({
+        colGroupNode: props.colGroupNode,
         renderIntro: props.renderIntro,
         segs: props.eventResize.segs,
         rowEls,
@@ -253,7 +260,7 @@ export default class Table extends BaseComponent<TableProps, TableState> {
   ------------------------------------------------------------------------------------------------------------------*/
 
 
-  updateSize(isResize: boolean) {
+  resize(isResize?: boolean) { // gaahhhhhhhhh
     let { calendar } = this.context
     let popover = this.popoverRef.current
 

+ 2 - 0
packages/daygrid/src/TableEvents.ts

@@ -19,6 +19,7 @@ import CellEvents from './CellEvents'
 export interface TableEventsProps extends BaseFgEventRendererProps {
   rowEls: HTMLElement[]
   colCnt: number
+  colGroupNode: VNode
   renderIntro: () => VNode[]
 }
 
@@ -43,6 +44,7 @@ export default class TableEvents extends CellEvents<TableEventsProps> {
       segs,
       rowEls: props.rowEls,
       colCnt: props.colCnt,
+      colGroupNode: props.colGroupNode,
       renderIntro: props.renderIntro,
       isDragging: props.isDragging,
       isResizing: props.isResizing,

+ 10 - 3
packages/daygrid/src/TableMirrorEvents.ts → packages/daygrid/src/TableMirrorEvents.tsx

@@ -1,5 +1,5 @@
 import {
-  htmlToElement, subrenderer, ComponentContext, removeElement
+  subrenderer, ComponentContext, removeElement, renderVNodes, h
 } from '@fullcalendar/core'
 import TableEvents, { renderSegRows, TableEventsProps } from './TableEvents'
 
@@ -12,13 +12,20 @@ export default class TableMirrorEvents extends TableEvents {
 
 
 // Renders the given foreground event segments onto the grid
-function attachSegs({ segs, rowEls, colCnt, renderIntro, interactingSeg }: TableEventsProps, context: ComponentContext) {
+function attachSegs({ segs, rowEls, colCnt, colGroupNode, renderIntro, interactingSeg }: TableEventsProps, context: ComponentContext) {
 
   let rowStructs = renderSegRows(segs, rowEls.length, colCnt, renderIntro, context)
 
   // inject each new event skeleton into each associated row
   rowEls.forEach(function(rowNode, row) {
-    let skeletonEl = htmlToElement('<div class="fc-mirror-skeleton"><table></table></div>') // will be absolutely positioned
+    let skeletonEl = renderVNodes(
+      <div class='fc-mirror-skeleton'>
+        <table>
+          {colGroupNode}
+        </table>
+      </div>,
+      context
+    )[0] as HTMLElement
     let skeletonTopEl: HTMLElement
     let skeletonTop
 

+ 4 - 1
packages/daygrid/src/TableSkeleton.tsx

@@ -25,6 +25,7 @@ export interface TableSkeletonProps {
   renderIntro: () => VNode[]
   colWeekNumbersVisible: boolean // week numbers render in own column? (caller does HTML via intro)
   cellWeekNumbersVisible: boolean // display week numbers in day cell?
+  colGroupNode: VNode
 }
 
 export interface CellModel {
@@ -62,7 +63,7 @@ export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
   renderDayRow(row) {
     let { theme } = this.context
     let { props } = this
-    let classes = [ 'fc-row', 'fc-week', theme.getClass('dayRow') ]
+    let classes = [ 'fc-row', 'fc-week' ]
 
     if (props.isRigid) {
       classes.push('fc-rigid')
@@ -72,6 +73,7 @@ export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
       <div class={classes.join(' ')}>
         <div class='fc-bg'>
           <table class={theme.getClass('tableGrid')}>
+            {props.colGroupNode}
             <DayBgRow
               cells={props.cells[row]}
               dateProfile={props.dateProfile}
@@ -81,6 +83,7 @@ export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
         </div>
         <div class="fc-content-skeleton">
           <table>
+            {props.colGroupNode}
             {this.getIsNumbersVisible() &&
               <thead>
                 {this.renderNumberTr(row)}

+ 48 - 153
packages/daygrid/src/TableView.tsx

@@ -1,20 +1,14 @@
 import {
-  VNode, h, createRef, ComponentChildren,
-  findElements,
-  matchCellWidths,
-  uncompensateScroll,
-  compensateScroll,
-  subtractInnerElHeight,
-  distributeHeight,
-  undistributeHeight,
+  VNode, h,
   createFormatter,
-  Scroller,
   View,
   memoize,
   getViewClassNames,
-  GotoAnchor
+  GotoAnchor,
+  SimpleScrollGrid,
+  SimpleScrollGridSection,
+  ChunkContentCallbackArgs
 } from '@fullcalendar/core'
-import Table from './Table'
 import TableDateProfileGenerator from './TableDateProfileGenerator'
 
 
@@ -30,40 +24,43 @@ const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
 export default abstract class TableView<State={}> extends View<State> {
 
   protected processOptions = memoize(this._processOptions)
-  private rootElRef = createRef<HTMLDivElement>()
-  private scrollerRef = createRef<Scroller>()
   private colWeekNumbersVisible: boolean // computed option
-  private weekNumberWidth: number
 
 
-  renderLayout(headerContent: ComponentChildren, bodyContent: ComponentChildren) {
-    let { theme, options } = this.context
+  renderLayout(headerRowContent: VNode | null, bodyContent: (contentArg: ChunkContentCallbackArgs) => VNode) {
     let classNames = getViewClassNames(this.props.viewSpec).concat('fc-dayGrid-view')
 
-    this.processOptions(options)
+    this.processOptions(this.context.options)
+
+    let sections: SimpleScrollGridSection[] = []
+
+    if (headerRowContent) {
+      sections.push({
+        type: 'head',
+        className: 'fc-head',
+        chunk: {
+          scrollerClassName: 'fc-head-container',
+          rowContent: headerRowContent
+        }
+      })
+    }
+
+    sections.push({
+      type: 'body',
+      className: 'fc-body',
+      chunk: {
+        scrollerClassName: 'fc-day-grid-container',
+        content: bodyContent
+      }
+    })
 
     return (
-      <div ref={this.rootElRef} class={classNames.join(' ')}>
-        <table class={theme.getClass('tableGrid')}>
-          {options.columnHeader &&
-            <thead class='fc-head'>
-              <tr>
-                <td class={'fc-head-container ' + theme.getClass('widgetHeader')}>
-                  {headerContent}
-                </td>
-              </tr>
-            </thead>
-          }
-          <tbody class='fc-body'>
-            <tr>
-              <td class={theme.getClass('widgetContent')}>
-                <Scroller ref={this.scrollerRef} overflowX='hidden' overflowY='auto' extraClassName='fc-day-grid-container'>
-                  {bodyContent}
-                </Scroller>
-              </td>
-            </tr>
-          </tbody>
-        </table>
+      <div class={classNames.join(' ')}>
+        <SimpleScrollGrid
+          vGrow={!this.props.isHeightAuto}
+          cols={[ { width: 'shrink' } ]}
+          sections={sections}
+        />
       </div>
     )
   }
@@ -92,119 +89,20 @@ export default abstract class TableView<State={}> extends View<State> {
   }
 
 
-  // Generates an HTML attribute string for setting the width of the week number column, if it is known
-  weekNumberStyles() {
-    if (this.weekNumberWidth != null) {
-      return { width: this.weekNumberWidth }
-    }
-    return {}
-  }
-
-
   /* Dimensions
   ------------------------------------------------------------------------------------------------------------------*/
 
+  // TODO: give eventLimit to DayTable
 
-  // Refreshes the horizontal dimensions of the view
-  updateLayoutHeight(headRowEl: HTMLElement | null, table: Table, viewHeight: number, isAuto: boolean) {
-    let rootEl = this.rootElRef.current
-    let scroller = this.scrollerRef.current
-    let { options } = this.context
-    let eventLimit = options.eventLimit
-    let scrollerHeight
-    let scrollbarWidths
-
-    // hack to give the view some height prior to dayGrid's columns being rendered
-    // TODO: separate setting height from scroller VS dayGrid.
-    if (!table.rowEls) {
-      if (!isAuto) {
-        scrollerHeight = this.computeScrollerHeight(viewHeight)
-        scroller.setHeight(scrollerHeight)
-      }
-      return
-    }
-
-    if (this.colWeekNumbersVisible) {
-      // Make sure all week number cells running down the side have the same width.
-      this.weekNumberWidth = matchCellWidths(
-        findElements(rootEl, '.fc-week-number')
-      )
-    }
-
-    // reset all heights to be natural
-    scroller.clear()
-    if (headRowEl) {
-      uncompensateScroll(headRowEl)
-    }
-
-    // is the event limit a constant level number?
-    if (eventLimit && typeof eventLimit === 'number') {
-      table.limitRows(eventLimit) // limit the levels first so the height can redistribute after
-    }
-
-    // distribute the height to the rows
-    // (viewHeight is a "recommended" value if isAuto)
-    scrollerHeight = this.computeScrollerHeight(viewHeight)
-    this.setGridHeight(table, scrollerHeight, isAuto, options)
-
-    // is the event limit dynamically calculated?
-    if (eventLimit && typeof eventLimit !== 'number') {
-      table.limitRows(eventLimit) // limit the levels after the grid's row heights have been set
-    }
-
-    if (!isAuto) { // should we force dimensions of the scroll container?
-
-      scroller.setHeight(scrollerHeight)
-      scrollbarWidths = scroller.getScrollbarWidths()
+  // // is the event limit a constant level number?
+  // if (eventLimit && typeof eventLimit === 'number') {
+  //   table.limitRows(eventLimit) // limit the levels first so the height can redistribute after
+  // }
 
-      if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
-
-        if (headRowEl) {
-          compensateScroll(headRowEl, scrollbarWidths)
-        }
-
-        // doing the scrollbar compensation might have created text overflow which created more height. redo
-        scrollerHeight = this.computeScrollerHeight(viewHeight)
-        scroller.setHeight(scrollerHeight)
-      }
-
-      // guarantees the same scrollbar widths
-      scroller.lockOverflow(scrollbarWidths)
-    }
-  }
-
-
-  // given a desired total height of the view, returns what the height of the scroller should be
-  computeScrollerHeight(viewHeight) {
-    let rootEl = this.rootElRef.current
-    let scroller = this.scrollerRef.current
-
-    return viewHeight - subtractInnerElHeight(rootEl, scroller.rootEl) // everything that's NOT the scroller
-  }
-
-
-  // Sets the height of just the Table component in this view
-  setGridHeight(table: Table, height, isAuto, options) {
-    let { rowEls } = table
-
-    if (options.monthMode) {
-
-      // if auto, make the height of each row the height that it would be if there were 6 weeks
-      if (isAuto) {
-        height *= rowEls.length / 6
-      }
-
-      distributeHeight(rowEls, height, !isAuto) // if auto, don't compensate for height-hogging rows
-
-    } else {
-
-      if (isAuto) {
-        undistributeHeight(rowEls) // let the rows be their natural height with no expanding
-      } else {
-        distributeHeight(rowEls, height, true) // true = compensate for height-hogging rows
-      }
-    }
-  }
+  // // is the event limit dynamically calculated?
+  // if (eventLimit && typeof eventLimit !== 'number') {
+  //   table.limitRows(eventLimit) // limit the levels after the grid's row heights have been set
+  // }
 
 
   /* Header Rendering
@@ -216,9 +114,8 @@ export default abstract class TableView<State={}> extends View<State> {
     let { theme, options } = this.context
 
     if (this.colWeekNumbersVisible) {
-      // inner span needed for matchCellWidths
       return [
-        <th class={'fc-week-number ' + theme.getClass('widgetHeader')} style={this.weekNumberStyles()}>
+        <th class={'fc-week-number fc-shrink ' + theme.getClass('widgetHeader')}>
           <span>
             {options.weekLabel}
           </span>
@@ -241,10 +138,8 @@ export default abstract class TableView<State={}> extends View<State> {
     let colCnt = cells[0].length
 
     if (this.colWeekNumbersVisible) {
-
-      // aside from link, the GotoAnchor is important for matchCellWidths
       return [
-        <td class='fc-week-number' style={this.weekNumberStyles()}>
+        <td class='fc-week-number fc-shrink'>
           <GotoAnchor
             navLinks={options.navLinks}
             gotoOptions={{ date: weekStart, type: 'week', forceOff: colCnt === 1 }}
@@ -263,7 +158,7 @@ export default abstract class TableView<State={}> extends View<State> {
 
     if (this.colWeekNumbersVisible) {
       return [
-        <td class={'fc-week-number ' + theme.getClass('widgetContent')} style={this.weekNumberStyles()}></td>
+        <td class={'fc-week-number fc-shrink ' + theme.getClass('widgetContent')}></td>
       ]
     }
 
@@ -277,7 +172,7 @@ export default abstract class TableView<State={}> extends View<State> {
 
     if (this.colWeekNumbersVisible) {
       return [
-        <td class='fc-week-number' style={this.weekNumberStyles()}></td>
+        <td class='fc-week-number fc-shrink'></td>
       ]
     }
 

+ 11 - 25
packages/list/src/ListView.tsx

@@ -1,6 +1,5 @@
 import {
   h, createRef,
-  subtractInnerElHeight,
   View,
   ViewProps,
   Scroller,
@@ -30,8 +29,7 @@ export default class ListView extends View {
   private computeDateVars = memoize(computeDateVars)
   private eventStoreToSegs = memoize(this._eventStoreToSegs)
   private renderEvents = subrenderer(ListViewEvents)
-  private rootEl: HTMLDivElement
-  private scrollerRef = createRef<Scroller>()
+  private scrollerElRef = createRef<HTMLDivElement>()
   private eventRenderer: ListViewEvents
 
 
@@ -45,15 +43,18 @@ export default class ListView extends View {
 
     return (
       <div ref={this.setRootEl} class={classNames.join(' ')}>
-        <Scroller ref={this.scrollerRef} overflowX='hidden' overflowY='auto' />
+        <Scroller
+          elRef={this.scrollerElRef}
+          vGrow={!props.isHeightAuto}
+          overflowX='hidden'
+          overflowY='auto'
+        />
       </div>
     )
   }
 
 
   setRootEl = (rootEl: HTMLDivElement | null) => {
-    this.rootEl = rootEl
-
     if (rootEl) {
       this.context.calendar.registerInteractiveComponent(this, { // TODO: make aware that it doesn't do Hits
         el: rootEl
@@ -67,11 +68,13 @@ export default class ListView extends View {
 
   componentDidMount() {
     this.subrender()
+    this.resize()
   }
 
 
   componentDidUpdate() {
     this.subrender()
+    this.resize() // called too often!!!
   }
 
 
@@ -82,7 +85,7 @@ export default class ListView extends View {
     this.eventRenderer = this.renderEvents({
       segs: this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges),
       dayDates,
-      contentEl: this.scrollerRef.current.rootEl,
+      contentEl: this.scrollerElRef.current,
       selectedInstanceId: props.eventSelection, // TODO: rename
       hiddenInstances: // TODO: more convenient
         (props.eventDrag ? props.eventDrag.affectedEvents.instances : null) ||
@@ -94,26 +97,9 @@ export default class ListView extends View {
   }
 
 
-  updateSize(isResize, viewHeight, isAuto) {
-
-    // efficient. uses flags
+  resize(isResize?: boolean) { // TODO: have caller use this flag!!!!!!
     this.eventRenderer.computeSizes(isResize, this)
     this.eventRenderer.assignSizes(isResize, this)
-
-    let scroller = this.scrollerRef.current
-    scroller.clear() // sets height to 'auto' and clears overflow
-
-    if (!isAuto) {
-      scroller.setHeight(this.computeScrollerHeight(viewHeight))
-    }
-  }
-
-
-  computeScrollerHeight(viewHeight) {
-    let { rootEl } = this
-    let scrollerEl = this.scrollerRef.current.rootEl
-
-    return viewHeight - subtractInnerElHeight(rootEl, scrollerEl) // everything that's NOT the scroller
   }
 
 

+ 2 - 5
packages/timegrid/src/DayTimeCols.tsx

@@ -28,6 +28,7 @@ export interface DayTimeColsProps {
   eventSelection: string
   eventDrag: EventInteractionState | null
   eventResize: EventInteractionState | null
+  colGroupNode: VNode
   renderBgIntro: () => VNode[]
   renderIntro: () => VNode[]
 }
@@ -56,6 +57,7 @@ export default class DayTimeCols extends DateComponent<DayTimeColsProps> {
         {...this.slicer.sliceProps(props, dateProfile, null, context.calendar, dayRanges)}
         dateProfile={dateProfile}
         cells={dayTableModel.cells[0]}
+        colGroupNode={props.colGroupNode}
         renderBgIntro={props.renderBgIntro}
         renderIntro={props.renderIntro}
       />
@@ -74,11 +76,6 @@ export default class DayTimeCols extends DateComponent<DayTimeColsProps> {
   }
 
 
-  updateSize(isResize: boolean) {
-    this.timeCols.updateSize(isResize)
-  }
-
-
   getNowIndicatorUnit() {
     return this.timeCols.getNowIndicatorUnit()
   }

+ 17 - 32
packages/timegrid/src/DayTimeColsView.tsx

@@ -6,7 +6,8 @@ import {
   DaySeries,
   DayTableModel,
   memoize,
-  ViewProps
+  ViewProps,
+  ChunkContentCallbackArgs
 } from '@fullcalendar/core'
 import { DayTable } from '@fullcalendar/daygrid'
 import TimeColsView from './TimeColsView'
@@ -34,50 +35,34 @@ export default class DayTimeColsView extends TimeColsView {
           datesRepDistinctDays={true}
           renderIntro={this.renderHeadIntro}
         />,
-      options.allDaySlot &&
+      options.allDaySlot && ((contentArg: ChunkContentCallbackArgs) => (
         <DayTable
           ref={this.dayTableRef}
           {...splitProps['allDay']}
           dateProfile={dateProfile}
           dayTableModel={dayTableModel}
           nextDayThreshold={nextDayThreshold}
+          colGroupNode={contentArg.colGroupNode}
           isRigid={false}
           renderNumberIntro={this.renderTableIntro}
           renderBgIntro={this.renderTableBgIntro}
           renderIntro={this.renderTableIntro}
           colWeekNumbersVisible={false}
           cellWeekNumbersVisible={false}
-        />,
-      <DayTimeCols
-        ref={this.timeColsRef}
-        {...splitProps['timed']}
-        dateProfile={dateProfile}
-        dayTableModel={dayTableModel}
-        renderBgIntro={this.renderTimeColsBgIntro}
-        renderIntro={this.renderTimeColsIntro}
-      />
-    )
-  }
-
-
-  updateSize(isResize: boolean, viewHeight: number, isAuto: boolean) {
-    let timeCols = this.timeColsRef.current
-    let dayTable = this.dayTableRef.current
-
-    if (isResize || this.isLayoutSizeDirty()) {
-      this.updateLayoutSize(
-        timeCols.timeCols,
-        dayTable ? dayTable.table : null,
-        viewHeight,
-        isAuto
+        />
+      )),
+      (contentArg: ChunkContentCallbackArgs) => (
+        <DayTimeCols
+          ref={this.timeColsRef}
+          {...splitProps['timed']}
+          dateProfile={dateProfile}
+          dayTableModel={dayTableModel}
+          colGroupNode={contentArg.colGroupNode}
+          renderBgIntro={this.renderTimeColsBgIntro}
+          renderIntro={this.renderTimeColsIntro}
+        />
       )
-    }
-
-    if (dayTable) {
-      dayTable.updateSize(isResize)
-    }
-
-    timeCols.updateSize(isResize)
+    )
   }
 
 

+ 7 - 6
packages/timegrid/src/TimeCols.tsx

@@ -1,5 +1,5 @@
 import {
-  h, VNode, createRef, Ref,
+  h, VNode, Ref,
   removeElement,
   applyStyle,
   PositionCache,
@@ -46,6 +46,7 @@ export interface TimeColsProps {
   eventDrag: EventSegUiInteractionState | null
   eventResize: EventSegUiInteractionState | null
   rootElRef?: Ref<HTMLDivElement>
+  colGroupNode: VNode
   renderBgIntro: () => VNode[]
   renderIntro: () => VNode[]
 }
@@ -68,7 +69,6 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
   private snapDuration: Duration // granularity of time for dragging and selecting
   private snapsPerSlot: any
 
-  private bottomRuleElRef = createRef<HTMLHRElement>()
   private contentSkeletonEl: HTMLElement
   private colContainerEls: HTMLElement[] // containers for each column
   private businessContainerEls: HTMLElement[]
@@ -87,8 +87,6 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
   private isColSizesDirty: boolean = false
   private segRenderers: (TimeColsEvents | TimeColsFills | null)[]
 
-  get bottomRuleEl() { return this.bottomRuleElRef.current }
-
 
   /* Rendering
   ------------------------------------------------------------------------------------------------------------------*/
@@ -103,6 +101,7 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
         <TimeColsBg
           dateProfile={props.dateProfile}
           cells={props.cells}
+          colGroupNode={props.colGroupNode}
           renderIntro={props.renderBgIntro}
           handleDom={this.handleBgDom}
         />
@@ -113,10 +112,10 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
         />
         <TimeColsContentSkeleton
           colCnt={props.cells.length}
+          colGroupNode={props.colGroupNode}
           renderIntro={props.renderIntro}
           handleDom={this.handleContentSkeletonDom}
         />
-        <hr class={'fc-divider ' + context.theme.getClass('widgetHeader')} ref={this.bottomRuleElRef} />
       </div>
     )
   }
@@ -186,11 +185,13 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
 
   componentDidMount() {
     this.subrender()
+    this.resize()
   }
 
 
   componentDidUpdate() {
     this.subrender()
+    this.resize()
   }
 
 
@@ -266,7 +267,7 @@ export default class TimeCols extends BaseComponent<TimeColsProps> {
   }
 
 
-  updateSize(isResize: boolean) {
+  resize(isResize?: boolean) { // gahhhhhhhh
     let { segRenderers } = this
 
     if (isResize || this.isSlatSizesDirty) {

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

@@ -13,6 +13,7 @@ import { DayBgRow } from '@fullcalendar/daygrid'
 export interface TimeColsBgProps {
   dateProfile: DateProfile
   cells: TimeColsCell[]
+  colGroupNode: VNode
   renderIntro: () => VNode[]
   handleDom?: (rootEl: HTMLElement | null, colEls: HTMLElement[] | null) => void
 }
@@ -32,6 +33,7 @@ export default class TimeColsBg extends BaseComponent<TimeColsBgProps> {
     return ( // guid rerenders whole DOM every time
       <div class='fc-bg' ref={this.handleRootEl} key={guid()}>
         <table class={theme.getClass('tableGrid')}>
+          {props.colGroupNode}
           <DayBgRow
             cells={props.cells}
             dateProfile={props.dateProfile}

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

@@ -9,6 +9,7 @@ import {
 
 export interface TimeColsContentSkeletonProps {
   colCnt: number
+  colGroupNode: VNode
   renderIntro: () => VNode[]
   handleDom?: (rootEl: HTMLElement | null, containers: TimeColsContentSkeletonContainers | null) => void
 }
@@ -51,6 +52,7 @@ export default class TimeColsContentSkeleton extends BaseComponent<TimeColsConte
     return ( // guid rerenders whole DOM every time
       <div class='fc-content-skeleton' ref={this.handleRootEl} key={guid()}>
         <table>
+          {props.colGroupNode}
           <tr>{cellNodes}</tr>
         </table>
       </div>

+ 67 - 139
packages/timegrid/src/TimeColsView.tsx

@@ -1,22 +1,22 @@
 import {
-  h, ComponentChildren, createRef,
-  findElements,
-  matchCellWidths, uncompensateScroll, compensateScroll, subtractInnerElHeight,
-  Scroller,
+  h, createRef,
   View,
   createFormatter, diffDays,
   Duration,
   DateMarker,
   getViewClassNames,
   GotoAnchor,
-  ViewProps
+  ViewProps,
+  SimpleScrollGridSection,
+  VNode,
+  SimpleScrollGrid,
+  ChunkContentCallbackArgs
 } from '@fullcalendar/core'
 import { Table } from '@fullcalendar/daygrid'
 import { TimeCols } from './main'
 import AllDaySplitter from './AllDaySplitter'
 
 
-const ALL_DAY_EVENT_LIMIT = 5
 const WEEK_HEADER_FORMAT = createFormatter({ week: 'short' })
 
 
@@ -31,7 +31,7 @@ export default abstract class TimeColsView extends View {
 
   private rootElRef = createRef<HTMLDivElement>()
   private dividerElRef = createRef<HTMLHRElement>()
-  private scrollerRef = createRef<Scroller>()
+  private scrollerElRef = createRef<HTMLDivElement>()
   private axisWidth: any // the width of the time axis running down the side
 
 
@@ -53,44 +53,53 @@ export default abstract class TimeColsView extends View {
 
 
   renderLayout(
-    headerChildren: ComponentChildren | null,
-    allDayChildren: ComponentChildren | null,
-    timeChildren: ComponentChildren
+    headerRowContent: VNode | null,
+    allDayContent: ((contentArg: ChunkContentCallbackArgs) => VNode) | null,
+    timeContent: ((contentArg: ChunkContentCallbackArgs) => VNode) | null
   ) {
     let { theme } = this.context
     let classNames = getViewClassNames(this.props.viewSpec).concat('fc-timeGrid-view')
+    let sections: SimpleScrollGridSection[] = []
+
+    if (headerRowContent) {
+      sections.push({
+        type: 'head',
+        className: 'fc-head',
+        chunk: {
+          scrollerClassName: 'fc-head-container', // needed for anything?
+          rowContent: headerRowContent
+        }
+      })
+    }
+
+    if (allDayContent) {
+      sections.push({
+        type: 'body',
+        chunk: {
+          content: allDayContent
+        }
+      })
+      sections.push({
+        outerContent: <hr class={'fc-divider ' + theme.getClass('widgetHeader')} ref={this.dividerElRef} />
+      })
+    }
+
+    sections.push({
+      type: 'body',
+      className: 'fc-body', // should we use above?
+      chunk: {
+        scrollerElRef: this.scrollerElRef,
+        scrollerClassName: 'fc-time-grid-container',
+        content: timeContent
+      }
+    })
 
     return (
       <div class={classNames.join(' ')} ref={this.rootElRef}>
-        <table class={theme.getClass('tableGrid')}>
-          {headerChildren &&
-            <thead class='fc-head'>
-              <tr>
-                <td class={'fc-head-container ' + theme.getClass('widgetHeader')}>
-                  {headerChildren}
-                </td>
-              </tr>
-            </thead>
-          }
-          <tbody class='fc-body'>
-            <tr>
-              <td class={theme.getClass('widgetContent')}>
-                {allDayChildren}
-                {allDayChildren &&
-                  <hr class={'fc-divider ' + theme.getClass('widgetHeader')} ref={this.dividerElRef}/>
-                }
-                <Scroller
-                  ref={this.scrollerRef}
-                  overflowX='hidden'
-                  overflowY='auto'
-                  extraClassName='fc-time-grid-container'
-                >
-                  {timeChildren}
-                </Scroller>
-              </td>
-            </tr>
-          </tbody>
-        </table>
+        <SimpleScrollGrid
+          cols={[ { width: 'shrink' } ]}
+          sections={sections}
+        />
       </div>
     )
   }
@@ -109,14 +118,8 @@ export default abstract class TimeColsView extends View {
   }
 
 
-  getSnapshotBeforeUpdate() {
-    let scroller = this.scrollerRef.current
-
-    return { scrollTop: scroller.controller.getScrollTop() }
-  }
-
-
   componentDidUpdate(prevProps: ViewProps, prevState: {}, snapshot) {
+    // not working on window resize!!!??
 
     if (prevProps.dateProfile !== this.props.dateProfile) {
       this.scrollToInitialTime()
@@ -154,91 +157,16 @@ export default abstract class TimeColsView extends View {
   /* Dimensions
   ------------------------------------------------------------------------------------------------------------------*/
 
-
-  abstract updateSize(isResize: boolean, viewHeight: number, isAuto: boolean)
-
-
-  // Adjusts the vertical dimensions of the view to the specified values
-  updateLayoutSize(timeCols: TimeCols, table: Table | null, viewHeight, isAuto) {
-    let rootEl = this.rootElRef.current
-    let scroller = this.scrollerRef.current
-    let eventLimit
-    let scrollerHeight
-    let scrollbarWidths
-
-    // make all axis cells line up
-    this.axisWidth = matchCellWidths(findElements(rootEl, '.fc-axis'))
-
-    // hack to give the view some height prior to timeGrid's columns being rendered
-    // TODO: separate setting height from scroller VS timeGrid.
-    if (!timeCols.colEls) {
-      if (!isAuto) {
-        scrollerHeight = this.computeScrollerHeight(viewHeight)
-        scroller.setHeight(scrollerHeight)
-      }
-      return
-    }
-
-    // set of fake row elements that must compensate when scroller has scrollbars
-    let noScrollRowEls: HTMLElement[] = findElements(rootEl, '.fc-row').filter((node) => {
-      return !scroller.rootEl.contains(node)
-    })
-
-    // reset all dimensions back to the original state
-    timeCols.bottomRuleEl.style.display = 'none' // will be shown later if this <hr> is necessary
-    scroller.clear() // sets height to 'auto' and clears overflow
-    noScrollRowEls.forEach(uncompensateScroll)
-
-    // limit number of events in the all-day area
-    if (table) {
-
-      eventLimit = this.context.options.eventLimit
-      if (eventLimit && typeof eventLimit !== 'number') {
-        eventLimit = ALL_DAY_EVENT_LIMIT // make sure "auto" goes to a real number
-      }
-      if (eventLimit) {
-        table.limitRows(eventLimit)
-      }
-    }
-
-    if (!isAuto) { // should we force dimensions of the scroll container?
-
-      scrollerHeight = this.computeScrollerHeight(viewHeight)
-      scroller.setHeight(scrollerHeight)
-      scrollbarWidths = scroller.getScrollbarWidths()
-
-      if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
-
-        // make the all-day and header rows lines up
-        noScrollRowEls.forEach(function(rowEl) {
-          compensateScroll(rowEl, scrollbarWidths)
-        })
-
-        // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
-        // and reapply the desired height to the scroller.
-        scrollerHeight = this.computeScrollerHeight(viewHeight)
-        scroller.setHeight(scrollerHeight)
-      }
-
-      // guarantees the same scrollbar widths
-      scroller.lockOverflow(scrollbarWidths)
-
-      // if there's any space below the slats, show the horizontal rule.
-      // this won't cause any new overflow, because lockOverflow already called.
-      if (timeCols.getTotalSlatHeight() < scrollerHeight) {
-        timeCols.bottomRuleEl.style.display = ''
-      }
-    }
-  }
-
-
-  // given a desired total height of the view, returns what the height of the scroller should be
-  computeScrollerHeight(viewHeight) {
-    let rootEl = this.rootElRef.current
-    let scroller = this.scrollerRef.current
-
-    return viewHeight - subtractInnerElHeight(rootEl, scroller.rootEl) // everything that's NOT the scroller
-  }
+  // const ALL_DAY_EVENT_LIMIT = 5
+  //
+  // let eventLimit
+  // eventLimit = this.context.options.eventLimit
+  // if (eventLimit && typeof eventLimit !== 'number') {
+  //   eventLimit = ALL_DAY_EVENT_LIMIT // make sure "auto" goes to a real number
+  // }
+  // if (eventLimit) {
+  //   table.limitRows(eventLimit)
+  // }
 
 
   /* Scroll
@@ -256,9 +184,9 @@ export default abstract class TimeColsView extends View {
 
   scrollTop(top: number) {
     this.afterSizing(() => { // hack
-      let scroller = this.scrollerRef.current
+      let scrollerEl = this.scrollerElRef.current
 
-      scroller.controller.setScrollTop(top)
+      scrollerEl.scrollTop = top
     })
   }
 
@@ -293,7 +221,7 @@ export default abstract class TimeColsView extends View {
       weekText = dateEnv.format(range.start, WEEK_HEADER_FORMAT)
 
       return [
-        <th class={'fc-axis fc-week-number ' + theme.getClass('widgetHeader')} style={this.getAxisStyles()}>
+        <th class={'fc-axis fc-week-number fc-shrink ' + theme.getClass('widgetHeader')} style={this.getAxisStyles()}>
           <GotoAnchor
             navLinks={options.navLinks}
             gotoOptions={{ date: range.start, type: 'week', forceOff: dayCnt > 1 }}
@@ -303,7 +231,7 @@ export default abstract class TimeColsView extends View {
     }
 
     return [
-      <th class={'fc-axis ' + theme.getClass('widgetHeader')} style={this.getAxisStyles()}></th>
+      <th class={'fc-axis fc-shrink ' + theme.getClass('widgetHeader')} style={this.getAxisStyles()}></th>
     ]
   }
 
@@ -326,7 +254,7 @@ export default abstract class TimeColsView extends View {
     let { theme } = this.context
 
     return [
-      <td class={'fc-axis ' + theme.getClass('widgetContent')} style={this.getAxisStyles()}></td>
+      <td class={'fc-axis fc-shrink ' + theme.getClass('widgetContent')} style={this.getAxisStyles()}></td>
     ]
   }
 
@@ -335,7 +263,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 fc-shrink' style={this.getAxisStyles()}></td>
     ]
   }
 
@@ -356,7 +284,7 @@ export default abstract class TimeColsView extends View {
     }
 
     return [
-      <td class={'fc-axis ' + theme.getClass('widgetContent')} style={this.getAxisStyles()}>
+      <td class={'fc-axis fc-shrink ' + theme.getClass('widgetContent')} style={this.getAxisStyles()}>
         <span {...spanAttrs}>
           {child}
         </span>
@@ -369,7 +297,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 fc-shrink' style={this.getAxisStyles()}></td>
     ]
   }
 

+ 1 - 0
tsconfig.json

@@ -32,6 +32,7 @@
       "@fullcalendar/moment-timezone": [ "packages/moment-timezone/src/main" ],
       "@fullcalendar/google-calendar": [ "packages/google-calendar/src/main" ],
       "@fullcalendar/bootstrap": [ "packages/bootstrap/src/main" ],
+      "@fullcalendar/scrollgrid": [ "packages-premium/scrollgrid/src/main" ],
       "@fullcalendar/timeline": [ "packages-premium/timeline/src/main" ],
       "@fullcalendar/resource-common": [ "packages-premium/resource-common/src/main" ],
       "@fullcalendar/resource-timeline": [ "packages-premium/resource-timeline/src/main" ],