Adam Shaw 6 лет назад
Родитель
Сommit
da5c966ead
56 измененных файлов с 2392 добавлено и 3376 удалено
  1. 25 27
      packages/core/src/Calendar.tsx
  2. 39 42
      packages/core/src/CalendarComponent.tsx
  3. 73 44
      packages/core/src/NowTimer.ts
  4. 134 0
      packages/core/src/StandardEvent.tsx
  5. 23 22
      packages/core/src/common/DayHeader.tsx
  6. 16 22
      packages/core/src/common/TableDateCell.tsx
  7. 8 4
      packages/core/src/component/DateComponent.ts
  8. 80 32
      packages/core/src/component/date-rendering.tsx
  9. 182 93
      packages/core/src/component/event-rendering.ts
  10. 0 349
      packages/core/src/component/renderers/FgEventRenderer.ts
  11. 0 111
      packages/core/src/component/renderers/FillRenderer.ts
  12. 3 1
      packages/core/src/datelib/env.ts
  13. 2 2
      packages/core/src/datelib/formatting.ts
  14. 1 2
      packages/core/src/interactions/event-dragging.ts
  15. 23 22
      packages/core/src/main.ts
  16. 7 6
      packages/core/src/plugin-system.ts
  17. 196 0
      packages/core/src/render-hook.ts
  18. 72 46
      packages/core/src/scrollgrid/SimpleScrollGrid.tsx
  19. 25 0
      packages/core/src/scrollgrid/table-styling.ts
  20. 4 28
      packages/core/src/scrollgrid/util.tsx
  21. 35 15
      packages/core/src/styles/_event-horizontal.scss
  22. 27 24
      packages/core/src/styles/_event.scss
  23. 0 1
      packages/core/src/types/input-types.ts
  24. 29 36
      packages/core/src/util/RefMap.ts
  25. 6 83
      packages/core/src/util/dom-manip.ts
  26. 0 42
      packages/core/src/util/html.ts
  27. 94 33
      packages/core/src/util/memoize.ts
  28. 38 1
      packages/core/src/util/object.ts
  29. 3 234
      packages/core/src/vdom-util.tsx
  30. 0 89
      packages/daygrid/src/CellEvents.ts
  31. 0 65
      packages/daygrid/src/DayBgCell.tsx
  32. 0 58
      packages/daygrid/src/DayBgRow.tsx
  33. 18 20
      packages/daygrid/src/DayTable.tsx
  34. 5 12
      packages/daygrid/src/DayTableView.tsx
  35. 0 105
      packages/daygrid/src/DayTile.tsx
  36. 0 51
      packages/daygrid/src/DayTileEvents.ts
  37. 115 0
      packages/daygrid/src/MorePopover.tsx
  38. 10 7
      packages/daygrid/src/Popover.tsx
  39. 139 556
      packages/daygrid/src/Table.tsx
  40. 165 0
      packages/daygrid/src/TableCell.tsx
  41. 26 0
      packages/daygrid/src/TableEvent.tsx
  42. 0 284
      packages/daygrid/src/TableEvents.ts
  43. 0 132
      packages/daygrid/src/TableFills.tsx
  44. 0 60
      packages/daygrid/src/TableMirrorEvents.tsx
  45. 382 0
      packages/daygrid/src/TableRow.tsx
  46. 67 0
      packages/daygrid/src/TableSeg.ts
  47. 0 153
      packages/daygrid/src/TableSkeleton.tsx
  48. 0 77
      packages/daygrid/src/TableSkeletonDayCell.tsx
  49. 1 127
      packages/daygrid/src/TableView.tsx
  50. 199 0
      packages/daygrid/src/event-placement.ts
  51. 1 2
      packages/daygrid/src/main.scss
  52. 4 3
      packages/daygrid/src/main.ts
  53. 0 169
      packages/daygrid/src/styles/_day-grid.scss
  54. 113 0
      packages/daygrid/src/styles/_daygrid.scss
  55. 0 48
      packages/daygrid/src/styles/_event-limit.scss
  56. 2 36
      packages/daygrid/src/styles/_event.scss

+ 25 - 27
packages/core/src/Calendar.tsx

@@ -37,6 +37,8 @@ import ViewApi from './ViewApi'
 import { globalPlugins } from './global-plugins'
 import { removeExact } from './util/array'
 import { guid } from './util/misc'
+import { CssDimValue } from './scrollgrid/util'
+import { applyStyleProp } from './util/dom-manip'
 
 
 export interface DateClickApi extends DatePointApi {
@@ -118,6 +120,7 @@ export default class Calendar {
   isDatesUpdated: boolean = false
   isEventsUpdated: boolean = false
   el: HTMLElement
+  currentClassNames: string[] = []
   componentRef = createRef<CalendarComponent>()
   view: ViewApi // public API
 
@@ -344,7 +347,6 @@ export default class Calendar {
       <ComponentContextType.Provider value={context}>
         <CalendarComponent
           ref={this.componentRef}
-          rootEl={this.el}
           { ...state }
           viewSpec={viewSpec}
           dateProfileGenerator={this.dateProfileGenerators[viewType]}
@@ -356,6 +358,8 @@ export default class Calendar {
           eventDrag={state.eventDrag}
           eventResize={state.eventResize}
           title={viewApi.title}
+          onClassNameChange={this.handleClassNames}
+          onHeightChange={this.handleHeightChange}
           />
       </ComponentContextType.Provider>,
       this.el
@@ -388,8 +392,6 @@ export default class Calendar {
     if (this.isEventsUpdated) {
       this.isEventsUpdated = false
     }
-
-    this.releaseAfterSizingTriggers()
   }
 
 
@@ -404,6 +406,26 @@ export default class Calendar {
   }
 
 
+  handleClassNames = (classNames: string[]) => {
+    let { classList } = this.el
+
+    for (let className of this.currentClassNames) {
+      classList.remove(className)
+    }
+
+    for (let className of classNames) {
+      classList.add(className)
+    }
+
+    this.currentClassNames = classNames
+  }
+
+
+  handleHeightChange = (height: CssDimValue) => {
+    applyStyleProp(this.el, 'height', height)
+  }
+
+
   // Options
   // -----------------------------------------------------------------------------------------------------------------
 
@@ -596,30 +618,6 @@ export default class Calendar {
   }
 
 
-  // Post-sizing hacks (kill after updateSize refactor)
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  publiclyTriggerAfterSizing<T extends EventHandlerName>(name: T, args: EventHandlerArgs<T>) {
-    let { afterSizingTriggers } = this;
-
-    (afterSizingTriggers[name] || (afterSizingTriggers[name] = [])).push(args)
-  }
-
-
-  releaseAfterSizingTriggers() {
-    let { afterSizingTriggers } = this
-
-    for (let name in afterSizingTriggers) {
-      for (let args of afterSizingTriggers[name]) {
-        this.publiclyTrigger(name as EventHandlerName, args)
-      }
-    }
-
-    this.afterSizingTriggers = {}
-  }
-
-
   // View
   // -----------------------------------------------------------------------------------------------------------------
 

+ 39 - 42
packages/core/src/CalendarComponent.tsx

@@ -12,19 +12,21 @@ import { CalendarState } from './reducers/types'
 import { ViewPropsTransformerClass } from './plugin-system'
 import { __assign } from 'tslib'
 import { h, Fragment, createRef } from './vdom'
-import { BaseComponent, subrenderer } from './vdom-util'
+import { BaseComponent } from './vdom-util'
 import { buildDelegationHandler } from './util/dom-event'
 import { capitaliseFirstLetter } from './util/misc'
-import { applyStyleProp } from './util/dom-manip'
 import ViewContainer from './ViewContainer'
+import { CssDimValue } from './scrollgrid/util'
+import Theme from './theme/Theme'
 
 
 export interface CalendarComponentProps extends CalendarState {
-  rootEl: HTMLElement
   viewSpec: ViewSpec
   dateProfileGenerator: DateProfileGenerator // for the current view
   eventUiBases: EventUiHash
   title: string
+  onClassNameChange?: (classNameHash) => void // will be fired with [] on cleanup
+  onHeightChange?: (height: CssDimValue) => void // will be fired with '' on cleanup
 }
 
 interface CalendarComponentState {
@@ -38,8 +40,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 updateOuterClassNames = subrenderer(setClassNames, unsetClassNames)
-  private updateOuterHeight = subrenderer(setHeight, unsetHeight)
+  private reportClassNames = memoize(reportClassNames)
+  private reportHeight = memoize(reportHeight)
   private handleNavLinkClick = buildDelegationHandler('a[data-goto]', this._handleNavLinkClick.bind(this))
   private headerRef = createRef<Toolbar>()
   private footerRef = createRef<Toolbar>()
@@ -83,10 +85,13 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
       viewAspectRatio = Math.max(options.aspectRatio, 0.5) // prevent from getting too tall
     }
 
-    // TODO: move this somewhere after real render!
-    // move to Calendar class?
-    this.updateOuterClassNames({ el: props.rootEl, forPrint: state.forPrint })
-    this.updateOuterHeight({ el: props.rootEl, height: calendarHeight })
+    if (props.onClassNameChange) {
+      this.reportClassNames(props.onClassNameChange, state.forPrint, options.dir, context.theme)
+    }
+
+    if (props.onHeightChange) {
+      this.reportHeight(props.onHeightChange, calendarHeight)
+    }
 
     return (
       <Fragment>
@@ -102,10 +107,10 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
           vGrow={viewVGrow}
           height={viewHeight}
           aspectRatio={viewAspectRatio}
-          elRef={this.setViewContainerEl}
           onClick={this.handleNavLinkClick}
         >
           {this.renderView(props, this.context)}
+          {this.buildAppendContent()}
         </ViewContainer>
         {footer &&
           <Toolbar
@@ -129,7 +134,14 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
   componentWillUnmount() {
     window.removeEventListener('beforeprint', this.handleBeforePrint)
     window.removeEventListener('afterprint', this.handleAfterPrint)
-    this.subrenderDestroy()
+
+    if (this.props.onClassNameChange) {
+      this.props.onClassNameChange([])
+    }
+
+    if (this.props.onHeightChange) {
+      this.props.onHeightChange('')
+    }
   }
 
 
@@ -167,14 +179,12 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
   }
 
 
-  setViewContainerEl = (viewContainerEl: HTMLElement | null) => {
+  buildAppendContent() {
     let { pluginHooks, calendar } = this.context
 
-    if (viewContainerEl) {
-      for (let modifyViewContainer of pluginHooks.viewContainerModifiers) {
-        modifyViewContainer(viewContainerEl, calendar)
-      }
-    }
+    return pluginHooks.viewContainerAppends.map(
+      (buildAppendContent) => buildAppendContent(calendar)
+    )
   }
 
 
@@ -227,6 +237,7 @@ export default class CalendarComponent extends BaseComponent<CalendarComponentPr
     )
   }
 
+
 }
 
 
@@ -261,12 +272,16 @@ function isHeightAuto(options) {
 // -----------------------------------------------------------------------------------------------------------------
 
 
-function setClassNames({ el, forPrint }: { el: HTMLElement, forPrint: boolean }, context: ComponentContext) {
-  let classList = el.classList
+function reportClassNames(onClassNameChange, forPrint: boolean, dir: string, theme: Theme) {
+  onClassNameChange(computeClassNames(forPrint, dir, theme))
+}
+
+
+function computeClassNames(forPrint: boolean, dir: string, theme: Theme) {
   let classNames: string[] = [
     'fc',
-    'fc-' + context.options.dir,
-    context.theme.getClass('root')
+    'fc-' + dir,
+    theme.getClass('root')
   ]
 
   if (forPrint) {
@@ -275,30 +290,12 @@ function setClassNames({ el, forPrint }: { el: HTMLElement, forPrint: boolean },
     classNames.push('fc-screen')
   }
 
-  for (let className of classNames) {
-    classList.add(className)
-  }
-
-  return { el, classNames }
+  return classNames
 }
 
 
-function unsetClassNames({ el, classNames }: { el: HTMLElement, classNames: string[] }) {
-  let classList = el.classList
-
-  for (let className of classNames) {
-    classList.remove(className)
-  }
-}
-
-
-function setHeight({ el, height }: { el: HTMLElement, height: any }) {
-  applyStyleProp(el, 'height', height)
-  return el
-}
-
-function unsetHeight(el: HTMLElement) {
-  applyStyleProp(el, 'height', '')
+function reportHeight(onHeightChange, height: CssDimValue) {
+  onHeightChange(height)
 }
 
 

+ 73 - 44
packages/core/src/NowTimer.ts

@@ -1,73 +1,102 @@
-import { DateMarker, addMs } from './datelib/marker'
+import { DateMarker, addMs, startOfDay, addDays } from './datelib/marker'
 import { createDuration } from './datelib/duration'
-import { SubRenderer } from './vdom-util'
-import ComponentContext from './component/ComponentContext'
+import ComponentContext, { ComponentContextType } from './component/ComponentContext'
+import { ComponentChildren, Component } from './vdom'
+import { DateRange } from './datelib/date-range'
 
 
 export interface NowTimerProps {
-  enabled: boolean
-  unit: string
-  callback: NowTimerCallback
+  unit: string // TODO: add type of unit
+  content: (now: DateMarker, todayRange: DateRange) => ComponentChildren
 }
 
-export type NowTimerCallback = (now: DateMarker) => void
+interface NowTimerState {
+  nowDate: DateMarker
+  todayRange: DateRange
+}
 
 
-export default class NowTimer extends SubRenderer {
+export default class NowTimer extends Component<NowTimerProps, NowTimerState> {
 
-  private timeoutId: any
-  private intervalId: any
+  static contextType = ComponentContextType
+  context: ComponentContext // do this for all components that use the context!!!
 
+  initialNowDate: DateMarker
+  initialNowQueriedMs: number
+  timeoutId: any
 
-  render(props: NowTimerProps, context: ComponentContext) {
 
-    if (!props.enabled) {
-      return
-    }
+  constructor(props: NowTimerProps, context: ComponentContext) {
+    super(props, context)
 
-    let { dateEnv } = context
-    let { unit, callback } = props
-    let initialNowDate = context.calendar.getNow()
-    let initialNowQueriedMs = new Date().valueOf()
+    this.initialNowDate = context.calendar.getNow()
+    this.initialNowQueriedMs = new Date().valueOf()
+
+    this.state = this.computeTiming().currentState
+  }
+
+
+  render(props: NowTimerProps, state: NowTimerState) {
+    return props.content(state.nowDate, state.todayRange)
+  }
 
-    function update() {
-      callback(addMs(initialNowDate, new Date().valueOf() - initialNowQueriedMs))
+
+  componentDidMount() {
+    this.setTimeout()
+  }
+
+
+  componentDidUpdate(prevProps: NowTimerProps) {
+    if (prevProps.unit !== this.props.unit) {
+      this.clearTimeout()
+      this.setTimeout()
     }
+  }
+
 
-    update()
+  componentWillUnmount() {
+    this.clearTimeout()
+  }
 
-    // wait until the beginning of the next interval
-    let delay = dateEnv.add(
-      dateEnv.startOf(initialNowDate, unit),
-      createDuration(1, unit)
-    ).valueOf() - initialNowDate.valueOf()
 
-    // TODO: maybe always use setTimeout, waiting until start of next unit
-    this.timeoutId = setTimeout(() => {
-      this.timeoutId = null
-      update()
+  private computeTiming() {
+    let { props, context } = this
+    let unroundedNow = addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs)
+    let currentUnitStart = context.dateEnv.startOf(unroundedNow, props.unit)
+    let nextUnitStart = context.dateEnv.add(currentUnitStart, createDuration(1, props.unit))
+    let waitMs = nextUnitStart.valueOf() - unroundedNow.valueOf()
 
-      if (unit === 'second') {
-        delay = 1000 // every second
-      } else {
-        delay = 1000 * 60 // otherwise, every minute
-      }
+    return {
+      currentState: { nowDate: currentUnitStart, todayRange: buildDayRange(currentUnitStart) } as NowTimerState,
+      nextState: { nowDate: nextUnitStart, todayRange: buildDayRange(nextUnitStart) } as NowTimerState,
+      waitMs
+    }
+  }
 
-      this.intervalId = setInterval(update, delay) // update every interval
-    }, delay)
+
+  private setTimeout() {
+    let { nextState, waitMs } = this.computeTiming()
+
+    this.timeoutId = setTimeout(() => {
+      this.setState(nextState, () => {
+        this.setTimeout()
+      })
+    }, waitMs)
   }
 
 
-  unrender() {
+  private clearTimeout() {
     if (this.timeoutId) {
       clearTimeout(this.timeoutId)
-      this.timeoutId = null
-    }
-
-    if (this.intervalId) {
-      clearInterval(this.intervalId)
-      this.intervalId = null
     }
   }
 
 }
+
+
+function buildDayRange(date: DateMarker): DateRange { // TODO: make this a general util
+  let start = startOfDay(date)
+  let end = addDays(start, 1)
+
+  return { start, end }
+}

+ 134 - 0
packages/core/src/StandardEvent.tsx

@@ -0,0 +1,134 @@
+import { ComponentContext, h, Fragment, VNode } from '@fullcalendar/core'
+import { BaseComponent, setRef } from './vdom-util'
+import { createFormatter } from './datelib/formatting'
+import { Seg } from './component/DateComponent'
+import { EventInnerContentProps, setElSeg, getEventClassNames, getSkinCss, computeSegDraggable, computeSegStartResizable, computeSegEndResizable, buildSegTimeText } from './component/event-rendering'
+import { MountHook, ClassNamesHook, InnerContentHook } from './render-hook'
+import EventApi from './api/EventApi'
+
+
+export interface StandardEventProps extends MinimalEventProps {
+  extraClassNames: string[]
+  defaultTimeFormat: any // date-formatter INPUT
+  defaultDisplayEventTime?: boolean // default true
+  defaultDisplayEventEnd?: boolean // default true
+  disableDragging?: boolean // default false
+  disableResizing?: boolean // default false
+  defaultInnerContent?: (innerProps: EventInnerContentProps) => VNode // not used by anyone yet
+}
+
+export interface MinimalEventProps {
+  seg: Seg
+  isDragging: boolean      // rename to isMirrorDragging? make optional?
+  isResizing: boolean      // rename to isMirrorResizing? make optional?
+  isDateSelecting: boolean // rename to isMirrorDateSelecting? make optional?
+  isSelected: boolean
+  isPast: boolean
+  isFuture: boolean
+  isToday: boolean
+}
+
+
+// should not be a purecomponent
+export default class StandardEvent extends BaseComponent<StandardEventProps> {
+
+
+  render(props: StandardEventProps, state: {}, context: ComponentContext) {
+    let { options } = context
+    let { seg } = props
+
+    let staticInnerProps = {
+      event: new EventApi(context.calendar, seg.eventRange.def, seg.eventRange.instance),
+      view: context.view
+    }
+
+    // TODO: avoid createFormatter, cache!!!
+    // SOLUTION: require that props.defaultTimeFormat is a real formatter, a top-level const,
+    // which will require that defaultRangeSeparator be part of the DateEnv (possible already?),
+    // and have options.eventTimeFormat be preprocessed.
+    let timeFormat = createFormatter(
+      options.eventTimeFormat || props.defaultTimeFormat,
+      options.defaultRangeSeparator
+    )
+
+    let innerProps: EventInnerContentProps = {
+      ...staticInnerProps,
+      timeText: buildSegTimeText(seg, timeFormat, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd),
+      isDraggable: !props.disableDragging && computeSegDraggable(seg, context),
+      isStartResizable: !props.disableResizing && computeSegStartResizable(seg, context),
+      isEndResizable: !props.disableResizing && computeSegEndResizable(seg, context),
+      isMirror: props.isDragging || props.isResizing || props.isDateSelecting,
+      isStart: seg.isStart,
+      isEnd: seg.isEnd,
+      isPast: props.isPast,
+      isFuture: props.isFuture,
+      isToday: props.isToday,
+      isSelected: props.isSelected,
+      isDragging: props.isDragging,
+      isResizing: props.isResizing
+    }
+
+    return (
+      <MountHook
+        name='event'
+        handlerProps={staticInnerProps}
+        content={(rootElRef) => (
+          <ClassNamesHook
+            name='event'
+            handlerProps={innerProps}
+            content={(customClassNames) => (
+              <InnerContentHook
+                name='event'
+                innerProps={innerProps}
+                defaultInnerContent={props.defaultInnerContent || renderInnerContent}
+                outerContent={(innerContentParentRef, innerContent) => {
+                  let url = seg.eventRange.def.url
+                  let anchorAttrs = url ? { href: url } : {}
+                  let style = getSkinCss(seg.eventRange.ui)
+                  let classNames = props.extraClassNames.concat(
+                    getEventClassNames(innerProps),
+                    customClassNames
+                  )
+
+                  const rootRefFunc = (el: HTMLElement | null) => {
+                    setRef(rootElRef, el)
+                    if (el) { setElSeg(el, seg) }
+                  }
+
+                  return (
+                    <a {...anchorAttrs} style={style} className={classNames.join(' ')} ref={rootRefFunc}>
+                      <div class='fc-event-inner' ref={innerContentParentRef}>
+                        {innerContent}
+                      </div>
+                      {innerProps.isStartResizable &&
+                        <div class='fc-event-resizer fc-event-resizer-start' />
+                      }
+                      {innerProps.isEndResizable &&
+                        <div class='fc-event-resizer fc-event-resizer-end' />
+                      }
+                    </a>
+                  )
+                }}
+              />
+            )}
+          />
+        )}
+      />
+    )
+  }
+
+}
+
+
+function renderInnerContent(innerProps: EventInnerContentProps) {
+  return (
+    <Fragment>
+      {innerProps.timeText &&
+        <div class='fc-event-time'>{innerProps.timeText}</div>
+      }
+      <div class='fc-event-title'>
+        {innerProps.event.title || <Fragment>&nbsp;</Fragment>}
+      </div>
+    </Fragment>
+  )
+}

+ 23 - 22
packages/core/src/common/DayHeader.tsx

@@ -6,13 +6,15 @@ import { createFormatter } from '../datelib/formatting'
 import { computeFallbackHeaderFormat } from './table-utils'
 import { VNode, h } from '../vdom'
 import TableDateCell from './TableDateCell'
+import NowTimer from '../NowTimer'
+import { DateRange } from '../datelib/date-range'
 
 
 export interface DayHeaderProps {
   dates: DateMarker[]
   dateProfile: DateProfile
   datesRepDistinctDays: boolean
-  renderIntro?: () => VNode[]
+  renderIntro?: () => VNode
 }
 
 
@@ -22,34 +24,33 @@ export default class DayHeader extends BaseComponent<DayHeaderProps> { // TODO:
   render(props: DayHeaderProps, state: {}, context: ComponentContext) {
     let { dateEnv } = this.context
     let { dates, datesRepDistinctDays } = props
-    let cells: VNode[] = []
-
-    if (props.renderIntro) {
-      cells = props.renderIntro()
-    }
 
     let colHeadFormat = createFormatter(
       context.options.columnHeaderFormat ||
       computeFallbackHeaderFormat(datesRepDistinctDays, dates.length)
     )
 
-    for (let date of dates) {
-      let distinctDateStr = datesRepDistinctDays ? dateEnv.formatIso(date, { omitTime: true }) : ''
-
-      cells.push(
-        <TableDateCell
-          key={distinctDateStr || date.getDay()}
-          distinctDateStr={distinctDateStr}
-          dateMarker={date}
-          dateProfile={props.dateProfile}
-          colCnt={dates.length}
-          colHeadFormat={colHeadFormat}
-        />
-      )
-    }
-
     return (
-      <tr>{cells}</tr>
+      <NowTimer unit='day' content={(nowDate: DateMarker, todayRange: DateRange) => (
+        <tr>
+          {props.renderIntro && props.renderIntro()}
+          {dates.map((date) => {
+            let distinctDateStr = datesRepDistinctDays ? dateEnv.formatIso(date, { omitTime: true }) : ''
+
+            return (
+              <TableDateCell
+                key={distinctDateStr || date.getDay()}
+                distinctDateStr={distinctDateStr}
+                date={date}
+                todayRange={todayRange}
+                dateProfile={props.dateProfile}
+                colCnt={dates.length}
+                colHeadFormat={colHeadFormat}
+              />
+            )
+          })}
+        </tr>
+      )} />
     )
   }
 

+ 16 - 22
packages/core/src/common/TableDateCell.tsx

@@ -1,7 +1,7 @@
-import { rangeContainsMarker } from '../datelib/date-range'
-import { getDayClasses } from '../component/date-rendering'
+import { rangeContainsMarker, DateRange } from '../datelib/date-range'
+import { getDayClassNames, getDayMeta } from '../component/date-rendering'
 import GotoAnchor from '../component/GotoAnchor'
-import { DateMarker, DAY_IDS } from '../datelib/marker'
+import { DateMarker } from '../datelib/marker'
 import { DateProfile } from '../DateProfileGenerator'
 import ComponentContext from '../component/ComponentContext'
 import { h } from '../vdom'
@@ -12,47 +12,41 @@ import { BaseComponent } from '../vdom-util'
 
 export interface TableDateCellProps {
   distinctDateStr: string
-  dateMarker: DateMarker
+  date: DateMarker
   dateProfile: DateProfile
+  todayRange: DateRange
   colCnt: number
   colHeadFormat: DateFormatter
   colSpan?: number
   otherAttrs?: object
 }
 
-export default class TableDateCell extends BaseComponent<TableDateCellProps> {
+export default class TableDateCell extends BaseComponent<TableDateCellProps> { // BAD name for this class now. used in the Header
 
   render(props: TableDateCellProps, state: {}, context: ComponentContext) {
     let { dateEnv, options } = context
-    let { dateMarker, dateProfile, distinctDateStr } = props
-    let isDateValid = rangeContainsMarker(dateProfile.activeRange, dateMarker) // TODO: called too frequently. cache somehow.
-    let classNames = [ 'fc-day-header' ]
+    let { date, dateProfile, distinctDateStr } = props
+    let isDateValid = rangeContainsMarker(dateProfile.activeRange, date) // TODO: called too frequently. cache somehow.
     let innerText
     let innerHtml
 
     if (typeof options.columnHeaderHtml === 'function') {
       innerHtml = options.columnHeaderHtml(
-        dateEnv.toDate(dateMarker)
+        dateEnv.toDate(date)
       )
     } else if (typeof options.columnHeaderText === 'function') {
       innerText = options.columnHeaderText(
-        dateEnv.toDate(dateMarker)
+        dateEnv.toDate(date)
       )
     } else {
-      innerText = dateEnv.format(dateMarker, props.colHeadFormat)
+      innerText = dateEnv.format(date, props.colHeadFormat)
     }
 
-    // if only one row of days, the classNames on the header can represent the specific days beneath
-    if (distinctDateStr) {
-      classNames = classNames.concat(
-        // includes the day-of-week class
-        // noThemeHighlight=true (don't highlight the header)
-        getDayClasses(dateMarker, dateProfile, context, true)
-      )
-    } else {
-      classNames.push('fc-' + DAY_IDS[dateMarker.getUTCDay()]) // only add the day-of-week class
-    }
+    let dayMeta = distinctDateStr // if only one row of days, the classNames on the header can represent the specific days beneath
+      ? getDayMeta(date, props.todayRange, props.dateProfile)
+      : getDayMeta(date)
 
+    let classNames = [ 'fc-day-header' ].concat(getDayClassNames(dayMeta, context.theme))
     let attrs = {} as any
 
     if (isDateValid && distinctDateStr) {
@@ -72,7 +66,7 @@ export default class TableDateCell extends BaseComponent<TableDateCellProps> {
         {isDateValid &&
           <GotoAnchor
             navLinks={options.navLinks}
-            gotoOptions={{ date: dateMarker, forceOff: isDateValid && (!distinctDateStr || props.colCnt === 1) }}
+            gotoOptions={{ date, forceOff: isDateValid && (!distinctDateStr || props.colCnt === 1) }}
             htmlContent={innerHtml}
           >{innerText}</GotoAnchor>
         }

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

@@ -55,6 +55,10 @@ export default abstract class DateComponent<Props={}, State={}> extends BaseComp
   // -----------------------------------------------------------------------------------------------------------------
 
 
+  prepareHits() {
+  }
+
+
   queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit | null {
     return null // this should be abstract
   }
@@ -68,7 +72,7 @@ export default abstract class DateComponent<Props={}, State={}> extends BaseComp
     let dateProfile = (this.props as any).dateProfile // HACK
     let instances = interaction.mutatedEvents.instances
 
-    if (dateProfile) { // HACK for DayTile
+    if (dateProfile) { // HACK for MorePopover
       for (let instanceId in instances) {
         if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) {
           return false
@@ -84,7 +88,7 @@ export default abstract class DateComponent<Props={}, State={}> extends BaseComp
     let dateProfile = (this.props as any).dateProfile // HACK
 
     if (
-      dateProfile && // HACK for DayTile
+      dateProfile && // HACK for MorePopover
       !rangeContainsRange(dateProfile.validRange, selection.range)
     ) {
       return false
@@ -128,5 +132,5 @@ export default abstract class DateComponent<Props={}, State={}> extends BaseComp
 
 }
 
-DateComponent.prototype.fgSegSelector = '.fc-event-container > *'
-DateComponent.prototype.bgSegSelector = '.fc-bgevent:not(.fc-nonbusiness)'
+DateComponent.prototype.fgSegSelector = '.fc-event'
+DateComponent.prototype.bgSegSelector = '.fc-bgevent'

+ 80 - 32
packages/core/src/component/date-rendering.tsx

@@ -1,43 +1,91 @@
-import { DateMarker, startOfDay, addDays, DAY_IDS } from '../datelib/marker'
-import { rangeContainsMarker } from '../datelib/date-range'
-import ComponentContext from '../component/ComponentContext'
+import { DateMarker, DAY_IDS } from '../datelib/marker'
+import { rangeContainsMarker, DateRange } from '../datelib/date-range'
 import { DateProfile } from '../DateProfileGenerator'
+import Theme from '../theme/Theme'
 
 
-// Computes HTML classNames for a single-day element
-export function getDayClasses(date: DateMarker, dateProfile: DateProfile, context: ComponentContext, noThemeHighlight?) {
-  let { calendar, options, theme, dateEnv } = context
-  let classes = []
-  let todayStart: DateMarker
-  let todayEnd: DateMarker
+export interface DateMeta {
+  dow: number
+  isDisabled: boolean
+  isOther: boolean // like, is it in the non-current "other" month
+  isToday: boolean
+  isPast: boolean
+  isFuture: boolean
+}
+
 
-  if (!rangeContainsMarker(dateProfile.activeRange, date)) {
-    classes.push('fc-disabled-day')
-  } else {
-    classes.push('fc-' + DAY_IDS[date.getUTCDay()])
+export function getDateMeta(exactDate: DateMarker, todayRange?: DateRange, nowDate?: DateMarker): DateMeta { // TODO: disallow optional
+  return {
+    dow: nowDate.getUTCDay(),
+    isDisabled: false,
+    isOther: false,
+    isToday: todayRange && rangeContainsMarker(todayRange, exactDate),
+    isPast: nowDate && exactDate < nowDate,
+    isFuture: nowDate && exactDate > nowDate
+  }
+}
 
-    if (
-      options.monthMode &&
-      dateEnv.getMonth(date) !== dateEnv.getMonth(dateProfile.currentRange.start)
-    ) {
-      classes.push('fc-other-month')
-    }
 
-    todayStart = startOfDay(calendar.getNow())
-    todayEnd = addDays(todayStart, 1)
+export function getDayMeta(dayDate: DateMarker, todayRange?: DateRange, dateProfile?: DateProfile): DateMeta { // TODO: disallow optional
+  return {
+    dow: dayDate.getUTCDay(),
+    isDisabled: dateProfile && !rangeContainsMarker(dateProfile.activeRange, dayDate),
+    isOther: dateProfile && !rangeContainsMarker(dateProfile.currentRange, dayDate),
+    isToday: todayRange && dayDate.valueOf() === todayRange.start.valueOf(),
+    isPast: todayRange && dayDate < todayRange.start,
+    isFuture: todayRange && dayDate >= todayRange.end
+  }
+}
+
 
-    if (date < todayStart) {
-      classes.push('fc-past')
-    } else if (date >= todayEnd) {
-      classes.push('fc-future')
-    } else {
-      classes.push('fc-today')
+export function getDayClassNames(meta: DateMeta, theme: Theme) {
+  let classNames: string[] = [
+    'fc-day',
+    'fc-day-' + DAY_IDS[meta.dow]
+  ]
+
+  if (meta.isDisabled) { // TODO: shouldn't we avoid all other classnames if disabled?
+    classNames.push('fc-day-disabled')
+  }
+
+  if (meta.isToday) {
+    classNames.push('fc-day-today')
+    classNames.push(theme.getClass('today'))
+  }
+
+  if (meta.isPast) {
+    classNames.push('fc-day-past')
+  }
+
+  if (meta.isFuture) {
+    classNames.push('fc-day-future')
+  }
+
+  if (meta.isOther) {
+    classNames.push('fc-day-other')
+  }
+
+  return classNames
+}
+
+
+export function getSlatClassNames(meta: DateMeta, theme: Theme) {
+  let classNames: string[] = [
+    'fc-slat'
+  ]
+
+  if (meta.isToday) {
+    classNames.push('fc-slat-today')
+    classNames.push(theme.getClass('today'))
+  }
+
+  if (meta.isPast) {
+    classNames.push('fc-slat-past')
+  }
 
-      if (noThemeHighlight !== true) {
-        classes.push(theme.getClass('today'))
-      }
-    }
+  if (meta.isFuture) {
+    classNames.push('fc-slat-future')
   }
 
-  return classes
+  return classNames
 }

+ 182 - 93
packages/core/src/component/event-rendering.ts

@@ -1,13 +1,17 @@
 import { EventDef, EventTuple, EventDefHash } from '../structs/event'
 import { EventStore } from '../structs/event-store'
-import { DateRange, invertRanges, intersectRanges } from '../datelib/date-range'
+import { DateRange, invertRanges, intersectRanges, rangeContainsMarker } from '../datelib/date-range'
 import { Duration } from '../datelib/duration'
-import { computeVisibleDayRange } from '../util/misc'
+import { computeVisibleDayRange, compareByFieldSpecs } from '../util/misc'
 import { Seg } from './DateComponent'
 import EventApi from '../api/EventApi'
 import { EventUi, EventUiHash, combineEventUis } from './event-ui'
 import { mapHash } from '../util/object'
 import ComponentContext from './ComponentContext'
+import { DateFormatter } from '../datelib/formatting'
+import { DateMarker } from '../datelib/marker'
+import ViewApi from '../ViewApi'
+
 
 export interface EventRenderRange extends EventTuple {
   ui: EventUi
@@ -16,6 +20,7 @@ export interface EventRenderRange extends EventTuple {
   isEnd: boolean
 }
 
+
 /*
 Specifying nextDayThreshold signals that all-day ranges should be sliced.
 */
@@ -113,65 +118,34 @@ export function sliceEventStore(eventStore: EventStore, eventUiBases: EventUiHas
   return { bg: bgRanges, fg: fgRanges }
 }
 
+
 export function hasBgRendering(def: EventDef) {
   return def.rendering === 'background' || def.rendering === 'inverse-background'
 }
 
-export function filterSegsViaEls(context: ComponentContext, segs: Seg[], isMirror: boolean) {
-  let { calendar, view } = context
-
-  if (calendar.hasPublicHandlers('eventRender')) {
-    segs = segs.filter(function(seg) {
-      let custom = calendar.publiclyTrigger('eventRender', [
-        {
-          event: new EventApi(
-            calendar,
-            seg.eventRange.def,
-            seg.eventRange.instance
-          ),
-          isMirror,
-          isStart: seg.isStart,
-          isEnd: seg.isEnd,
-          // TODO: include seg.range once all components consistently generate it
-          el: seg.el,
-          view
-        }
-      ])
-
-      if (custom === false) { // means don't render at all
-        return false
-      } else if (custom && custom !== true) {
-        seg.el = custom
-      }
-
-      return true
-    })
-  }
 
-  for (let seg of segs) {
-    setElSeg(seg.el, seg)
-  }
-
-  return segs
-}
-
-function setElSeg(el: HTMLElement, seg: Seg) {
+export function setElSeg(el: HTMLElement, seg: Seg) {
   (el as any).fcSeg = seg
 }
 
+
 export function getElSeg(el: HTMLElement): Seg | null {
-  return (el as any).fcSeg || null
+  return (el as any).fcSeg ||
+    (el.parentNode as any).fcSeg || // for the harness
+    null
 }
 
 
 // event ui computation
 
+
 export function compileEventUis(eventDefs: EventDefHash, eventUiBases: EventUiHash) {
   return mapHash(eventDefs, function(eventDef: EventDef) {
     return compileEventUi(eventDef, eventUiBases)
   })
 }
 
+
 export function compileEventUi(eventDef: EventDef, eventUiBases: EventUiHash) {
   let uis = []
 
@@ -189,83 +163,198 @@ export function compileEventUi(eventDef: EventDef, eventUiBases: EventUiHash) {
 }
 
 
-// triggers
+export function sortEventSegs(segs, eventOrderSpecs): Seg[] {
+  let objs = segs.map(buildSegCompareObj)
+
+  objs.sort(function(obj0, obj1) {
+    return compareByFieldSpecs(obj0, obj1, eventOrderSpecs)
+  })
 
-export function triggerPositionedSegs(context: ComponentContext, segs: Seg[], isMirrors: boolean) {
-  let { calendar, view } = context
+  return objs.map(function(c) {
+    return c._seg
+  })
+}
 
-  if (calendar.hasPublicHandlers('eventPositioned')) {
 
-    for (let seg of segs) {
-      calendar.publiclyTriggerAfterSizing('eventPositioned', [
-        {
-          event: new EventApi(
-            calendar,
-            seg.eventRange.def,
-            seg.eventRange.instance
-          ),
-          isMirror: isMirrors,
-          isStart: seg.isStart,
-          isEnd: seg.isEnd,
-          el: seg.el,
-          view
-        }
-      ])
-    }
+// returns a object with all primitive props that can be compared
+export function buildSegCompareObj(seg: Seg) {
+  let { eventRange } = seg
+  let eventDef = eventRange.def
+  let range = eventRange.instance ? eventRange.instance.range : eventRange.range
+  let start = range.start ? range.start.valueOf() : 0 // TODO: better support for open-range events
+  let end = range.end ? range.end.valueOf() : 0 // "
+
+  return {
+    ...eventDef.extendedProps,
+    ...eventDef,
+    id: eventDef.publicId,
+    start,
+    end,
+    duration: end - start,
+    allDay: Number(eventDef.allDay),
+    _seg: seg // for later retrieval
   }
+}
 
-  if (!calendar.state.loadingLevel) { // avoid initial empty state while pending
-    calendar.afterSizingTriggers._eventsPositioned = [ null ] // fire once
-  }
+
+// other stuff
+
+
+export interface EventInnerContentProps { // for *InnerContent handlers
+  event: EventApi
+  timeText: string
+  isDraggable: boolean
+  isStartResizable: boolean
+  isEndResizable: boolean
+  isMirror: boolean
+  isStart: boolean
+  isEnd: boolean
+  isPast: boolean
+  isFuture: boolean
+  isToday: boolean
+  isSelected: boolean
+  isDragging: boolean
+  isResizing: boolean
+  view: ViewApi // specifically for the API
 }
 
-export function triggerWillRemoveSegs(context: ComponentContext, segs: Seg[], isMirrors: boolean) {
-  let { calendar, view } = context
 
-  for (let seg of segs) {
-    calendar.trigger('eventElRemove', seg.el)
+export function computeSegDraggable(seg: Seg, context: ComponentContext) {
+  let { pluginHooks, calendar } = context
+  let transformers = pluginHooks.isDraggableTransformers
+  let { def, ui } = seg.eventRange
+  let val = ui.startEditable
+
+  for (let transformer of transformers) {
+    val = transformer(val, def, ui, calendar)
   }
 
-  if (calendar.hasPublicHandlers('eventDestroy')) {
+  return val
+}
+
 
-    for (let seg of segs) {
-      calendar.publiclyTrigger('eventDestroy', [
+export function computeSegStartResizable(seg: Seg, context: ComponentContext) {
+  return seg.isStart && seg.eventRange.ui.durationEditable && context.options.eventResizableFromStart
+}
+
+
+export function computeSegEndResizable(seg: Seg, context: ComponentContext) {
+  return seg.isEnd && seg.eventRange.ui.durationEditable
+}
+
+
+export function buildSegTimeText(
+  seg: Seg,
+  timeFormat: DateFormatter,
+  context: ComponentContext,
+  defaultDisplayEventTime?: boolean, // defaults to true
+  defaultDisplayEventEnd?: boolean, // defaults to true
+  startOverride?: DateMarker,
+  endOverride?: DateMarker
+) {
+  let { dateEnv, options } = context
+  let { displayEventTime, displayEventEnd } = options
+  let eventDef = seg.eventRange.def
+  let eventInstance = seg.eventRange.instance
+
+  if (displayEventTime == null) { displayEventTime = defaultDisplayEventTime !== false }
+  if (displayEventEnd == null) { displayEventEnd = defaultDisplayEventEnd !== false }
+
+  if (displayEventTime && !eventDef.allDay) {
+    let { range } = eventInstance
+
+    if (displayEventEnd && eventDef.hasEnd) {
+      return dateEnv.formatRange(
+        startOverride || range.start,
+        endOverride || range.end,
+        timeFormat,
         {
-          event: new EventApi(
-            calendar,
-            seg.eventRange.def,
-            seg.eventRange.instance
-          ),
-          isMirror: isMirrors,
-          el: seg.el,
-          view
+          forcedStartTzo: startOverride ? null : eventInstance.forcedStartTzo, // nooooooooooooo, give tzo if same date
+          forcedEndTzo: endOverride ? null : eventInstance.forcedEndTzo
         }
-      ])
+      )
+
+    } else {
+      return dateEnv.format(
+        startOverride || range.start,
+        timeFormat,
+        {
+          forcedTzo: startOverride ? null : eventInstance.forcedStartTzo // nooooo, same
+        }
+      )
     }
   }
+
+  return ''
 }
 
 
-// is-interactable
+export function getSegMeta(seg: Seg, todayRange: DateRange, nowDate?: DateMarker) { // TODO: make arg order consistent with date util
+  let segRange = seg.eventRange.range
 
-export function computeEventDraggable(context: ComponentContext, eventDef: EventDef, eventUi: EventUi) {
-  let { pluginHooks, calendar } = context
-  let transformers = pluginHooks.isDraggableTransformers
-  let val = eventUi.startEditable
+  return {
+    isPast: segRange.end < (nowDate || todayRange.start),
+    isFuture: segRange.start >= (nowDate || todayRange.end),
+    isToday: todayRange && rangeContainsMarker(todayRange, segRange.start)
+  }
+}
 
-  for (let transformer of transformers) {
-    val = transformer(val, eventDef, eventUi, calendar.component.view) // yuck
+
+export function getEventClassNames(props: EventInnerContentProps) { // weird that we use this interface, but convenient
+  let classNames: string[] = [ 'fc-event' ]
+
+  if (props.isMirror) {
+    classNames.push('fc-event-mirror')
   }
 
-  return val
-}
+  if (props.isDraggable) {
+    classNames.push('fc-event-draggable')
+  }
+
+  if (props.isStartResizable || props.isEndResizable) {
+    classNames.push('fc-event-resizable')
+  }
+
+  if (props.isDragging) {
+    classNames.push('fc-event-dragging')
+  }
 
+  if (props.isResizing) {
+    classNames.push('fc-event-resizing')
+  }
+
+  if (props.isSelected) {
+    classNames.push('fc-event-selected')
+  }
+
+  if (props.isStart) {
+    classNames.push('fc-event-start')
+  }
 
-export function computeEventStartResizable(context: ComponentContext, eventDef: EventDef, eventUi: EventUi) {
-  return eventUi.durationEditable && context.options.eventResizableFromStart
+  if (props.isEnd) {
+    classNames.push('fc-event-end')
+  }
+
+  if (props.isPast) {
+    classNames.push('fc-event-past')
+  }
+
+  if (props.isToday) {
+    classNames.push('fc-event-today')
+  }
+
+  if (props.isFuture) {
+    classNames.push('fc-event-future')
+  }
+
+  return classNames
 }
 
 
-export function computeEventEndResizable(context: ComponentContext, eventDef: EventDef, eventUi: EventUi) {
-  return eventUi.durationEditable
+export function getSkinCss(ui: EventUi) {
+  return {
+    'background-color': ui.backgroundColor,
+    'border-color': ui.borderColor,
+    color: ui.textColor
+  }
 }

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

@@ -1,349 +0,0 @@
-import { DateMarker } from '../../datelib/marker'
-import { createFormatter, DateFormatter } from '../../datelib/formatting'
-import { htmlToElements } from '../../util/dom-manip'
-import { compareByFieldSpecs } from '../../util/misc'
-import { EventUi } from '../event-ui'
-import { EventRenderRange, filterSegsViaEls, triggerPositionedSegs, triggerWillRemoveSegs } from '../event-rendering'
-import { Seg } from '../DateComponent'
-import ComponentContext from '../ComponentContext'
-import { memoize } from '../../util/memoize'
-import { subrenderer, SubRenderer } from '../../vdom-util'
-
-
-export interface BaseFgEventRendererProps {
-  segs: Seg[]
-  selectedInstanceId?: string
-  hiddenInstances?: { [instanceId: string]: any }
-  isDragging: boolean
-  isResizing: boolean
-  isSelecting: boolean
-  interactingSeg?: any
-}
-
-export default abstract class FgEventRenderer<
-  FgEventRendererProps extends BaseFgEventRendererProps = BaseFgEventRendererProps
-> extends SubRenderer<FgEventRendererProps> {
-
-  private updateComputedOptions = memoize(this._updateComputedOptions)
-  private renderSegsPlain = subrenderer(this._renderSegsPlain, this._unrenderSegsPlain)
-  private renderSelectedInstance = subrenderer(renderSelectedInstance, unrenderSelectedInstance)
-  private renderHiddenInstances = subrenderer(renderHiddenInstances, unrenderHiddenInstances)
-
-  // computed options
-  protected eventTimeFormat: DateFormatter
-  protected displayEventTime: boolean
-  protected displayEventEnd: boolean
-
-
-  renderSegs(props: BaseFgEventRendererProps) {
-    this.updateComputedOptions(this.context.options)
-
-    let { segs } = this.renderSegsPlain({
-      segs: props.segs,
-      isDragging: props.isDragging,
-      isResizing: props.isResizing,
-      isSelecting: props.isSelecting
-    })
-
-    this.renderSelectedInstance({
-      segs,
-      instanceId: props.selectedInstanceId
-    })
-
-    this.renderHiddenInstances({
-      segs,
-      hiddenInstances: props.hiddenInstances
-    })
-
-    return segs
-  }
-
-
-  private _updateComputedOptions(options: any) {
-    let eventTimeFormat = createFormatter(
-      options.eventTimeFormat || this.computeEventTimeFormat(),
-      options.defaultRangeSeparator
-    )
-
-    let displayEventTime = options.displayEventTime
-    if (displayEventTime == null) {
-      displayEventTime = this.computeDisplayEventTime() // might be based off of range
-    }
-
-    let displayEventEnd = options.displayEventEnd
-    if (displayEventEnd == null) {
-      displayEventEnd = this.computeDisplayEventEnd() // might be based off of range
-    }
-
-    this.eventTimeFormat = eventTimeFormat
-    this.displayEventTime = displayEventTime
-    this.displayEventEnd = displayEventEnd
-  }
-
-
-  // doesn't worry about selection/hidden state
-  _renderSegsPlain(
-    { segs, isDragging, isResizing, isSelecting } : { segs: Seg[], isDragging: boolean, isResizing: boolean, isSelecting: boolean },
-    context: ComponentContext
-  ) {
-    segs = this.renderSegEls(segs, isDragging, isResizing, isSelecting) // returns a subset!
-
-    let isMirror = isDragging || isResizing || isSelecting
-    triggerPositionedSegs(context, segs, isMirror)
-
-    return { segs, isMirror }
-  }
-
-
-  _unrenderSegsPlain({ segs, isMirror }: { segs: Seg[], isMirror: boolean }, context: ComponentContext) {
-    triggerWillRemoveSegs(context, segs, isMirror)
-  }
-
-
-  // Renders and assigns an `el` property for each foreground event segment.
-  // Only returns segments that successfully rendered.
-  renderSegEls(segs: Seg[], isDragging: boolean, isResizing: boolean, isSelecting: boolean) {
-    let html = ''
-    let i
-
-    if (segs.length) { // don't build an empty html string
-
-      // build a large concatenation of event segment HTML
-      for (i = 0; i < segs.length; i++) {
-        html += this.renderSegHtml(segs[i], isDragging, isResizing, isSelecting)
-      }
-
-      // Grab individual elements from the combined HTML string. Use each as the default rendering.
-      // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
-      htmlToElements(html).forEach((el, i) => {
-        let seg = segs[i]
-
-        if (el) {
-          seg.el = el
-        }
-      })
-
-      segs = filterSegsViaEls(this.context, segs, isDragging || isResizing || isSelecting)
-    }
-
-    return segs
-  }
-
-
-  abstract renderSegHtml(seg: Seg, isDragging: boolean, isResizing: boolean, isSelecting: boolean): string
-
-
-  // Generic utility for generating the HTML classNames for an event segment's element
-  // TODO: move to outside func
-  getSegClasses(seg: Seg, isDraggable, isResizable, isDragging: boolean, isResizing: boolean, isSelecting: boolean) {
-    let classes = [
-      'fc-event',
-      seg.isStart ? 'fc-start' : 'fc-not-start',
-      seg.isEnd ? 'fc-end' : 'fc-not-end'
-    ].concat(seg.eventRange.ui.classNames)
-
-    if (isDraggable) {
-      classes.push('fc-draggable')
-    }
-
-    if (isResizable) {
-      classes.push('fc-resizable')
-    }
-
-    if (isDragging || isResizing || isSelecting) {
-      classes.push('fc-mirror')
-
-      if (isDragging) {
-        classes.push('fc-dragging')
-      }
-
-      if (isResizing) {
-        classes.push('fc-resizing')
-      }
-    }
-
-    return classes
-  }
-
-
-  // Compute the text that should be displayed on an event's element.
-  // `range` can be the Event object itself, or something range-like, with at least a `start`.
-  // If event times are disabled, or the event has no time, will return a blank string.
-  // If not specified, formatter will default to the eventTimeFormat setting,
-  // and displayEnd will default to the displayEventEnd setting.
-  getTimeText(eventRange: EventRenderRange, formatter?, displayEnd?) {
-    let { def, instance } = eventRange
-
-    return this._getTimeText(
-      instance.range.start,
-      def.hasEnd ? instance.range.end : null,
-      def.allDay,
-      formatter,
-      displayEnd,
-      instance.forcedStartTzo,
-      instance.forcedEndTzo
-    )
-  }
-
-
-  _getTimeText(
-    start: DateMarker,
-    end: DateMarker,
-    allDay,
-    formatter?,
-    displayEnd?,
-    forcedStartTzo?: number,
-    forcedEndTzo?: number
-) {
-    let { dateEnv } = this.context
-
-    if (formatter == null) {
-      formatter = this.eventTimeFormat
-    }
-
-    if (displayEnd == null) {
-      displayEnd = this.displayEventEnd
-    }
-
-    if (this.displayEventTime && !allDay) {
-      if (displayEnd && end) {
-        return dateEnv.formatRange(start, end, formatter, {
-          forcedStartTzo,
-          forcedEndTzo
-        })
-      } else {
-        return dateEnv.format(start, formatter, {
-          forcedTzo: forcedStartTzo
-        })
-      }
-    }
-
-    return ''
-  }
-
-
-  computeEventTimeFormat(): any {
-    return {
-      hour: 'numeric',
-      minute: '2-digit',
-      omitZeroMinute: true
-    }
-  }
-
-
-  computeDisplayEventTime() {
-    return true
-  }
-
-
-  computeDisplayEventEnd() {
-    return true
-  }
-
-
-  // Utility for generating event skin-related CSS properties
-  // TODO: move to outside func
-  getSkinCss(ui: EventUi) {
-    return {
-      'background-color': ui.backgroundColor,
-      'border-color': ui.borderColor,
-      color: ui.textColor
-    }
-  }
-
-}
-
-
-// Manipulation on rendered segs
-// ----------------------------------------------------------------------------------------------------
-// TODO: slow. use more hashes to quickly reference relevant elements
-
-
-function renderHiddenInstances(props: { segs: Seg[], hiddenInstances: { [instanceId: string]: any } }) {
-  let { segs, hiddenInstances } = props
-
-  if (hiddenInstances) {
-    for (let seg of segs) {
-      if (hiddenInstances[seg.eventRange.instance.instanceId]) {
-        seg.el.style.visibility = 'hidden'
-      }
-    }
-  }
-
-  return props
-}
-
-
-function unrenderHiddenInstances({ segs, hiddenInstances }: { segs: Seg[], hiddenInstances: { [instanceId: string]: any } }) {
-  if (hiddenInstances) {
-    for (let seg of segs) {
-      if (hiddenInstances[seg.eventRange.instance.instanceId]) {
-        seg.el.style.visibility = ''
-      }
-    }
-  }
-}
-
-
-function renderSelectedInstance(props: { segs: Seg[], instanceId: string }) {
-  let { segs, instanceId } = props
-
-  if (instanceId) {
-    for (let seg of segs) {
-      let eventInstance = seg.eventRange.instance
-      if (
-        eventInstance && eventInstance.instanceId === instanceId &&
-        seg.el // necessary?
-      ) {
-        seg.el.classList.add('fc-selected')
-      }
-    }
-  }
-
-  return props
-}
-
-
-function unrenderSelectedInstance({ segs, instanceId }: { segs: Seg[], instanceId: string }) {
-  if (instanceId) {
-    for (let seg of segs) {
-      if (seg.el) { // necessary?
-        seg.el.classList.remove('fc-selected')
-      }
-    }
-  }
-}
-
-
-export function sortEventSegs(segs, eventOrderSpecs): Seg[] {
-  let objs = segs.map(buildSegCompareObj)
-
-  objs.sort(function(obj0, obj1) {
-    return compareByFieldSpecs(obj0, obj1, eventOrderSpecs)
-  })
-
-  return objs.map(function(c) {
-    return c._seg
-  })
-}
-
-
-// returns a object with all primitive props that can be compared
-export function buildSegCompareObj(seg: Seg) {
-  let { eventRange } = seg
-  let eventDef = eventRange.def
-  let range = eventRange.instance ? eventRange.instance.range : eventRange.range
-  let start = range.start ? range.start.valueOf() : 0 // TODO: better support for open-range events
-  let end = range.end ? range.end.valueOf() : 0 // "
-
-  return {
-    ...eventDef.extendedProps,
-    ...eventDef,
-    id: eventDef.publicId,
-    start,
-    end,
-    duration: end - start,
-    allDay: Number(eventDef.allDay),
-    _seg: seg // for later retrieval
-  }
-}

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

@@ -1,111 +0,0 @@
-import { cssToStr } from '../../util/html'
-import { htmlToElements, elementMatches } from '../../util/dom-manip'
-import { Seg } from '../DateComponent'
-import { filterSegsViaEls, triggerPositionedSegs, triggerWillRemoveSegs } from '../event-rendering'
-import ComponentContext from '../ComponentContext'
-import { SubRenderer, subrenderer } from '../../vdom-util'
-
-export interface BaseFillRendererProps {
-  segs: Seg[]
-  type: string
-}
-
-// use for highlight, background events, business hours
-export default abstract class FillRenderer<FillRendererProps extends BaseFillRendererProps> extends SubRenderer<FillRendererProps> {
-
-  renderSegs = subrenderer(this._renderSegs, this._unrenderSegs)
-
-  fillSegTag: string = 'div'
-
-
-  _renderSegs({ segs, type }: BaseFillRendererProps, context: ComponentContext) {
-
-    // assignes `.el` to each seg. returns successfully rendered segs, a SUBSET
-    segs = this.renderSegEls(segs, type)
-
-    if (type === 'bgEvent') {
-      triggerPositionedSegs(context, segs, false) // isMirror=false
-    }
-
-    return segs
-  }
-
-
-  // Unrenders a specific type of fill that is currently rendered on the grid
-  _unrenderSegs(segs: Seg[], context: ComponentContext) {
-    if (this.props.type === 'bgEvent') {
-      triggerWillRemoveSegs(context, segs, false) // isMirror=false
-    }
-  }
-
-
-  // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
-  // Only returns segments that successfully rendered.
-  renderSegEls(segs: Seg[], type: string) {
-    let html = ''
-    let i
-
-    if (segs.length) {
-
-      // build a large concatenation of segment HTML
-      for (i = 0; i < segs.length; i++) {
-        html += this.renderSegHtml(segs[i])
-      }
-
-      // Grab individual elements from the combined HTML string. Use each as the default rendering.
-      // Then, compute the 'el' for each segment.
-      htmlToElements(html).forEach((el, i) => {
-        let seg = segs[i]
-
-        if (el) {
-          seg.el = el
-        }
-      })
-
-      if (type === 'bgEvent') {
-        segs = filterSegsViaEls(
-          this.context,
-          segs,
-          false // isMirror. background events can never be mirror elements
-        )
-      }
-
-      // correct element type? (would be bad if a non-TD were inserted into a table for example)
-      segs = segs.filter((seg) => {
-        return elementMatches(seg.el, this.fillSegTag)
-      })
-    }
-
-    return segs
-  }
-
-
-  // Builds the HTML needed for one fill segment. Generic enough to work with different types.
-  renderSegHtml(seg: Seg) {
-    let { type } = this.props
-    let css = null
-    let classNames = []
-
-    if (type !== 'highlight' && type !== 'businessHours') {
-      css = {
-        'background-color': seg.eventRange.ui.backgroundColor
-      }
-    }
-
-    if (type !== 'highlight') {
-      classNames = classNames.concat(seg.eventRange.ui.classNames)
-    }
-
-    if (type === 'businessHours') {
-      classNames.push('fc-bgevent')
-    } else {
-      classNames.push('fc-' + type.toLowerCase())
-    }
-
-    return '<' + this.fillSegTag +
-      (classNames.length ? ' class="' + classNames.join(' ') + '"' : '') +
-      (css ? ' style="' + cssToStr(css) + '"' : '') +
-      '></' + this.fillSegTag + '>'
-  }
-
-}

+ 3 - 1
packages/core/src/datelib/env.ts

@@ -112,12 +112,14 @@ export class DateEnv {
 
     if (typeof input === 'number') {
       marker = this.timestampToMarker(input)
+
     } else if (input instanceof Date) {
       input = input.valueOf()
 
       if (!isNaN(input)) {
         marker = this.timestampToMarker(input)
       }
+
     } else if (Array.isArray(input)) {
       marker = arrayToUtcDate(input)
     }
@@ -299,8 +301,8 @@ export class DateEnv {
     return (m1.valueOf() - m0.valueOf()) / asRoughMs(d)
   }
 
-
   // Start-Of
+  // these DON'T return zoned-dates. only UTC start-of dates
 
   startOf(m: DateMarker, unit: string) {
     if (unit === 'year') {

+ 2 - 2
packages/core/src/datelib/formatting.ts

@@ -41,8 +41,8 @@ export interface DateFormattingContext {
 }
 
 export interface DateFormatter {
-  format(date: ZonedMarker, context: DateFormattingContext)
-  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext)
+  format(date: ZonedMarker, context: DateFormattingContext): string
+  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext): string
 }
 
 // TODO: use Intl.DateTimeFormatOptions

+ 1 - 2
packages/core/src/interactions/event-dragging.ts

@@ -3,8 +3,7 @@ import { EventMutation } from '../structs/event-mutation'
 import { Hit } from './hit'
 import { EventDef } from '../structs/event'
 import { EventUi } from '../component/event-ui'
-import View from '../View'
 
 export type eventDragMutationMassager = (mutation: EventMutation, hit0: Hit, hit1: Hit) => void
 export type EventDropTransformers = (mutation: EventMutation, calendar: Calendar) => any
-export type eventIsDraggableTransformer = (val: boolean, eventDef: EventDef, eventUi: EventUi, view: View) => boolean
+export type eventIsDraggableTransformer = (val: boolean, eventDef: EventDef, eventUi: EventUi, Calendar: Calendar) => boolean

+ 23 - 22
packages/core/src/main.ts

@@ -32,17 +32,12 @@ export {
   computeSmallestCellWidth
 } from './util/misc'
 
-export {
-  htmlEscape,
-  cssToStr
-} from './util/html'
-
 export {
   removeExact,
   isArraysEqual
 } from './util/array'
 
-export { memoize, memoizeParallel } from './util/memoize'
+export { memoize, memoizeArraylike, memoizeHashlike } from './util/memoize'
 
 export {
   intersectRects,
@@ -52,28 +47,23 @@ export {
   translateRect
 } from './util/geom'
 
-export { mapHash, filterHash, isPropsEqual, compareObjs } from './util/object'
+export { mapHash, filterHash, isPropsEqual, compareObjs, buildHashFromArray, collectFromHash } from './util/object'
 
 export {
   findElements,
   findDirectChildren,
   htmlToElement,
-  htmlToElements,
-  insertAfterElement,
-  prependToElement,
   removeElement,
-  appendToElement,
   applyStyle,
   applyStyleProp,
   elementMatches,
-  elementClosest,
-  forceClassName
+  elementClosest
 } from './util/dom-manip'
 
 export { EventStore, filterEventStoreDefs, createEmptyEventStore, mergeEventStores, getRelevantEvents, eventTupleToStore } from './structs/event-store'
 export { EventUiHash, EventUi, processScopedUiProps, combineEventUis } from './component/event-ui'
 export { default as Splitter, SplittableProps } from './component/event-splitting'
-export { getDayClasses } from './component/date-rendering'
+export { getDayClassNames, getDateMeta, getDayMeta, getSlatClassNames, DateMeta } from './component/date-rendering'
 export { default as GotoAnchor } from './component/GotoAnchor'
 
 export {
@@ -103,8 +93,6 @@ export { default as DateComponent, Seg, EventSegUiInteractionState } from './com
 export { default as Calendar, DatePointTransform, DateSpanTransform, DateSelectionApi } from './Calendar'
 export { default as View, ViewProps, getViewClassNames } from './View'
 export { default as ViewApi } from './ViewApi'
-export { default as FgEventRenderer, buildSegCompareObj, BaseFgEventRendererProps, sortEventSegs } from './component/renderers/FgEventRenderer'
-export { default as FillRenderer, BaseFillRendererProps } from './component/renderers/FillRenderer'
 
 export { default as DateProfileGenerator, DateProfile } from './DateProfileGenerator'
 export { ViewDef } from './structs/view-def'
@@ -147,7 +135,7 @@ export { RecurringType, ParsedRecurring } from './structs/recurring-event'
 
 export { DragMetaInput, DragMeta, parseDragMeta } from './structs/drag-meta'
 
-export { createPlugin, PluginDef, PluginDefInput, ViewPropsTransformer, ViewContainerModifier } from './plugin-system'
+export { createPlugin, PluginDef, PluginDefInput, ViewPropsTransformer, ViewContainerAppend } from './plugin-system'
 export { reducerFunc, Action, CalendarState } from './reducers/types'
 export { CalendarComponentProps } from './CalendarComponent'
 
@@ -158,7 +146,13 @@ export { default as TableDateCell } from './common/TableDateCell'
 export { default as DaySeries } from './common/DaySeriesModel'
 
 export { EventInteractionState } from './interactions/event-interaction-state'
-export { EventRenderRange, sliceEventStore, hasBgRendering, getElSeg, computeEventDraggable, computeEventStartResizable, computeEventEndResizable } from './component/event-rendering'
+export {
+  EventRenderRange, sliceEventStore, hasBgRendering, setElSeg, getElSeg,
+  computeSegDraggable, computeSegStartResizable, computeSegEndResizable,
+  EventInnerContentProps, getEventClassNames, buildSegTimeText,
+  buildSegCompareObj, sortEventSegs,
+  getSegMeta
+} from './component/event-rendering'
 
 export { default as DayTableModel, DayTableSeg, DayTableCell } from './common/DayTableModel'
 
@@ -171,24 +165,31 @@ export { default as EventApi } from './api/EventApi'
 export { default as requestJson } from './util/requestJson'
 
 export * from './vdom'
-export { subrenderer, SubRenderer, BaseComponent, setRef, renderVNodes, buildMapSubRenderer } from './vdom-util'
+export { BaseComponent, setRef } from './vdom-util'
 export { DelayedRunner } from './util/runner'
 
 export { default as SimpleScrollGrid, SimpleScrollGridSection } from './scrollgrid/SimpleScrollGrid'
 export {
   CssDimValue, ScrollerLike, SectionConfig, ColProps, ChunkConfig, hasShrinkWidth, renderMicroColGroup,
-  getScrollGridClassNames, getSectionClassNames, getChunkVGrow, getAllowYScrolling, renderChunkContent, computeForceScrollbars, computeShrinkWidth,
+  getScrollGridClassNames, getSectionClassNames, getChunkVGrow, getAllowYScrolling, renderChunkContent, computeShrinkWidth,
   getChunkClassNames, ChunkContentCallbackArgs,
-  computeScrollerClientWidths, computeScrollerClientHeights,
+  CLIENT_HEIGHT_WIGGLE,
   sanitizeShrinkWidth,
   ChunkConfigRowContent, ChunkConfigContent,
   isColPropsEqual
 } from './scrollgrid/util'
+export { getCanVGrowWithinCell} from './scrollgrid/table-styling'
 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'
 
-export { default as NowTimer, NowTimerCallback } from './NowTimer'
+export { default as NowTimer } from './NowTimer'
 export { default as ScrollResponder, ScrollRequest } from './ScrollResponder'
 export { globalPlugins } from './global-plugins'
+export {
+  MountHook, MountHookProps,
+  ClassNamesHook, ClassNamesHookProps,
+  InnerContentHook, InnerContentHookProps
+} from './render-hook'
+export { default as StandardEvent, StandardEventProps, MinimalEventProps } from './StandardEvent'

+ 7 - 6
packages/core/src/plugin-system.ts

@@ -19,6 +19,7 @@ import { RecurringType } from './structs/recurring-event'
 import { NamedTimeZoneImplClass } from './datelib/timezone'
 import { ElementDraggingClass } from './interactions/ElementDragging'
 import { guid } from './util/misc'
+import { ComponentChildren } from './vdom'
 
 // TODO: easier way to add new hooks? need to update a million things
 
@@ -37,7 +38,7 @@ export interface PluginDefInput {
   isPropsValid?: isPropsValidTester
   externalDefTransforms?: ExternalDefTransform[]
   eventResizeJoinTransforms?: EventResizeJoinTransforms[]
-  viewContainerModifiers?: ViewContainerModifier[]
+  viewContainerAppends?: ViewContainerAppend[]
   eventDropTransformers?: EventDropTransformers[]
   componentInteractions?: InteractionClass[]
   calendarInteractions?: CalendarInteractionClass[]
@@ -65,7 +66,7 @@ export interface PluginHooks {
   isPropsValid: isPropsValidTester | null
   externalDefTransforms: ExternalDefTransform[]
   eventResizeJoinTransforms: EventResizeJoinTransforms[]
-  viewContainerModifiers: ViewContainerModifier[]
+  viewContainerAppends: ViewContainerAppend[]
   eventDropTransformers: EventDropTransformers[]
   componentInteractions: InteractionClass[]
   calendarInteractions: CalendarInteractionClass[]
@@ -90,7 +91,7 @@ export interface ViewPropsTransformer {
   transform(viewProps: ViewProps, viewSpec: ViewSpec, calendarProps: CalendarComponentProps, allOptions: any): any
 }
 
-export type ViewContainerModifier = (contentEl: HTMLElement, calendar: Calendar) => void
+export type ViewContainerAppend = (calendar: Calendar) => ComponentChildren
 
 
 export function createPlugin(input: PluginDefInput): PluginDef {
@@ -110,7 +111,7 @@ export function createPlugin(input: PluginDefInput): PluginDef {
     isPropsValid: input.isPropsValid || null,
     externalDefTransforms: input.externalDefTransforms || [],
     eventResizeJoinTransforms: input.eventResizeJoinTransforms || [],
-    viewContainerModifiers: input.viewContainerModifiers || [],
+    viewContainerAppends: input.viewContainerAppends || [],
     eventDropTransformers: input.eventDropTransformers || [],
     componentInteractions: input.componentInteractions || [],
     calendarInteractions: input.calendarInteractions || [],
@@ -145,7 +146,7 @@ export class PluginSystem {
       isPropsValid: null,
       externalDefTransforms: [],
       eventResizeJoinTransforms: [],
-      viewContainerModifiers: [],
+      viewContainerAppends: [],
       eventDropTransformers: [],
       componentInteractions: [],
       calendarInteractions: [],
@@ -190,7 +191,7 @@ function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
     isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
     externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
     eventResizeJoinTransforms: hooks0.eventResizeJoinTransforms.concat(hooks1.eventResizeJoinTransforms),
-    viewContainerModifiers: hooks0.viewContainerModifiers.concat(hooks1.viewContainerModifiers),
+    viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends),
     eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
     calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
     componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),

+ 196 - 0
packages/core/src/render-hook.ts

@@ -0,0 +1,196 @@
+import { Component, VNode, Ref, createRef } from './vdom'
+import ComponentContext, { ComponentContextType } from './component/ComponentContext'
+
+
+export interface MountHookProps<HandlerProps> {
+  name: string // TODO: rename to entity or something
+  handlerProps: HandlerProps // TODO: these props should not be very dynamic!
+  content: (rootElRef: Ref<any>) => VNode
+}
+
+
+export class MountHook<HandlerProps> extends Component<MountHookProps<HandlerProps>> {
+
+  static contextType = ComponentContextType
+
+  private rootElRef = createRef()
+
+
+  render(props: MountHookProps<HandlerProps>) {
+    return props.content(this.rootElRef)
+  }
+
+
+  componentDidMount() {
+    this.triggerLifecycle('DidMount')
+  }
+
+
+  componentWillUnmount() {
+    this.triggerLifecycle('WillUnmount')
+  }
+
+
+  private triggerLifecycle(baseName: string) {
+    let handler = this.context.options[this.props.name + baseName]
+
+    if (handler) {
+      handler({ // TODO: make a better type for this
+        ...this.props.handlerProps,
+        el: this.rootElRef.current
+      })
+    }
+  }
+
+}
+
+
+export interface ClassNamesHookProps<HandlerProps> {
+  name: string
+  handlerProps: HandlerProps
+  content: (classNames: string[]) => VNode
+}
+
+
+export class ClassNamesHook<HandlerProps> extends Component<ClassNamesHookProps<HandlerProps>> {
+
+  static contextType = ComponentContextType
+
+
+  render(props: ClassNamesHookProps<HandlerProps>, state: {}, context: ComponentContext) {
+    let { options } = context
+    let generateClassNames = options[props.name + 'ClassNames']
+    let classNames: string[]
+
+    if (typeof generateClassNames === 'function') {
+      classNames = generateClassNames(props.handlerProps)
+
+    } else if (Array.isArray(generateClassNames)) {
+      classNames = generateClassNames
+
+    } else {
+      classNames = []
+    }
+
+    return props.content(classNames)
+  }
+
+}
+
+
+export interface InnerContentHookProps<InnerProps> {
+  name: string
+  innerProps: InnerProps
+  defaultInnerContent?: (innerProps: InnerProps) => VNode
+  outerContent: (innerContentParentRef: Ref<any>, innerContent: VNode, anySpecified: boolean) => VNode
+}
+
+
+export class InnerContentHook<InnerProps> extends Component<InnerContentHookProps<InnerProps>> {
+
+  static contextType = ComponentContextType
+
+  private customContentHandler: ContentHandler<any>
+
+
+  render(props: InnerContentHookProps<InnerProps>, state: {}, context: ComponentContext) {
+    let { options } = context
+    let renderInner = options[props.name + 'InnerContent'] || props.defaultInnerContent
+    let innerContentVNode: VNode
+
+    if (renderInner) {
+      let innerContentRaw = renderInner(props.innerProps)
+
+      if ((innerContentRaw as VNode).type) {
+        innerContentVNode = (innerContentRaw as VNode)
+
+      } else if (this.customContentHandler) {
+        this.customContentHandler.handleProps(innerContentRaw)
+
+      } else if (typeof innerContentRaw === 'string') {
+        this.customContentHandler = new HtmlContentHandler(innerContentRaw)
+
+      } else if (
+        innerContentRaw instanceof HTMLElement ||
+        typeof (innerContentRaw as NodeList | HTMLElement[]).length === 'number'
+      ) {
+        this.customContentHandler = new DomContentHandler(innerContentRaw as DomMeta)
+      }
+    }
+
+    return props.outerContent(this.handleInnerContentParent, innerContentVNode, Boolean(renderInner))
+  }
+
+
+  handleInnerContentParent = (parentEl: HTMLElement | null) => {
+    if (this.customContentHandler) {
+      this.customContentHandler.handleEl(parentEl)
+    }
+  }
+
+}
+
+
+// TODO: allow for returning a "string" that will be Preact-escaped text???
+// look at Preact's ComponentChild interface for what it could be
+
+
+abstract class ContentHandler<RenderMeta> {
+
+  private el: HTMLElement
+
+  constructor(private meta: RenderMeta) {
+  }
+
+  handleProps(meta: RenderMeta) {
+    this.meta = meta
+  }
+
+  handleEl(el: HTMLElement) {
+    this.render(el, this.meta, this.el !== el)
+    this.el = el
+  }
+
+  abstract render(el: HTMLElement, meta: RenderMeta, isInitial: boolean)
+
+}
+
+
+class HtmlContentHandler extends ContentHandler<string> {
+
+  render(el: HTMLElement, meta: string) {
+    el.innerHTML = meta
+  }
+
+}
+
+
+type DomMeta = HTMLElement | HTMLElement[] | NodeList
+
+class DomContentHandler extends ContentHandler<DomMeta> {
+
+  render(el: HTMLElement, meta: DomMeta) {
+    removeAllChildren(el)
+
+    let length = (meta as HTMLElement[] | NodeList).length
+
+    if (length != undefined) {
+      for (let i = 0; i < length; i++) {
+        el.appendChild(meta[i])
+      }
+
+    } else if (meta) {
+      el.appendChild(meta as HTMLElement)
+    }
+  }
+
+}
+
+
+function removeAllChildren(parentEl: HTMLElement) { // TODO: move to util file
+  let { childNodes } = parentEl
+
+  while (childNodes.length) {
+    parentEl.removeChild(childNodes[0])
+  }
+}

+ 72 - 46
packages/core/src/scrollgrid/SimpleScrollGrid.tsx

@@ -3,13 +3,15 @@ import ComponentContext from '../component/ComponentContext'
 import { BaseComponent, setRef } from '../vdom-util'
 import Scroller, { OverflowValue } from './Scroller'
 import RefMap from '../util/RefMap'
-import { ColProps, SectionConfig, renderMicroColGroup, computeShrinkWidth, getScrollGridClassNames, getSectionClassNames, getAllowYScrolling,
-  renderChunkContent, getChunkVGrow, computeForceScrollbars, ChunkConfig, hasShrinkWidth, CssDimValue, getChunkClassNames, computeScrollerClientWidths, computeScrollerClientHeights,
+import {
+  ColProps, SectionConfig, renderMicroColGroup, computeShrinkWidth, getScrollGridClassNames, getSectionClassNames, getAllowYScrolling,
+  renderChunkContent, getChunkVGrow, ChunkConfig, hasShrinkWidth, CssDimValue, getChunkClassNames,
   isColPropsEqual
-  } from './util'
+} from './util'
 import { memoize } from '../util/memoize'
 import { isPropsEqual } from '../util/object'
-import { guid } from '../util/misc'
+import { getScrollbarWidths } from '../util/scrollbar-width'
+import { getCanVGrowWithinCell } from './table-styling'
 
 
 export interface SimpleScrollGridProps {
@@ -27,10 +29,9 @@ export interface SimpleScrollGridSection extends SectionConfig {
 
 interface SimpleScrollGridState {
   shrinkWidth: number | null
-  forceYScrollbars: boolean | null
-  scrollerClientWidths: { [index: string]: number }
+  forceYScrollbars: boolean
+  scrollerClientWidths: { [index: string]: number } // why not use array?
   scrollerClientHeights: { [index: string]: number }
-  sizingId: string
 }
 
 
@@ -39,14 +40,13 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
   processCols = memoize((a) => a, isColPropsEqual) // so we get same `cols` props every time
   renderMicroColGroup = memoize(renderMicroColGroup) // yucky to memoize VNodes, but much more efficient for consumers
   scrollerRefs = new RefMap<Scroller>()
-  scrollerElRefs = new RefMap<HTMLElement, [ChunkConfig]>(this._handleScrollerEl.bind(this))
+  scrollerElRefs = new RefMap<HTMLElement>(this._handleScrollerEl.bind(this))
 
   state: SimpleScrollGridState = {
     shrinkWidth: null,
-    forceYScrollbars: null,
+    forceYScrollbars: false,
     scrollerClientWidths: {},
-    scrollerClientHeights: {},
-    sizingId: ''
+    scrollerClientHeights: {}
   }
 
 
@@ -63,6 +63,10 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
       classNames.push('scrollgrid--forprint')
     }
 
+    if (!getCanVGrowWithinCell()) {
+      classNames.push('scrollgrid-vgrow-cell-hack')
+    }
+
     return (
       <table class={classNames.join(' ')} style={{ height: props.height }}>
         {sectionConfigs.map((sectionConfig, sectionI) => this.renderSection(sectionConfig, sectionI, microColGroupNode))}
@@ -97,8 +101,8 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
     let vGrow = getChunkVGrow(this.props, sectionConfig, chunkConfig)
 
     let overflowY: OverflowValue =
-      state.forceYScrollbars === true ? 'scroll' :
-      (state.forceYScrollbars === false || !needsYScrolling) ? 'hidden' :
+      state.forceYScrollbars ? 'scroll' :
+      !needsYScrolling ? 'hidden' :
       'auto'
 
     let content = renderChunkContent(sectionConfig, chunkConfig, {
@@ -107,7 +111,7 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
       clientWidth: state.scrollerClientWidths[sectionI] || '',
       clientHeight: state.scrollerClientHeights[sectionI] || '',
       vGrowRows: sectionConfig.vGrowRows || chunkConfig.vGrowRows,
-      rowSyncHeights: []
+      rowSyncHeights: {}
     })
 
     return (
@@ -115,7 +119,7 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
         <div class={'scrollerharness' + (vGrow ? ' vgrow' : '')}>
           <Scroller
             ref={this.scrollerRefs.createRef(sectionI)}
-            elRef={this.scrollerElRefs.createRef(sectionI, chunkConfig)}
+            elRef={this.scrollerElRefs.createRef(sectionI)}
             overflowY={overflowY}
             overflowX='hidden'
             maxHeight={sectionConfig.maxHeight}
@@ -127,20 +131,34 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
   }
 
 
-  _handleScrollerEl(scrollerEl: HTMLElement | null, key: string, chunkConfig: ChunkConfig) {
+  _handleScrollerEl(scrollerEl: HTMLElement | null, key: string) {
+    let sectionI = parseInt(key, 10)
+    let chunkConfig = this.props.sections[sectionI].chunk
+
     setRef(chunkConfig.scrollerElRef, scrollerEl)
   }
 
 
+  // TODO: can do a really simple print-view. dont need to join rows
+  handleSizing = () => {
+    if (!this.props.forPrint) {
+      this.setState({
+        shrinkWidth: this.computeShrinkWidth(), // will create each chunk's <colgroup>. TODO: precompute hasShrinkWidth
+        ...this.computeScrollerDims()
+      })
+    }
+  }
+
+
   componentDidMount() {
-    this.handleSizing(true)
+    this.handleSizing()
     this.context.addResizeHandler(this.handleSizing)
   }
 
 
-  componentDidUpdate(prevProps: SimpleScrollGridProps) {
+  componentDidUpdate() {
     // TODO: need better solution when state contains non-sizing things
-    this.handleSizing(!isPropsEqual(this.props, prevProps))
+    this.handleSizing()
   }
 
 
@@ -149,39 +167,47 @@ export default class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProp
   }
 
 
-  handleSizing = (isExternalChange: boolean) => {
-    if (isExternalChange && !this.props.forPrint) {
-      let sizingId = guid()
-      this.setState({
-        sizingId,
-        shrinkWidth: // will create each chunk's <colgroup>. TODO: precompute hasShrinkWidth
-          hasShrinkWidth(this.props.cols)
-            ? 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({ // will set each chunk's clientWidth
-                scrollerClientWidths: computeScrollerClientWidths(this.scrollerElRefs),
-                scrollerClientHeights: computeScrollerClientHeights(this.scrollerElRefs)
-              })
-            }
-          })
-        }
-      })
-    }
+  computeShrinkWidth() {
+    return hasShrinkWidth(this.props.cols)
+      ? computeShrinkWidth(this.scrollerElRefs.getAll())
+      : 0
   }
 
 
-  // TODO: can do a really simple print-view. dont need to join rows
+  computeScrollerDims() {
+    let scrollbarWidth = getScrollbarWidths()
+    let sectionCnt = this.props.sections.length
+    let { scrollerRefs, scrollerElRefs } = this
+
+    let forceYScrollbars = false
+    let scrollerClientWidths: { [index: string]: number } = {}
+    let scrollerClientHeights: { [index: string]: number } = {}
+
+    for (let sectionI = 0; sectionI < sectionCnt; sectionI++) { // along edge
+      let scroller = scrollerRefs.currentMap[sectionI]
+
+      if (scroller && scroller.needsYScrolling()) {
+        forceYScrollbars = true
+        break
+      }
+    }
+
+    for (let sectionI = 0; sectionI < sectionCnt; sectionI++) { // along edge
+      let scrollerEl = scrollerElRefs.currentMap[sectionI]
+
+      if (scrollerEl) {
+        scrollerClientWidths[sectionI] = scrollerEl.offsetWidth - (forceYScrollbars ? scrollbarWidth.y : 0)
+        scrollerClientHeights[sectionI] = scrollerEl.offsetHeight
+        // TODO: need IE wiggle?
+      }
+    }
+
+    return { forceYScrollbars, scrollerClientWidths, scrollerClientHeights }
+  }
 
 }
 
 SimpleScrollGrid.addStateEquality({
   scrollerClientWidths: isPropsEqual,
-  scrollerClientHeights: isPropsEqual,
-  sizingId: true // never update base on this
+  scrollerClientHeights: isPropsEqual
 })

+ 25 - 0
packages/core/src/scrollgrid/table-styling.ts

@@ -0,0 +1,25 @@
+
+let canVGrowWithinCell: boolean
+
+
+export function getCanVGrowWithinCell() {
+  if (canVGrowWithinCell == null) {
+    canVGrowWithinCell = computeCanVGrowWithinCell()
+  }
+  return canVGrowWithinCell
+}
+
+
+function computeCanVGrowWithinCell() {
+  // TODO: abstraction for creating these temporary detection-based els
+  var el = document.createElement('div')
+  el.style.position = 'absolute' // for not interfering with current layout
+  el.style.top = '0'
+  el.style.left = '0'
+  el.innerHTML = '<table style="height:100px"><tr><td><div style="height:100%"></div></td></tr></table>'
+  document.body.appendChild(el)
+  let div = el.querySelector('div')
+  let possible = div.offsetHeight > 0
+  document.body.removeChild(el)
+  return possible
+}

+ 4 - 28
packages/core/src/scrollgrid/util.tsx

@@ -2,8 +2,7 @@ import { VNode, h, Ref } from '../vdom'
 import { findElements } from '../util/dom-manip'
 import ComponentContext from '../component/ComponentContext'
 import { computeSmallestCellWidth } from '../util/misc'
-import { mapHash, isPropsEqual } from '../util/object'
-import RefMap from '../util/RefMap'
+import { isPropsEqual } from '../util/object'
 import { isArraysEqual } from '../util/array'
 
 
@@ -32,7 +31,6 @@ export interface ChunkConfig {
   outerContent?: VNode
   content?: ChunkConfigContent
   rowContent?: ChunkConfigRowContent
-  rowSelector?: string
   vGrowRows?: boolean
   scrollerElRef?: Ref<HTMLDivElement>
   elRef?: Ref<HTMLTableCellElement>
@@ -45,7 +43,8 @@ export interface ChunkContentCallbackArgs { // TODO: util for wrapping tables!?
   clientWidth: CssDimValue
   clientHeight: CssDimValue
   vGrowRows: boolean
-  rowSyncHeights: number[]
+  rowSyncHeights: { [rowKey: string]: number }
+  reportRowHeight?: (rowKey: string, innerEl: HTMLElement) => void // TODO: don't make optional
 }
 
 
@@ -70,21 +69,6 @@ export interface ScrollerLike { // have scrollers implement?
 }
 
 
-export function computeForceScrollbars(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
 }
@@ -202,12 +186,4 @@ export function getChunkClassNames(sectionConfig: SectionConfig, chunkConfig: Ch
 
 // IE sometimes reports a certain clientHeight, but when inner content is set to that height,
 // some sort of rounding error causes it to spill out and create unnecessary scrollbars. Compensate.
-const CLIENT_HEIGHT_WIGGLE = /Trident/.test(navigator.userAgent) ? 1 : 0
-
-export function computeScrollerClientWidths(scrollerElRefs: RefMap<HTMLElement, any>) {
-  return mapHash(scrollerElRefs.currentMap, (scrollerEl) => scrollerEl.clientWidth)
-}
-
-export function computeScrollerClientHeights(scrollerElRefs: RefMap<HTMLElement, any>) {
-  return mapHash(scrollerElRefs.currentMap, (scrollerEl) => scrollerEl.clientHeight - CLIENT_HEIGHT_WIGGLE)
-}
+export const CLIENT_HEIGHT_WIGGLE = /Trident/.test(navigator.userAgent) ? 1 : 0

+ 35 - 15
packages/core/src/styles/_event-horizontal.scss

@@ -10,7 +10,7 @@ this class is used for:
 */
 
 /* bigger touch area when selected */
-.fc-h-event.fc-selected:before {
+.fc-h-event.fc-event-selected:before {
   content: "";
   position: absolute;
   z-index: 3; /* below resizers */
@@ -20,10 +20,30 @@ this class is used for:
   right: 0;
 }
 
+
+.fc-h-event {
+  position: relative; /* for resize handle and other inner positioning */
+  display: block; /* make the <a> tag block */
+  font-size: .85em;
+  line-height: 1.4;
+  border: 1px solid #3788d8;
+  background-color: #3788d8;
+  color: #fff; /* default TEXT color. TODO: test hovering over <a> when HREF and theme */
+  text-decoration: none; /* if <a> has an href */
+}
+
+
+.fc-h-event .fc-event-time,
+.fc-h-event .fc-event-title {
+  display: inline;
+}
+
+
+
 /* events that are continuing to/from another week. kill rounded corners and butt up against edge */
 
-.fc-ltr .fc-h-event.fc-not-start,
-.fc-rtl .fc-h-event.fc-not-end {
+.fc-ltr .fc-h-event:not(.fc-event-start),
+.fc-rtl .fc-h-event:not(.fc-event-end) {
   margin-left: 0;
   border-left-width: 0;
   padding-left: 1px; /* replace the border with padding */
@@ -31,8 +51,8 @@ this class is used for:
   border-bottom-left-radius: 0;
 }
 
-.fc-ltr .fc-h-event.fc-not-end,
-.fc-rtl .fc-h-event.fc-not-start {
+.fc-ltr .fc-h-event:not(.fc-event-end),
+.fc-rtl .fc-h-event:not(.fc-event-start) {
   margin-right: 0;
   border-right-width: 0;
   padding-right: 1px; /* replace the border with padding */
@@ -43,22 +63,22 @@ this class is used for:
 /* resizer (cursor AND touch devices) */
 
 /* left resizer  */
-.fc-ltr .fc-h-event .fc-start-resizer,
-.fc-rtl .fc-h-event .fc-end-resizer {
+.fc-ltr .fc-h-event .fc-event-resizer-start,
+.fc-rtl .fc-h-event .fc-event-resizer-end {
   cursor: w-resize;
   left: -1px; /* overcome border */
 }
 
 /* right resizer */
-.fc-ltr .fc-h-event .fc-end-resizer,
-.fc-rtl .fc-h-event .fc-start-resizer {
+.fc-ltr .fc-h-event .fc-event-resizer-end,
+.fc-rtl .fc-h-event .fc-event-resizer-start {
   cursor: e-resize;
   right: -1px; /* overcome border */
 }
 
 /* resizer (mouse devices) */
 
-.fc-h-event.fc-allow-mouse-resize .fc-resizer {
+.fc-h-event.fc-allow-mouse-resize .fc-event-resizer {
   width: 7px;
   top: -1px; /* overcome top border */
   bottom: -1px; /* overcome bottom border */
@@ -66,7 +86,7 @@ this class is used for:
 
 /* resizer (touch devices) */
 
-.fc-h-event.fc-selected .fc-resizer {
+.fc-h-event.fc-event-selected .fc-event-resizer {
   /* 8x8 little dot */
   border-radius: 4px;
   border-width: 1px;
@@ -81,13 +101,13 @@ this class is used for:
 }
 
 /* left resizer  */
-.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,
-.fc-rtl .fc-h-event.fc-selected .fc-end-resizer {
+.fc-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start,
+.fc-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end {
   margin-left: -4px; /* centers the 8x8 dot on the left edge */
 }
 
 /* right resizer */
-.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,
-.fc-rtl .fc-h-event.fc-selected .fc-start-resizer {
+.fc-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end,
+.fc-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start {
   margin-right: -4px; /* centers the 8x8 dot on the right edge */
 }

+ 27 - 24
packages/core/src/styles/_event.scss

@@ -4,28 +4,35 @@
 .fc-not-end --> :not(.fc-event-end)
 */
 
-.fc-event {
-  position: relative; /* for resize handle and other inner positioning */
-  display: block; /* make the <a> tag block */
-  font-size: .85em;
-  line-height: 1.4;
-  border-radius: 3px;
-  border: 1px solid #3788d8;
+
+.fc-bgevent,
+.fc-nonbusiness {
+  opacity: 0.3;
 }
 
-.fc-event,
+
 .fc-event-dot {
   background-color: #3788d8; /* default BACKGROUND color */
 }
 
-.fc-event,
-.fc-event:hover {
-  color: #fff; /* default TEXT color */
-  text-decoration: none; /* if <a> has an href */
-}
+
+// TODO: copy and paste to other event types
+// .fc-event {
+//   position: relative; /* for resize handle and other inner positioning */
+//   display: block; /* make the <a> tag block */
+//   font-size: .85em;
+//   line-height: 1.4;
+//   border-radius: 3px;
+//   border: 1px solid #3788d8;
+//   background-color: #3788d8;
+//   color: #fff; /* default TEXT color. TODO: test hovering over <a> when HREF and theme */
+//   text-decoration: none; /* if <a> has an href */
+// }
+
+
 
 .fc-event[href],
-.fc-event.fc-draggable {
+.fc-event.fc-event-draggable {
   cursor: pointer; /* give events with links and draggable events a hand mouse pointer */
 }
 
@@ -34,33 +41,29 @@
   cursor: not-allowed;
 }
 
-.fc-event .fc-content {
-  position: relative;
-  z-index: 2;
-}
 
 /* resizer (cursor AND touch devices) */
 
-.fc-event .fc-resizer {
+.fc-event .fc-event-resizer {
   position: absolute;
   z-index: 4;
 }
 
 /* resizer (touch devices) */
 
-.fc-event .fc-resizer {
+.fc-event .fc-event-resizer {
   display: none;
 }
 
-.fc-event.fc-allow-mouse-resize .fc-resizer,
-.fc-event.fc-selected .fc-resizer {
+.fc-event.fc-allow-mouse-resize .fc-event-resizer,
+.fc-event.fc-selected .fc-event-resizer {
   /* only show when hovering or selected (with touch) */
   display: block;
 }
 
 /* hit area */
 
-.fc-event.fc-selected .fc-resizer:before {
+.fc-event.fc-selected .fc-event-resizer:before {
   /* 40x40 touch area */
   content: "";
   position: absolute;
@@ -120,7 +123,7 @@
     page-break-inside: avoid;
   }
 
-  .fc-event .fc-resizer {
+  .fc-event .fc-event-resizer {
     display: none;
   }
 

+ 0 - 1
packages/core/src/types/input-types.ts

@@ -101,7 +101,6 @@ export interface OptionsInputBase {
   hiddenDays?: number[]
   fixedWeekCount?: boolean
   weekNumbers?: boolean
-  weekNumbersWithinDays?: boolean
   weekNumberCalculation?: 'local' | 'ISO' | ((m: Date) => number)
   businessHours?: BusinessHoursInput
   showNonCurrentDates?: boolean

+ 29 - 36
packages/core/src/util/RefMap.ts

@@ -1,74 +1,67 @@
-import { hashValuesToArray } from './object'
+import { hashValuesToArray, collectFromHash } from './object'
 
 /*
 TODO: somehow infer OtherArgs from masterCallback?
-TODO: make OtherArgs a single object to avoid spreading?
+TODO: infer RefType from masterCallback if provided
 */
-export default class RefMap<RefType, OtherArgs extends any[] = []> {
+export default class RefMap<RefType> {
 
   public currentMap: { [key: string]: RefType } = {}
-  private otherArgsMap: { [key: string]: OtherArgs } = {}
+  private depths: { [key: string]: number } = {}
   private callbackMap: { [key: string]: (val: RefType | null) => void } = {}
 
 
-  constructor(public masterCallback?: (val: RefType | null, key: string, ...otherArgs: OtherArgs) => void) {
+  constructor(public masterCallback?: (val: RefType | null, key: string) => void) {
   }
 
 
-  createRef(key: string | number, ...otherArgs: OtherArgs) {
+  createRef(key: string | number) {
     let refCallback = this.callbackMap[key]
 
     if (!refCallback) {
       refCallback = this.callbackMap[key] = (val: RefType | null) => {
-        this.handleValue(val, String(key), ...otherArgs)
+        this.handleValue(val, String(key))
       }
     }
 
-    this.otherArgsMap[key] = otherArgs
-
     return refCallback
   }
 
 
-  handleValue = (val: RefType | null, key: string, ...otherArgs: OtherArgs) => { // bind in case users want to pass it around
+  handleValue = (val: RefType | null, key: string) => { // bind in case users want to pass it around
+    let { depths, currentMap } = this
+    let removed = false
+    let added = false
+
     if (val !== null) {
-      this.currentMap[key] = val
-    } else {
-      delete this.currentMap[key]
+      removed = (key in currentMap) // for bug
+      currentMap[key] = val
+      depths[key] = (depths[key] || 0) + 1
+      added = true
+
+    } else if (--depths[key] === 0) {
+      delete currentMap[key]
       delete this.callbackMap[key]
-      delete this.otherArgsMap[key]
+      removed = true
     }
 
     if (this.masterCallback) {
-      this.masterCallback(val, String(key), ...otherArgs)
+      if (removed) {
+        this.masterCallback(null, String(key))
+      }
+      if (added) {
+        this.masterCallback(val, String(key))
+      }
     }
   }
 
 
   collect( // TODO: check callers that don't care about order. should use getAll instead
-    startIndex = 0,
+    startIndex?: number,
     endIndex?: number,
-    step = 1,
-    filterFunc?: (val: RefType, key: number, ...otherArgs: OtherArgs) => boolean
+    step?: number
   ) {
-    let { currentMap, otherArgsMap } = this
-    let res: RefType[] = []
-
-    if (endIndex == null) {
-      endIndex = Object.keys(currentMap).length
-    }
-
-    for (let i = startIndex; i < endIndex; i += step) {
-      let val = currentMap[i]
-
-      if (val !== undefined) { // will disregard undefined for sparse arrays
-        if (!filterFunc || filterFunc(val, i, ...otherArgsMap[i])) {
-          res.push(val)
-        }
-      }
-    }
-
-    return res
+    return collectFromHash(this.currentMap, startIndex, endIndex, step)
   }
 
 

+ 6 - 83
packages/core/src/util/dom-manip.ts

@@ -1,80 +1,11 @@
 
-// Creating
-// ----------------------------------------------------------------------------------------------------------------
-
-const containerTagHash = {
-  '<tr': 'tbody',
-  '<td': 'tr'
-}
-
-export function htmlToElement(html: string): HTMLElement { // TODO: use renderVNodes instead?
+export function htmlToElement(html: string): HTMLElement {
   html = html.trim()
-  let container = document.createElement(computeContainerTag(html))
+  let container = document.createElement('div')
   container.innerHTML = html
   return container.firstChild as HTMLElement
 }
 
-export function htmlToElements(html: string): HTMLElement[] {
-  return Array.prototype.slice.call(htmlToNodeList(html))
-}
-
-function htmlToNodeList(html: string): NodeList {
-  html = html.trim()
-  let container = document.createElement(computeContainerTag(html))
-  container.innerHTML = html
-  return container.childNodes
-}
-
-// assumes html already trimmed and tag names are lowercase
-function computeContainerTag(html: string) {
-  return containerTagHash[
-    html.substr(0, 3) // faster than using regex
-  ] || 'div'
-}
-
-
-// Inserting / Removing
-// ----------------------------------------------------------------------------------------------------------------
-
-export type ElementContent = string | Node | Node[] | NodeList
-
-export function appendToElement(el: HTMLElement, content: ElementContent) {
-  let childNodes = normalizeContent(content)
-
-  for (let i = 0; i < childNodes.length; i++) {
-    el.appendChild(childNodes[i])
-  }
-}
-
-export function prependToElement(parent: HTMLElement, content: ElementContent) {
-  let newEls = normalizeContent(content)
-  let afterEl = parent.firstChild || null // if no firstChild, will append to end, but that's okay, b/c there were no children
-
-  for (let i = 0; i < newEls.length; i++) {
-    parent.insertBefore(newEls[i], afterEl)
-  }
-}
-
-export function insertAfterElement(refEl: HTMLElement, content: ElementContent) {
-  let newEls = normalizeContent(content)
-  let afterEl = refEl.nextSibling || null
-
-  for (let i = 0; i < newEls.length; i++) {
-    refEl.parentNode.insertBefore(newEls[i], afterEl)
-  }
-}
-
-function normalizeContent(content: ElementContent): Node[] {
-  let els
-  if (typeof content === 'string') {
-    els = htmlToElements(content)
-  } else if (content instanceof Node) {
-    els = [ content ]
-  } else { // Node[] or NodeList
-    els = Array.prototype.slice.call(content)
-  }
-  return els
-}
 
 export function removeElement(el: HTMLElement) {
   if (el.parentNode) {
@@ -107,14 +38,17 @@ const closestMethod = Element.prototype.closest || function(selector) {
   return null
 }
 
+
 export function elementClosest(el: HTMLElement, selector: string): HTMLElement {
   return (closestMethod as any).call(el, selector)
 }
 
+
 export function elementMatches(el: HTMLElement, selector: string): HTMLElement {
   return matchesMethod.call(el, selector)
 }
 
+
 // accepts multiple subject els
 // returns a real array. good for methods like forEach
 export function findElements(container: HTMLElement[] | HTMLElement | NodeListOf<HTMLElement>, selector: string): HTMLElement[] {
@@ -132,6 +66,7 @@ export function findElements(container: HTMLElement[] | HTMLElement | NodeListOf
   return allMatches
 }
 
+
 // accepts multiple subject els
 // only queries direct child elements // TODO: rename to findDirectChildren!
 export function findDirectChildren(parent: HTMLElement[] | HTMLElement, selector?: string): HTMLElement[] {
@@ -154,18 +89,6 @@ export function findDirectChildren(parent: HTMLElement[] | HTMLElement, selector
 }
 
 
-// Attributes
-// ----------------------------------------------------------------------------------------------------------------
-
-export function forceClassName(el: HTMLElement, className: string, bool) { // instead of classList.toggle, which IE doesn't support
-  if (bool) {
-    el.classList.add(className)
-  } else {
-    el.classList.remove(className)
-  }
-}
-
-
 // Style
 // ----------------------------------------------------------------------------------------------------------------
 

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

@@ -1,46 +1,4 @@
 
-export function htmlEscape(s) {
-  return (s + '').replace(/&/g, '&amp;')
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/'/g, '&#039;')
-    .replace(/"/g, '&quot;')
-    .replace(/\n/g, '<br />')
-}
-
-
-// Given a hash of CSS properties, returns a string of CSS.
-// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
-export function cssToStr(cssProps) {
-  let statements = []
-
-  for (let name in cssProps) {
-    let val = cssProps[name]
-    if (val != null && val !== '') {
-      statements.push(name + ':' + val)
-    }
-  }
-
-  return statements.join(';')
-}
-
-
-// Given an object hash of HTML attribute names to values,
-// generates a string that can be injected between < > in HTML
-export function attrsToStr(attrs) {
-  let parts = []
-
-  for (let name in attrs) {
-    let val = attrs[name]
-    if (val != null) {
-      parts.push(name + '="' + htmlEscape(val) + '"')
-    }
-  }
-
-  return parts.join(' ')
-}
-
-
 export type ClassNameInput = string | string[]
 
 export function parseClassName(raw: ClassNameInput) {

+ 94 - 33
packages/core/src/util/memoize.ts

@@ -1,57 +1,118 @@
 import { isArraysEqual } from './array'
 
 
-// TODO: better typings!!!
+export function memoize<Args extends any[], Res>(
+  workerFunc: (...args: Args) => Res,
+  resEquality?: (res0: Res, res1: Res) => boolean,
+  teardownFunc?: (res: Res) => void
+): (...args: Args) => Res {
 
+  let currentArgs: Args | undefined
+  let currentRes: Res | undefined
 
-export function memoize<T>(workerFunc: T, resEquality?: (res0, res1) => boolean): T {
-  let args
-  let res
+  return function(...newArgs: Args) {
 
-  return function() {
-    if (!args || !isArraysEqual(args, arguments)) { // the arguments have changed?...
-      args = arguments
+    if (!currentArgs) {
+      currentRes = workerFunc.apply(this, newArgs)
 
-      let newRes = (workerFunc as any).apply(this, arguments)
+    } else if (!isArraysEqual(currentArgs, newArgs)) {
 
-      if (res === undefined || !(resEquality ? resEquality(newRes, res) : newRes === res)) {
-        res = newRes // the result has changed
+      if (teardownFunc) {
+        teardownFunc(currentRes)
+      }
+
+      let res = workerFunc.apply(this, newArgs)
+
+      if (!resEquality || !resEquality(res, currentRes)) {
+        currentRes = res
       }
     }
 
-    return res
-  } as any
+    currentArgs = newArgs
+
+    return currentRes
+  }
 }
 
 
-export function memoizeParallel<Res>(workerFunc: (...args: any[]) => Res, resEquality?: (res0, res1) => boolean): (...args: any[]) => Res[] {
-  let memoizers = []
+export function memoizeArraylike<Args extends any[], Res>( // used at all?
+  workerFunc: (...args: Args) => Res,
+  resEquality?: (res0: Res, res1: Res) => boolean,
+  teardownFunc?: (res: Res) => void
+): (argSets: Args[]) => Res[] {
 
-  return function() {
-    let argCnt = arguments.length
-    let memoizerCnt = arguments[0].length
-    let i
-    let allRes = []
+  let currentArgSets: Args[] = []
+  let currentResults: Res[] = []
 
-    memoizers.splice(memoizerCnt) // remove excess
+  return function(newArgSets: Args[]) {
+    let currentLen = currentArgSets.length
+    let newLen = newArgSets.length
+    let i = 0
 
-    // add new
-    for (i = memoizers.length; i < memoizerCnt; i++) {
-      memoizers[i] = memoize(workerFunc, resEquality)
-    }
+    for (; i < currentLen; i++) {
+      if (!isArraysEqual(currentArgSets[i], newArgSets[i])) {
+
+        if (teardownFunc) {
+          teardownFunc(currentResults[i])
+        }
 
-    for (i = 0; i < memoizerCnt; i++) {
-      let args = []
+        let res = workerFunc.apply(this, newArgSets[i])
 
-      for (let argIndex = 0; argIndex < argCnt; argIndex++) {
-        args.push(arguments[argIndex][i])
+        if (!resEquality || !resEquality(res, currentResults[i])) {
+          currentResults[i] = res
+        }
       }
+    }
+
+    for (; i < newLen; i++) {
+      currentResults[i] = workerFunc.apply(this, newArgSets[i])
+    }
+
+    currentArgSets = newArgSets
+    currentResults.splice(newLen) // remove excess
+
+    return currentResults
+  }
+}
 
-      allRes.push(
-        memoizers[i].apply(this, args)
-      )
+
+export function memoizeHashlike<Args extends any[], Res>(
+  workerFunc: (...args: Args) => Res,
+  resEquality?: (res0: Res, res1: Res) => boolean,
+  teardownFunc?: (res: Res) => void // TODO: change arg order
+): (argHash: { [key: string]: Args }) => { [key: string]: Res } {
+
+  let currentArgHash: { [key: string]: Args } = {}
+  let currentResHash: { [key: string]: Res } = {}
+
+  return function(newArgHash: { [key: string]: Args }) {
+    let newResHash: { [key: string]: Res } = {}
+
+    for (let key in newArgHash) {
+
+      if (!currentResHash[key]) {
+        newResHash[key] = workerFunc.apply(this, newArgHash[key])
+
+      } else if (!isArraysEqual(currentArgHash[key], newArgHash[key])) {
+
+        if (teardownFunc) {
+          teardownFunc(currentResHash[key])
+        }
+
+        let res = workerFunc.apply(this, newArgHash[key])
+
+        newResHash[key] = (resEquality && resEquality(res, currentResHash[key]))
+          ? currentResHash[key]
+          : res
+
+      } else {
+        newResHash[key] = currentResHash[key]
+      }
     }
 
-    return allRes
-  } as any
+    currentArgHash = newArgHash
+    currentResHash = newResHash
+
+    return newResHash
+  }
 }

+ 38 - 1
packages/core/src/util/object.ts

@@ -78,7 +78,7 @@ export function mapHash<InputItem, OutputItem>(
 }
 
 
-export function arrayToHash(a): { [key: string]: true } {
+export function arrayToHash(a): { [key: string]: true } { // TODO: rename to strinArrayToHash or something
   let hash = {}
 
   for (let item of a) {
@@ -89,6 +89,19 @@ export function arrayToHash(a): { [key: string]: true } {
 }
 
 
+export function buildHashFromArray<Item, ItemRes>(a: Item[], func: (item: Item, index: number) => [ string, ItemRes ]) {
+  let hash: { [key: string]: ItemRes } = {}
+
+  for (let i = 0; i < a.length; i++) {
+    let tuple = func(a[i], i)
+
+    hash[tuple[0]] = tuple[1]
+  }
+
+  return hash
+}
+
+
 export function hashValuesToArray(obj) { // can't use Object.values yet because of no IE support
   let a = []
 
@@ -193,3 +206,27 @@ function isObjValsEqual<T>(val0: T, val1: T, comparator: EqualityThing<T>) {
   }
   return false
 }
+
+
+export function collectFromHash<Item>(
+  hash: { [key: string]: Item },
+  startIndex = 0,
+  endIndex?: number,
+  step = 1,
+) {
+  let res: Item[] = []
+
+  if (endIndex == null) {
+    endIndex = Object.keys(hash).length
+  }
+
+  for (let i = startIndex; i < endIndex; i += step) {
+    let val = hash[i]
+
+    if (val !== undefined) { // will disregard undefined for sparse arrays
+      res.push(val)
+    }
+  }
+
+  return res
+}

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

@@ -1,17 +1,11 @@
-import { Component, h, Fragment, Ref, ComponentChildren, render } from './vdom'
+import { Component, Ref } from './vdom'
 import ComponentContext, { ComponentContextType } from './component/ComponentContext'
 import { __assign } from 'tslib'
 import { compareObjs, EqualityFuncs } from './util/object'
 
 
-interface SubRendererOwner {
-  context: ComponentContext
-  subrendererDestroys: (() => void)[]
-}
-
-
 // TODO: make a HOC instead
-export abstract class BaseComponent<Props={}, State={}> extends Component<Props, State> implements SubRendererOwner {
+export abstract class BaseComponent<Props={}, State={}> extends Component<Props, State> {
 
   static addPropsEquality = addPropsEquality
   static addStateEquality = addStateEquality
@@ -20,7 +14,6 @@ export abstract class BaseComponent<Props={}, State={}> extends Component<Props,
   context: ComponentContext
   propEquality: EqualityFuncs<Props>
   stateEquality: EqualityFuncs<State>
-  subrendererDestroys: (() => void)[] = []
 
   abstract render(props: Props, state: State, context: ComponentContext) // why aren't arg types being enforced!?
 
@@ -30,58 +23,10 @@ export abstract class BaseComponent<Props={}, State={}> extends Component<Props,
       this.context !== nextContext
   }
 
-  subrenderDestroy: typeof subrenderDestroy
-
 }
 
 BaseComponent.prototype.propEquality = {}
 BaseComponent.prototype.stateEquality = {}
-BaseComponent.prototype.subrenderDestroy = subrenderDestroy
-
-
-export abstract class SubRenderer<Props={}, RenderRes=void> implements SubRendererOwner {
-
-  static addPropsEquality = addPropsEquality
-
-  propEquality: EqualityFuncs<Props>
-  subrendererDestroys: (() => void)[] = []
-
-  constructor(
-    public props: Props,
-    public context: ComponentContext
-  ) {
-  }
-
-  abstract render(props: Props, context: ComponentContext): RenderRes
-
-  unrender(renderRes: RenderRes, context: ComponentContext) {
-  }
-
-  subrenderDestroy: typeof subrenderDestroy
-
-  willDestroy() {
-    this.subrenderDestroy()
-  }
-
-}
-
-SubRenderer.prototype.propEquality = {}
-SubRenderer.prototype.subrenderDestroy = subrenderDestroy
-
-
-export type SubRendererClass<SubRendererType> = (
-  new(
-    props: SubRendererType extends SubRenderer<infer Props> ? Props : never,
-    context: ComponentContext
-  ) => SubRendererType
-) & {
-  prototype: {
-    render(
-      props: SubRendererType extends SubRenderer<infer Props> ? Props : never,
-      context: ComponentContext
-    )
-  }
-}
 
 
 function addPropsEquality(this: { prototype: { propEquality: any } }, propEquality) {
@@ -98,169 +43,7 @@ function addStateEquality(this: { prototype: { stateEquality: any } }, stateEqua
 }
 
 
-function subrenderDestroy(this: SubRendererOwner) {
-  for (let destroy of this.subrendererDestroys) {
-    destroy()
-  }
-  this.subrendererDestroys = []
-}
-
-
-export function subrenderer<SubRendererType>(subRendererClass: SubRendererClass<SubRendererType>): ((
-  props: (SubRendererType extends SubRenderer<infer Props> ? Props : never) | false,
-  context?: ComponentContext
-) => SubRendererType)
-export function subrenderer<FuncProps, RenderRes>(
-  renderFunc: (funcProps: FuncProps, context?: ComponentContext) => RenderRes,
-  unrenderFunc?: (funcState: RenderRes, context?: ComponentContext) => void
-): ((
-  funcProps: FuncProps | false,
-  context?: ComponentContext
-) => RenderRes)
-export function subrenderer(worker, unrender?) {
-  if (worker.prototype.render) {
-    return buildClassSubRenderer(worker)
-  } else {
-    return buildFuncSubRenderer(worker, unrender)
-  }
-}
-
-
-function buildClassSubRenderer(subRendererClass: SubRendererClass<any>) {
-  let instance: SubRenderer
-  let renderRes
-
-  function destroy() {
-    if (instance) {
-      instance.unrender(renderRes, instance.context)
-      instance.willDestroy()
-      instance = null
-    }
-  }
-
-  return function(this: SubRendererOwner, props: any) { // what about passing in Context?
-    let context = this.context
-
-    if (!props) {
-      destroy()
-
-    } else if (!instance) {
-      instance = new subRendererClass(props, context) // will set internal props/context
-      renderRes = instance.render(props, context)
-      this.subrendererDestroys.push(destroy)
-
-    } else if (
-      !compareObjs(props, instance.props, instance.propEquality) ||
-      !compareObjs(context, instance.context)
-    ) {
-      instance.unrender(renderRes, context)
-      instance.props = props
-      instance.context = context
-      renderRes = instance.render(props, context)
-    }
-
-    return instance
-  }
-}
-
-
-function buildFuncSubRenderer(renderFunc, unrenderFunc) {
-  let thisContext
-  let currentProps
-  let currentContext
-  let renderRes
-
-  function destroy() {
-    if (currentProps) {
-      unrenderFunc && unrenderFunc.call(thisContext, renderRes, currentContext)
-      currentProps = null
-      currentContext = null
-      renderRes = null
-    }
-  }
-
-  return function(this: SubRendererOwner, props: any) { // what about passing in Context?
-    thisContext = this
-    let context = thisContext.context
-
-    if (!props) {
-      destroy()
-
-    } else {
-
-      if (!currentProps) {
-        renderRes = renderFunc.call(thisContext, props, context)
-        this.subrendererDestroys.push(destroy)
-
-      } else if (
-        !compareObjs(props, currentProps) || (
-          renderFunc.length > 1 && // has second arg? cares about context?
-          context !== currentContext
-        )
-      ) {
-        unrenderFunc && unrenderFunc.call(thisContext, renderRes, context)
-        renderRes = renderFunc.call(thisContext, props, context)
-      }
-
-      currentProps = props
-      currentContext = context
-    }
-
-    return renderRes
-  }
-}
-
-
-export function buildMapSubRenderer(subRendererClass: SubRendererClass<any>) {
-  let currentInstances = {}
-
-  function destroyAll() {
-    for (let key in currentInstances) {
-      currentInstances[key].willDestroy()
-    }
-    currentInstances = {}
-  }
-
-  return function(this: SubRendererOwner, propMap) { // what about passing in Context?
-    let context = this.context
-
-    if (!propMap) {
-      destroyAll()
-
-    } else {
-
-      for (let key in currentInstances) {
-        if (!propMap[key]) {
-          currentInstances[key].destroy()
-          delete currentInstances[key]
-        }
-      }
-
-      for (let key in propMap) {
-        let props = propMap[key]
-        let instance = currentInstances[key]
-
-        if (!instance) {
-          instance = currentInstances[key] = new subRendererClass(props, context) // TODO: pass in state???
-          instance.render(props, context)
-
-        } else if (
-          !compareObjs(props, instance.props, instance.propEquality) ||
-          !compareObjs(context, instance.context)
-        ) {
-          instance.unrender()
-          instance.props = props
-          instance.context = context
-          instance.render(props, context)
-        }
-      }
-    }
-
-    return currentInstances
-  }
-}
-
-
+// use other one
 export function setRef<RefType>(ref: Ref<RefType> | void, current: RefType) {
   if (typeof ref === 'function') {
     ref(current)
@@ -268,17 +51,3 @@ export function setRef<RefType>(ref: Ref<RefType> | void, current: RefType) {
     ref.current = current
   }
 }
-
-
-export function renderVNodes(children: ComponentChildren, context: ComponentContext): Node[] {
-  let containerEl = document.createElement('div')
-
-  render(
-    <ComponentContextType.Provider value={context}>
-      <Fragment>{children}</Fragment>
-    </ComponentContextType.Provider>,
-    containerEl
-  )
-
-  return Array.prototype.slice.call(containerEl.childNodes)
-}

+ 0 - 89
packages/daygrid/src/CellEvents.ts

@@ -1,89 +0,0 @@
-import {
-  htmlEscape, cssToStr,
-  FgEventRenderer,
-  Seg,
-  computeEventDraggable, computeEventStartResizable, computeEventEndResizable, BaseFgEventRendererProps
-} from '@fullcalendar/core'
-
-
-/* Event-rendering methods for the DayGrid class
-----------------------------------------------------------------------------------------------------------------------*/
-
-export default abstract class CellEvents<Props extends BaseFgEventRendererProps> extends FgEventRenderer<Props> {
-
-
-  // Builds the HTML to be used for the default element for an individual segment
-  renderSegHtml(seg: Seg, isDragging: boolean, isResizing: boolean, isSelecting: boolean) {
-    let { context } = this
-    let eventRange = seg.eventRange
-    let eventDef = eventRange.def
-    let eventUi = eventRange.ui
-    let allDay = eventDef.allDay
-    let isDraggable = computeEventDraggable(context, eventDef, eventUi)
-    let isResizableFromStart = allDay && seg.isStart && computeEventStartResizable(context, eventDef, eventUi)
-    let isResizableFromEnd = allDay && seg.isEnd && computeEventEndResizable(context, eventDef, eventUi)
-    let classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, isDragging, isResizing, isSelecting)
-    let skinCss = cssToStr(this.getSkinCss(eventUi))
-    let timeHtml = ''
-    let timeText
-    let titleHtml
-
-    classes.unshift('fc-day-grid-event', 'fc-h-event')
-
-    // Only display a timed events time if it is the starting segment
-    if (seg.isStart) {
-      timeText = this.getTimeText(eventRange)
-      if (timeText) {
-        timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>'
-      }
-    }
-
-    titleHtml =
-      '<span class="fc-title">' +
-        (htmlEscape(eventDef.title || '') || '&nbsp;') + // we always want one line of height
-      '</span>'
-
-    return '<a class="' + classes.join(' ') + '"' +
-        (eventDef.url ?
-          ' href="' + htmlEscape(eventDef.url) + '"' :
-          ''
-          ) +
-        (skinCss ?
-          ' style="' + skinCss + '"' :
-          ''
-          ) +
-      '>' +
-        '<div class="fc-content">' +
-          (context.options.dir === 'rtl' ?
-            titleHtml + ' ' + timeHtml : // put a natural space in between
-            timeHtml + ' ' + titleHtml   //
-            ) +
-        '</div>' +
-        (isResizableFromStart ?
-          '<div class="fc-resizer fc-start-resizer"></div>' :
-          ''
-          ) +
-        (isResizableFromEnd ?
-          '<div class="fc-resizer fc-end-resizer"></div>' :
-          ''
-          ) +
-      '</a>'
-  }
-
-
-  // Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined
-  computeEventTimeFormat() {
-    return {
-      hour: 'numeric',
-      minute: '2-digit',
-      omitZeroMinute: true,
-      meridiem: 'narrow'
-    }
-  }
-
-
-  computeDisplayEventEnd() {
-    return false // TODO: somehow consider the originating DayGrid's column count
-  }
-
-}

+ 0 - 65
packages/daygrid/src/DayBgCell.tsx

@@ -1,65 +0,0 @@
-import {
-  h,
-  ComponentContext,
-  DateMarker,
-  getDayClasses,
-  rangeContainsMarker,
-  DateProfile,
-  BaseComponent,
-  Ref,
-  setRef
-} from '@fullcalendar/core'
-
-
-export interface DayBgCellProps {
-  date: DateMarker
-  dateProfile: DateProfile
-  otherAttrs?: object
-  elRef?: Ref<HTMLTableCellElement>
-}
-
-
-export default class DayBgCell extends BaseComponent<DayBgCellProps> {
-
-
-  render(props: DayBgCellProps, state: {}, context: ComponentContext) {
-    let { date, dateProfile, otherAttrs } = props
-    let isDateValid = rangeContainsMarker(dateProfile.activeRange, date) // TODO: called too frequently. cache somehow
-    let classes = getDayClasses(date, dateProfile, context)
-    let dateStr = context.dateEnv.formatIso(date, { omitTime: true })
-    let dataAttrs = isDateValid ? { 'data-date': dateStr } : {}
-
-    classes.unshift('fc-day')
-
-    return (
-      <td
-        ref={this.handleEl}
-        key={dateStr /* fresh rerender for new date, mostly because of dayRender
-          TODO: only do if there are dayRender triggers!!! */}
-        class={classes.join(' ')}
-        { ...dataAttrs }
-        { ...otherAttrs }
-      />
-    )
-  }
-
-
-  handleEl = (el: HTMLElement | null) => {
-    let { props } = this
-
-    if (el) {
-      let { calendar, view, dateEnv } = this.context
-
-      calendar.publiclyTrigger('dayRender', [
-        {
-          date: dateEnv.toDate(props.date),
-          el,
-          view
-        }
-      ])
-    }
-
-    setRef(props.elRef, el)
-  }
-
-}

+ 0 - 58
packages/daygrid/src/DayBgRow.tsx

@@ -1,58 +0,0 @@
-import {
-  h, VNode,
-  ComponentContext,
-  DateMarker,
-  DateProfile,
-  BaseComponent,
-  RefMap
-} from '@fullcalendar/core'
-import DayBgCell from './DayBgCell'
-
-
-export interface DayBgRowProps {
-  cells: DayBgCellModel[]
-  dateProfile: DateProfile
-  cellElRefs: RefMap<HTMLTableCellElement>
-  renderIntro?: () => VNode[]
-}
-
-export interface DayBgCellModel {
-  date: DateMarker
-  htmlAttrs?: object
-}
-
-
-export default class DayBgRow extends BaseComponent<DayBgRowProps> {
-
-
-  render(props: DayBgRowProps, state: {}, context: ComponentContext) {
-    let { cells } = props
-    let parts: VNode[] = []
-
-    if (props.renderIntro) {
-      parts.push(...props.renderIntro())
-    }
-
-    for (let i = 0; i < cells.length; i++) {
-      let cell = cells[i]
-
-      parts.push(
-        <DayBgCell
-          date={cell.date}
-          dateProfile={props.dateProfile}
-          otherAttrs={cell.htmlAttrs}
-          elRef={props.cellElRefs.createRef(i)}
-        />
-      )
-    }
-
-    if (!cells.length) {
-      parts.push(
-        <td class='fc-day'></td>
-      )
-    }
-
-    return (<tr>{parts}</tr>)
-  }
-
-}

+ 18 - 20
packages/daygrid/src/DayTable.tsx

@@ -10,16 +10,17 @@ import {
   DateComponent,
   DateRange,
   Slicer,
-  Hit,
   ComponentContext,
   RefObject,
-  CssDimValue
+  CssDimValue,
+  Hit
 } from '@fullcalendar/core'
-import Table, { TableSeg  } from './Table'
+import Table from './Table'
+import TableSeg from './TableSeg'
 
 
 export interface DayTableProps {
-  dateProfile: DateProfile | null
+  dateProfile: DateProfile
   dayTableModel: DayTableModel
   nextDayThreshold: Duration
   businessHours: EventStore
@@ -29,17 +30,13 @@ export interface DayTableProps {
   eventSelection: string
   eventDrag: EventInteractionState | null
   eventResize: EventInteractionState | null
-  isRigid: boolean
   colGroupNode: VNode
-  renderNumberIntro: (row: number, cells: any) => VNode[]
-  renderBgIntro: () => VNode[]
-  renderIntro: () => VNode[]
-  colWeekNumbersVisible: boolean // week numbers render in own column? (caller does HTML via intro)
-  cellWeekNumbersVisible: boolean // display week numbers in day cell?
+  renderRowIntro?: () => VNode
   eventLimit: boolean | number
-  vGrow: boolean
+  vGrowRows: boolean
   headerAlignElRef?: RefObject<HTMLElement> // for more popover alignment
   clientWidth: CssDimValue
+  clientHeight: CssDimValue
 }
 
 export default class DayTable extends DateComponent<DayTableProps, ComponentContext> {
@@ -54,21 +51,17 @@ export default class DayTable extends DateComponent<DayTableProps, ComponentCont
     return (
       <Table
         ref={this.tableRef}
-        rootElRef={this.handleRootEl}
+        elRef={this.handleRootEl}
         { ...this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, context.calendar, dayTableModel) }
-        dateProfile={dateProfile}
         cells={dayTableModel.cells}
-        isRigid={props.isRigid}
+        dateProfile={dateProfile}
         colGroupNode={props.colGroupNode}
-        renderNumberIntro={props.renderNumberIntro}
-        renderBgIntro={props.renderBgIntro}
-        renderIntro={props.renderIntro}
-        colWeekNumbersVisible={props.colWeekNumbersVisible}
-        cellWeekNumbersVisible={props.cellWeekNumbersVisible}
+        renderRowIntro={props.renderRowIntro}
         eventLimit={props.eventLimit}
-        vGrow={props.vGrow}
+        vGrowRows={props.vGrowRows}
         headerAlignElRef={props.headerAlignElRef}
         clientWidth={props.clientWidth}
+        clientHeight={props.clientHeight}
       />
     )
   }
@@ -85,6 +78,11 @@ export default class DayTable extends DateComponent<DayTableProps, ComponentCont
   }
 
 
+  prepareHits() {
+    this.tableRef.current.prepareHits()
+  }
+
+
   queryHit(positionLeft: number, positionTop: number): Hit {
     let rawHit = this.tableRef.current.positionToHit(positionLeft, positionTop)
 

+ 5 - 12
packages/daygrid/src/DayTableView.tsx

@@ -8,13 +8,13 @@ import {
   memoize,
   DaySeries,
   DayTableModel,
-  ChunkContentCallbackArgs,
+  ChunkContentCallbackArgs
 } from '@fullcalendar/core'
-import TableView, { isEventLimitAuto } from './TableView'
+import TableView from './TableView'
 import DayTable from './DayTable'
 
 
-export default class DayTableView extends TableView { // TODO: use clientWidth/clientHeight
+export default class DayTableView extends TableView {
 
   private buildDayTableModel = memoize(buildDayTableModel)
   private headerRef = createRef<DayHeader>()
@@ -25,7 +25,6 @@ export default class DayTableView extends TableView { // TODO: use clientWidth/c
     let { options } = context
     let { dateProfile } = props
     let dayTableModel = this.buildDayTableModel(dateProfile, props.dateProfileGenerator)
-    let { colWeekNumbersVisible, cellWeekNumbersVisible } = this.processOptions(options)
 
     return this.renderLayout(
       options.columnHeader &&
@@ -34,7 +33,6 @@ export default class DayTableView extends TableView { // TODO: use clientWidth/c
           dateProfile={dateProfile}
           dates={dayTableModel.headerDates}
           datesRepDistinctDays={dayTableModel.rowCnt === 1}
-          renderIntro={this.renderHeadIntro}
         />,
       (contentArg: ChunkContentCallbackArgs) => (
         <DayTable
@@ -48,18 +46,13 @@ export default class DayTableView extends TableView { // TODO: use clientWidth/c
           eventSelection={props.eventSelection}
           eventDrag={props.eventDrag}
           eventResize={props.eventResize}
-          isRigid={isEventLimitAuto(context.options) && !props.isHeightAuto}
           nextDayThreshold={context.nextDayThreshold}
           colGroupNode={contentArg.tableColGroupNode}
-          renderNumberIntro={this.renderNumberIntro}
-          renderBgIntro={this.renderBgIntro}
-          renderIntro={this.renderIntro}
-          colWeekNumbersVisible={colWeekNumbersVisible}
-          cellWeekNumbersVisible={cellWeekNumbersVisible}
           eventLimit={options.eventLimit}
-          vGrow={!props.isHeightAuto}
+          vGrowRows={!props.isHeightAuto}
           headerAlignElRef={this.headerElRef}
           clientWidth={contentArg.clientWidth}
+          clientHeight={contentArg.clientHeight}
         />
       )
     )

+ 0 - 105
packages/daygrid/src/DayTile.tsx

@@ -1,105 +0,0 @@
-import {
-  h, createRef,
-  DateComponent, Seg,
-  Hit,
-  addDays, DateMarker,
-  EventInstanceHash,
-  subrenderer,
-  elementClosest
-} from '@fullcalendar/core'
-import DayTileEvents from './DayTileEvents'
-
-
-export interface DayTileProps {
-  date: DateMarker
-  fgSegs: Seg[]
-  selectedInstanceId: string
-  hiddenInstances: EventInstanceHash
-}
-
-export default class DayTile extends DateComponent<DayTileProps> {
-
-  private renderEvents = subrenderer(DayTileEvents)
-  private rootElRef = createRef<HTMLDivElement>()
-  private popoverEl: HTMLElement // HACK. contains this component
-
-
-  render() {
-    return (
-      <div class='fc-event-container' ref={this.rootElRef} />
-    )
-  }
-
-
-  componentDidMount() {
-    let rootEl = this.rootElRef.current
-    let popoverEl = this.popoverEl = elementClosest(rootEl, '.fc-popover')
-
-    this.subrender()
-
-    // HACK referencing parent's elements.
-    // also, if parent's elements change, this will break.
-    this.context.calendar.registerInteractiveComponent(this, {
-      el: popoverEl,
-      useEventCenter: false
-    })
-  }
-
-
-  componentDidUpdate() {
-    this.subrender()
-  }
-
-
-  componentWillUnmount() {
-    this.context.calendar.unregisterInteractiveComponent(this)
-    this.subrenderDestroy()
-  }
-
-
-  subrender() {
-    let { props } = this
-    let rootEl = this.rootElRef.current
-
-    this.renderEvents({
-      segs: props.fgSegs,
-      segContainerEl: rootEl,
-      selectedInstanceId: props.selectedInstanceId,
-      hiddenInstances: props.hiddenInstances,
-      isDragging: false,
-      isResizing: false,
-      isSelecting: false
-    })
-
-    this.context.calendar.releaseAfterSizingTriggers() // hack for eventPositioned
-  }
-
-
-  queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit | null {
-    let date = this.props.date
-
-    if (positionLeft < elWidth && positionTop < elHeight) {
-      return {
-        component: this,
-        dateSpan: {
-          allDay: true,
-          range: { start: date, end: addDays(date, 1) }
-        },
-        dayEl: this.popoverEl,
-        rect: {
-          left: 0,
-          top: 0,
-          right: elWidth,
-          bottom: elHeight
-        },
-        layer: 1
-      }
-    }
-  }
-
-
-  isPopover() {
-    return true // HACK for hit system
-  }
-
-}

+ 0 - 51
packages/daygrid/src/DayTileEvents.ts

@@ -1,51 +0,0 @@
-import {
-  Seg,
-  ComponentContext,
-  BaseFgEventRendererProps,
-  subrenderer,
-  removeElement
-} from '@fullcalendar/core'
-import CellEvents from './CellEvents'
-
-
-export interface DayTileEventsProps extends BaseFgEventRendererProps {
-  segContainerEl: HTMLElement
-}
-
-export default class DayTileEvents extends CellEvents<DayTileEventsProps> {
-
-  attachSegs = subrenderer(attachSegs, detachSegs)
-
-
-  render(props: DayTileEventsProps, context: ComponentContext) {
-    let segs = this.renderSegs({
-      segs: props.segs,
-      selectedInstanceId: props.selectedInstanceId,
-      hiddenInstances: props.hiddenInstances,
-      isDragging: props.isDragging,
-      isResizing: props.isResizing,
-      isSelecting: props.isSelecting,
-      interactingSeg: props.interactingSeg
-    })
-
-    this.attachSegs({
-      parentEl: props.segContainerEl,
-      segs
-    })
-  }
-
-}
-
-
-function attachSegs({ segs, parentEl }: { segs: Seg[], parentEl: HTMLElement }) {
-  for (let seg of segs) {
-    parentEl.appendChild(seg.el)
-  }
-
-  return segs
-}
-
-
-function detachSegs(segs: Seg[]) {
-  segs.forEach((seg) => removeElement(seg.el))
-}

+ 115 - 0
packages/daygrid/src/MorePopover.tsx

@@ -0,0 +1,115 @@
+import { DateComponent, DateMarker, h, EventInstanceHash, ComponentContext, createFormatter, Hit, addDays, DateRange, getDayMeta, getDayClassNames, getSegMeta } from '@fullcalendar/core'
+import TableSeg from './TableSeg'
+import TableEvent from './TableEvent'
+import Popover from './Popover'
+
+
+export interface MorePopoverProps {
+  date: DateMarker
+  segs: TableSeg[]
+  selectedInstanceId: string
+  hiddenInstances: EventInstanceHash
+  alignmentEl: HTMLElement
+  topAlignmentEl?: HTMLElement
+  onCloseClick?: () => void
+  todayRange: DateRange
+}
+
+
+export default class MorePopover extends DateComponent<MorePopoverProps> {
+
+  private popoverEl: HTMLElement
+
+
+  render(props: MorePopoverProps, state: {}, context: ComponentContext) {
+    let { options, dateEnv } = context
+    let { date, hiddenInstances, todayRange } = props
+    let title = dateEnv.format(date, createFormatter(options.dayPopoverFormat)) // TODO: cache formatter
+
+    let contentClassNames = [ 'fc-more-popover-content' ].concat(
+      getDayClassNames(
+        getDayMeta(date, todayRange),
+        context.theme
+      )
+    )
+
+    return (
+      <Popover
+        elRef={this.handlePopoverEl}
+        title={title}
+        onClose={props.onCloseClick}
+        alignmentEl={props.alignmentEl}
+        topAlignmentEl={props.topAlignmentEl}
+      >
+        <div className={contentClassNames.join(' ')}>
+          {props.segs.map((seg) => {
+            let { eventRange } = seg
+            let instanceId = eventRange.instance.instanceId
+
+            return (
+              <div
+                class='fc-daygrid-event-harness'
+                key={instanceId}
+                style={{
+                  visibility: hiddenInstances[instanceId] ? 'hidden' : ''
+                }}
+              >
+                <TableEvent
+                  seg={seg}
+                  isDragging={false}
+                  isResizing={false}
+                  isDateSelecting={false}
+                  isSelected={instanceId === props.selectedInstanceId}
+                  {...getSegMeta(seg, todayRange)}
+                />
+              </div>
+            )
+          })}
+        </div>
+      </Popover>
+    )
+  }
+
+
+  handlePopoverEl = (popoverEl: HTMLElement | null) => {
+    this.popoverEl = popoverEl
+
+    if (popoverEl) {
+      this.context.calendar.registerInteractiveComponent(this, {
+        el: popoverEl,
+        useEventCenter: false
+      })
+    } else {
+      this.context.calendar.unregisterInteractiveComponent(this)
+    }
+  }
+
+
+  queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit | null {
+    let date = this.props.date
+
+    if (positionLeft < elWidth && positionTop < elHeight) {
+      return {
+        component: this,
+        dateSpan: {
+          allDay: true,
+          range: { start: date, end: addDays(date, 1) }
+        },
+        dayEl: this.popoverEl,
+        rect: {
+          left: 0,
+          top: 0,
+          right: elWidth,
+          bottom: elHeight
+        },
+        layer: 1
+      }
+    }
+  }
+
+
+  isPopover() {
+    return true // gross
+  }
+
+}

+ 10 - 7
packages/daygrid/src/Popover.tsx

@@ -1,6 +1,6 @@
 import {
-  h, ComponentChildren, createRef,
-  applyStyle, BaseComponent, ComponentContext, DelayedRunner
+  h, ComponentChildren,
+  applyStyle, BaseComponent, ComponentContext, DelayedRunner, Ref
 } from '@fullcalendar/core'
 
 
@@ -11,6 +11,7 @@ export interface PopoverProps {
   alignmentEl: HTMLElement
   topAlignmentEl?: HTMLElement
   onClose?: () => void
+  elRef?: Ref<HTMLDivElement>
 }
 
 const PADDING_FROM_VIEWPORT = 10
@@ -19,7 +20,6 @@ const SCROLL_DEBOUNCE = 10
 
 export default class Popover extends BaseComponent<PopoverProps> {
 
-  private rootElRef = createRef<HTMLDivElement>()
   private repositioner = new DelayedRunner(this.updateSize.bind(this))
 
 
@@ -28,7 +28,7 @@ export default class Popover extends BaseComponent<PopoverProps> {
     let classNames = [ 'fc-popover', context.theme.getClass('popover'), props.extraClassName ]
 
     return (
-      <div class={classNames.join(' ')} ref={this.rootElRef}>
+      <div class={classNames.join(' ')} ref={props.elRef}>
         <div class={'fc-header ' + theme.getClass('popoverHeader')}>
           <span class='fc-title'>
             {props.title}
@@ -59,7 +59,7 @@ export default class Popover extends BaseComponent<PopoverProps> {
   // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
   handleDocumentMousedown = (ev) => {
     let { onClose } = this.props
-    let rootEl = this.rootElRef.current
+    let rootEl = this.base // bad
 
     // only hide the popover if the click happened outside the popover
     if (onClose && !rootEl.contains(ev.target)) {
@@ -82,14 +82,17 @@ export default class Popover extends BaseComponent<PopoverProps> {
   }
 
 
+  // TODO: adjust on window resize
+
+
   /*
   NOTE: the popover is position:fixed, so coordinates are relative to the viewport
   NOTE: the PARENT calls this as well, on window resize. we would have wanted to use the repositioner,
         but need to ensure that all other components have updated size first (for alignmentEl)
   */
-  updateSize() {
+  private updateSize() {
     let { alignmentEl, topAlignmentEl } = this.props
-    let rootEl = this.rootElRef.current
+    let rootEl = this.base as HTMLElement // BAD
 
     if (!rootEl) {
       return // not sure why this was null, but we shouldn't let external components call updateSize() anyway

+ 139 - 556
packages/daygrid/src/Table.tsx

@@ -1,35 +1,37 @@
 import {
-  h, Fragment, createRef,
-  insertAfterElement,
-  findDirectChildren,
-  removeElement,
-  PositionCache,
-  addDays,
   EventSegUiInteractionState,
-  Seg,
-  intersectRanges,
-  EventRenderRange,
-  ComponentContext,
-  subrenderer,
-  createFormatter,
   VNode,
-  RefMap,
   DateComponent,
+  RefObject,
+  CssDimValue,
+  h,
+  PositionCache,
+  Ref,
+  memoize,
+  addDays,
+  RefMap,
   setRef,
-  RefObject
+  ComponentContext,
+  DateRange,
+  NowTimer,
+  DateMarker,
+  DateProfile,
 } 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, { TableBaseProps } from './TableSkeleton'
-
+import TableSeg, { splitSegsByRow, splitInteractionByRow } from './TableSeg'
+import TableRow, { RowMoreLinkArg } from './TableRow'
+import { TableCellModel } from './TableCell'
+import MorePopover from './MorePopover'
 
-/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
-----------------------------------------------------------------------------------------------------------------------*/
 
-export interface TableProps extends TableBaseProps {
+export interface TableProps {
+  elRef?: Ref<HTMLDivElement>
+  cells: TableCellModel[][] // cells-BY-ROW
+  dateProfile: DateProfile
+  renderRowIntro?: () => VNode
+  colGroupNode: VNode
+  vGrowRows: boolean
+  clientWidth: CssDimValue
+  clientHeight: CssDimValue
   businessHourSegs: TableSeg[]
   bgEventSegs: TableSeg[]
   fgEventSegs: TableSeg[]
@@ -37,270 +39,170 @@ export interface TableProps extends TableBaseProps {
   eventSelection: string
   eventDrag: EventSegUiInteractionState | null
   eventResize: EventSegUiInteractionState | null
-  colGroupNode: VNode
   eventLimit: boolean | number
-  vGrow: boolean
-  headerAlignElRef?: RefObject<HTMLElement> // for more popover alignment
-}
-
-export interface TableSeg extends Seg {
-  row: number
-  firstCol: number
-  lastCol: number
+  headerAlignElRef?: RefObject<HTMLElement>
 }
 
 interface TableState {
-  segPopover: SegPopoverState
+  morePopoverState: MorePopoverState | null
 }
 
-interface SegPopoverState {
-  origFgSegs: Seg[]
-  date: Date
-  title: string
-  fgSegs: Seg[]
-  alignmentEl: HTMLElement
+interface MorePopoverState extends RowMoreLinkArg {
+  currentFgEventSegs: TableSeg[]
 }
 
 
 export default class Table extends DateComponent<TableProps, TableState> {
 
-  private renderFgEvents = subrenderer(TableEvents)
-  private renderMirrorEvents = subrenderer(TableMirrorEvents)
-  private renderBgEvents = subrenderer(TableFills)
-  private renderBusinessHours = subrenderer(TableFills)
-  private renderHighlight = subrenderer(TableFills)
-  private popoverRef = createRef<Popover>()
+  private splitBusinessHourSegs = memoize(splitSegsByRow)
+  private splitBgEventSegs = memoize(splitSegsByRow)
+  private splitFgEventSegs = memoize(splitSegsByRow)
+  private splitDateSelectionSegs = memoize(splitSegsByRow)
+  private splitEventDrag = memoize(splitInteractionByRow)
+  private splitEventResize = memoize(splitInteractionByRow)
   private rootEl: HTMLElement
-  private rowElRefs = new RefMap<HTMLTableRowElement>()
-  private cellElRefs: RefMap<HTMLTableCellElement>[] = []
+  private rowRefs = new RefMap<TableRow>()
+  private rowPositions: PositionCache
+  private colPositions: PositionCache
 
-  rowStructs: any
-  rowPositions: PositionCache
-  colPositions: PositionCache
 
+  render(props: TableProps, state: TableState, context: ComponentContext) {
+    let { morePopoverState } = state
+    let rowCnt = props.cells.length
 
-  render(props: TableProps) {
+    let businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, rowCnt)
+    let bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, rowCnt)
+    let fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, rowCnt)
+    let dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, rowCnt)
+    let eventDragByRow = this.splitEventDrag(props.eventDrag, rowCnt)
+    let eventResizeByRow = this.splitEventResize(props.eventResize, rowCnt)
 
-    // 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()
-      }
+    let classNames = [ 'fc-daygrid' ]
+    if (props.vGrowRows && props.eventLimit === true) {
+      classNames.push('fc-daygrid-constantrowheight')
     }
 
     return (
-      <Fragment>
-        <TableSkeleton
-          rootElRef={this.handleRootEl}
-          dateProfile={props.dateProfile}
-          cells={props.cells}
-          isRigid={props.isRigid}
-          renderNumberIntro={props.renderNumberIntro}
-          renderBgIntro={props.renderBgIntro}
-          renderIntro={props.renderIntro}
-          colWeekNumbersVisible={props.colWeekNumbersVisible}
-          cellWeekNumbersVisible={props.cellWeekNumbersVisible}
-          colGroupNode={props.colGroupNode}
-          rowElRefs={this.rowElRefs}
-          cellElRefs={this.cellElRefs}
-          vGrow={props.vGrow}
-          clientWidth={props.clientWidth}
-        />
-        {this.renderPopover()}
-      </Fragment>
+      <div class={classNames.join(' ')} ref={this.handleRootEl}>
+        <NowTimer unit='day' content={(nowDate: DateMarker, todayRange: DateRange) => [
+          <table style={{
+            width: props.clientWidth,
+            height: props.vGrowRows ? props.clientHeight : ''
+          }}>
+            {props.colGroupNode}
+            <tbody>
+              {props.cells.map((cells, row) => (
+                <TableRow
+                  ref={this.rowRefs.createRef(row)}
+                  key={cells[0].date.toISOString() /* best? or put key on cell? or use diff formatter? */}
+                  enableNumbers={rowCnt > 1}
+                  todayRange={todayRange}
+                  dateProfile={props.dateProfile}
+                  cells={cells}
+                  renderIntro={props.renderRowIntro}
+                  businessHourSegs={businessHourSegsByRow[row]}
+                  eventSelection={props.eventSelection}
+                  bgEventSegs={bgEventSegsByRow[row]}
+                  fgEventSegs={fgEventSegsByRow[row]}
+                  dateSelectionSegs={dateSelectionSegsByRow[row]}
+                  eventDrag={eventDragByRow[row]}
+                  eventResize={eventResizeByRow[row]}
+                  eventLimit={props.eventLimit}
+                  clientWidth={props.clientWidth}
+                  onMoreClick={this.handleMoreLinkClick}
+                />
+              ))}
+            </tbody>
+          </table>,
+          (morePopoverState && morePopoverState.currentFgEventSegs === props.fgEventSegs) && // clear popover on event mod
+            <MorePopover
+              date={state.morePopoverState.date}
+              segs={state.morePopoverState.allSegs}
+              alignmentEl={state.morePopoverState.dayEl}
+              topAlignmentEl={rowCnt === 1 ? props.headerAlignElRef.current : null}
+              onCloseClick={this.handleMorePopoverClose}
+              selectedInstanceId={props.eventSelection}
+              hiddenInstances={ // yuck
+                (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
+                (props.eventResize ? props.eventResize.affectedInstances : null) ||
+                {}
+              }
+              todayRange={todayRange}
+            />
+        ]} />
+      </div>
     )
   }
 
 
-  renderPopover() {
-    let { props } = this
-    let segPopoverState = this.state.segPopover
-
-    if (segPopoverState && segPopoverState.origFgSegs === props.fgEventSegs) { // clear on new event segs
-      return (
-        <Popover
-          extraClassName='fc-more-popover'
-          title={segPopoverState.title}
-          alignmentEl={segPopoverState.alignmentEl}
-          topAlignmentEl={props.cells.length === 1 ? props.headerAlignElRef.current : null /* align with header top when only one row */}
-          onClose={this.handlePopoverClose}
-          ref={this.popoverRef}
-        >
-          <DayTile
-            date={segPopoverState.date}
-            fgSegs={segPopoverState.fgSegs}
-            selectedInstanceId={props.eventSelection}
-            hiddenInstances={ // TODO: more convenient
-              (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
-              (props.eventResize ? props.eventResize.affectedInstances : null)
-            }
-          />
-        </Popover>
-      )
-    }
-  }
-
-
   handleRootEl = (rootEl: HTMLElement | null) => {
     this.rootEl = rootEl
-
-    if (!rootEl) {
-      this.subrenderDestroy()
-    }
-
-    if (this.props.rootElRef) {
-      setRef(this.props.rootElRef, rootEl)
+    setRef(this.props.elRef, rootEl)
+  }
+
+
+  handleMoreLinkClick = (arg: RowMoreLinkArg) => {
+    let { calendar, view, options, dateEnv } = this.context
+    let clickOption = options.eventLimitClick
+
+    if (typeof clickOption === 'function') {
+      // the returned value can be an atomic option
+      clickOption = calendar.publiclyTrigger('eventLimitClick', [
+        {
+          date: dateEnv.toDate(arg.date),
+          allDay: true,
+          dayEl: arg.dayEl,
+          moreEl: null, // moreEl, // TODO
+          segs: arg.allSegs,
+          hiddenSegs: arg.hiddenSegs,
+          jsEvent: arg.ev as MouseEvent, // TODO: better
+          view
+        }
+      ])
     }
-  }
-
-
-  componentDidMount() {
-    this.subrender()
-    this.handleSizing()
-    this.context.addResizeHandler(this.handleSizing)
-  }
-
-
-  componentDidUpdate() {
-    this.subrender()
-    this.handleSizing()
-  }
-
 
-  componentWillUnmount() {
-    this.context.removeResizeHandler(this.handleSizing)
-  }
-
-
-  subrender() {
-    let { props } = this
-    let rowEls = this.rowElRefs.collect()
-    let colCnt = props.cells[0].length
-
-    if (props.eventDrag && props.eventDrag.segs.length) { // messy check
-      this.renderHighlight({
-        type: 'highlight',
-        colGroupNode: props.colGroupNode,
-        renderIntro: props.renderIntro,
-        segs: props.eventDrag.segs,
-        rowEls,
-        colCnt
-      })
-    } else if (props.eventResize && props.eventResize.segs.length) { // messy check
-      this.renderHighlight({
-        type: 'highlight',
-        colGroupNode: props.colGroupNode,
-        renderIntro: props.renderIntro,
-        segs: props.eventResize.segs,
-        rowEls,
-        colCnt
-      })
-    } else {
-      this.renderHighlight({
-        type: 'highlight',
-        colGroupNode: props.colGroupNode,
-        renderIntro: props.renderIntro,
-        segs: props.dateSelectionSegs,
-        rowEls,
-        colCnt
+    if (clickOption === 'popover') {
+      this.setState({
+        morePopoverState: {
+          ...arg,
+          currentFgEventSegs: this.props.fgEventSegs
+        }
       })
-    }
 
-    this.renderBusinessHours({
-      type: 'businessHours',
-      colGroupNode: props.colGroupNode,
-      renderIntro: props.renderIntro,
-      segs: props.businessHourSegs,
-      rowEls,
-      colCnt
-    })
-
-    this.renderBgEvents({
-      type: 'bgEvent',
-      colGroupNode: props.colGroupNode,
-      renderIntro: props.renderIntro,
-      segs: props.bgEventSegs,
-      rowEls,
-      colCnt
-    })
-
-    let eventsRenderer = this.renderFgEvents({
-      colGroupNode: props.colGroupNode,
-      renderIntro: props.renderIntro,
-      segs: props.fgEventSegs,
-      rowEls,
-      colCnt,
-      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
-    })
-
-    this.rowStructs = eventsRenderer.rowStructs
-
-    if (props.eventResize && props.eventResize.segs.length) { // messy check
-      this.renderMirrorEvents({
-        colGroupNode: props.colGroupNode,
-        renderIntro: props.renderIntro,
-        segs: props.eventResize.segs,
-        rowEls,
-        colCnt,
-        isDragging: false,
-        isResizing: true,
-        isSelecting: false,
-        interactingSeg: props.eventResize.interactingSeg
-      })
-    } else {
-      this.renderMirrorEvents(false)
+    } else if (typeof clickOption === 'string') { // a view name
+      calendar.zoomTo(arg.date, clickOption)
     }
   }
 
 
-  handlePopoverClose = () => {
-    this.setState({ segPopover: null })
+  handleMorePopoverClose = () => {
+    this.setState({
+      morePopoverState: null
+    })
   }
 
 
-  /* Sizing
-  ------------------------------------------------------------------------------------------------------------------*/
-
+  // Hit System
+  // ----------------------------------------------------------------------------------------------------
 
-  handleSizing = () => { // TODO: make much more optimal!!!
-    this.updateEventLimitSizing()
 
+  prepareHits() {
     this.rowPositions = new PositionCache(
       this.rootEl,
-      this.rowElRefs.collect(),
+      this.rowRefs.collect().map((rowObj) => rowObj.cellElRefs.currentMap[0]), // first cell el in each row
       false,
       true // vertical
     )
 
     this.colPositions = new PositionCache(
       this.rootEl,
-      this.cellElRefs[0].collect(), // only the first row
+      this.rowRefs.currentMap[0].cellElRefs.collect(), // cell els in first row
       true, // horizontal
       false
     )
-
-    let popover = this.popoverRef.current
-    if (popover) {
-      popover.updateSize()
-    }
   }
 
 
-
-  /* Hit System
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
   positionToHit(leftPosition, topPosition) {
     let { colPositions, rowPositions } = this
 
@@ -327,13 +229,8 @@ export default class Table extends DateComponent<TableProps, TableState> {
   }
 
 
-  /* Cell System
-  ------------------------------------------------------------------------------------------------------------------*/
-  // FYI: the first column is the leftmost column, regardless of date
-
-
   private getCellEl(row, col) {
-    return this.cellElRefs[row].currentMap[col]
+    return this.rowRefs.currentMap[row].cellElRefs.currentMap[col]
   }
 
 
@@ -343,318 +240,4 @@ export default class Table extends DateComponent<TableProps, TableState> {
     return { start, end }
   }
 
-
-  /* More+ Link Popover
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  updateEventLimitSizing() {
-    let { props, rowStructs } = this
-
-    if (props.eventLimit) {
-      this._limitRows(props.eventLimit, this.rowElRefs.collect(), rowStructs, this.props.cells, this.context)
-
-    } else {
-      for (let row = 0; row < rowStructs.length; row++) {
-        this.unlimitRow(row, rowStructs)
-      }
-    }
-  }
-
-
-  // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
-  // `levelLimit` can be false (don't limit), a number, or true (should be computed).
-  _limitRows(levelLimit, rowEls, rowStructs, cells, context: ComponentContext) {
-    let row // row #
-    let rowLevelLimit
-
-    for (row = 0; row < rowStructs.length; row++) {
-      this.unlimitRow(row, rowStructs)
-
-      if (!levelLimit) {
-        rowLevelLimit = false
-      } else if (typeof levelLimit === 'number') {
-        rowLevelLimit = levelLimit
-      } else {
-        rowLevelLimit = this.computeRowLevelLimit(rowEls[row], rowStructs[row])
-      }
-
-      if (rowLevelLimit !== false) {
-        this.limitRow(row, rowLevelLimit, rowStructs, cells, context)
-      }
-    }
-  }
-
-
-  // Computes the number of levels a row will accomodate without going outside its bounds.
-  // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
-  // `row` is the row number.
-  computeRowLevelLimit(
-    rowEl, // the containing "fake" row div
-    rowStruct
-  ): (number | false) {
-    let rowBottom = rowEl.getBoundingClientRect().bottom // relative to viewport!
-    let trEls = findDirectChildren(rowStruct.tbodyEl) as HTMLTableRowElement[]
-    let i
-    let trEl: HTMLTableRowElement
-
-    // Reveal one level <tr> at a time and stop when we find one out of bounds
-    for (i = 0; i < trEls.length; i++) {
-      trEl = trEls[i]
-      trEl.classList.remove('fc-limited') // reset to original state (reveal)
-
-      if (trEl.getBoundingClientRect().bottom > rowBottom) {
-        return i
-      }
-    }
-
-    return false // should not limit at all
-  }
-
-
-  // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
-  // `row` is the row number.
-  // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
-  limitRow(row, levelLimit, rowStructs, cells, context: ComponentContext) {
-    let colCnt = cells[0].length
-    let rowStruct = rowStructs[row]
-    let moreNodes = [] // array of "more" <a> links and <td> DOM nodes
-    let col = 0 // col #, left-to-right (not chronologically)
-    let levelSegs // array of segment objects in the last allowable level, ordered left-to-right
-    let cellMatrix // a matrix (by level, then column) of all <td> elements in the row
-    let limitedNodes // array of temporarily hidden level <tr> and segment <td> DOM nodes
-    let i
-    let seg
-    let segsBelow // array of segment objects below `seg` in the current `col`
-    let totalSegsBelow // total number of segments below `seg` in any of the columns `seg` occupies
-    let colSegsBelow // array of segment arrays, below seg, one for each column (offset from segs's first column)
-    let td: HTMLTableCellElement
-    let rowSpan
-    let segMoreNodes // array of "more" <td> cells that will stand-in for the current seg's cell
-    let j
-    let moreTd: HTMLTableCellElement
-    let moreWrap
-    let moreLink
-
-    // Iterates through empty level cells and places "more" links inside if need be
-    let emptyCellsUntil = (endCol) => { // goes from current `col` to `endCol`
-      while (col < endCol) {
-        segsBelow = getCellSegs(rowStructs[row], col, levelLimit)
-        if (segsBelow.length) {
-          td = cellMatrix[levelLimit - 1][col]
-          moreLink = this.renderMoreLink(row, col, segsBelow, cells, rowStructs, context)
-          moreWrap = document.createElement('div')
-          moreWrap.appendChild(moreLink)
-          td.appendChild(moreWrap)
-          moreNodes.push(moreWrap)
-        }
-        col++
-      }
-    }
-
-    if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
-      levelSegs = rowStruct.segLevels[levelLimit - 1]
-      cellMatrix = rowStruct.cellMatrix
-
-      limitedNodes = findDirectChildren(rowStruct.tbodyEl).slice(levelLimit) // get level <tr> elements past the limit
-      limitedNodes.forEach(function(node) {
-        node.classList.add('fc-limited') // hide elements and get a simple DOM-nodes array
-      })
-
-      // iterate though segments in the last allowable level
-      for (i = 0; i < levelSegs.length; i++) {
-        seg = levelSegs[i]
-        let { firstCol, lastCol } = seg
-
-        emptyCellsUntil(firstCol) // process empty cells before the segment
-
-        // determine *all* segments below `seg` that occupy the same columns
-        colSegsBelow = []
-        totalSegsBelow = 0
-        while (col <= lastCol) {
-          segsBelow = getCellSegs(rowStructs[row], col, levelLimit)
-          colSegsBelow.push(segsBelow)
-          totalSegsBelow += segsBelow.length
-          col++
-        }
-
-        if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
-          td = cellMatrix[levelLimit - 1][firstCol] // the segment's parent cell
-          rowSpan = td.rowSpan || 1
-          segMoreNodes = []
-
-          // make a replacement <td> for each column the segment occupies. will be one for each colspan
-          for (j = 0; j < colSegsBelow.length; j++) {
-            moreTd = document.createElement('td')
-            moreTd.className = 'fc-more-cell'
-            moreTd.rowSpan = rowSpan
-            segsBelow = colSegsBelow[j]
-            moreLink = this.renderMoreLink(
-              row,
-              firstCol + j,
-              [ seg ].concat(segsBelow), // count seg as hidden too
-              cells,
-              rowStructs,
-              context
-            )
-            moreWrap = document.createElement('div')
-            moreWrap.appendChild(moreLink)
-            moreTd.appendChild(moreWrap)
-            segMoreNodes.push(moreTd)
-            moreNodes.push(moreTd)
-          }
-
-          td.classList.add('fc-limited')
-          insertAfterElement(td, segMoreNodes)
-
-          limitedNodes.push(td)
-        }
-      }
-
-      emptyCellsUntil(colCnt) // finish off the level
-      rowStruct.moreEls = moreNodes // for easy undoing later
-      rowStruct.limitedEls = limitedNodes // for easy undoing later
-    }
-  }
-
-
-  // Reveals all levels and removes all "more"-related elements for a grid's row.
-  // `row` is a row number.
-  unlimitRow(row, rowStructs) {
-    let rowStruct = rowStructs[row]
-
-    if (rowStruct.moreEls) {
-      rowStruct.moreEls.forEach(removeElement)
-      rowStruct.moreEls = null
-    }
-
-    if (rowStruct.limitedEls) {
-      rowStruct.limitedEls.forEach(function(limitedEl) {
-        limitedEl.classList.remove('fc-limited')
-      })
-      rowStruct.limitedEls = null
-    }
-  }
-
-
-  // Renders an <a> element that represents hidden event element for a cell.
-  // Responsible for attaching click handler as well.
-  renderMoreLink(row, col, hiddenSegs, cells, rowStructs, context: ComponentContext) {
-    let { calendar, view, dateEnv, options } = context
-
-    let a = document.createElement('a')
-    a.className = 'fc-more'
-    a.innerText = getMoreLinkText(hiddenSegs.length, options)
-    a.addEventListener('click', (ev) => {
-      let clickOption = options.eventLimitClick
-      let date = cells[row][col].date
-      let moreEl = ev.currentTarget as HTMLElement
-      let dayEl = this.getCellEl(row, col)
-      let allSegs = getCellSegs(rowStructs[row], col)
-
-      // rescope the segments to be within the cell's date
-      let reslicedAllSegs = resliceDaySegs(allSegs, date)
-      let reslicedHiddenSegs = resliceDaySegs(hiddenSegs, date)
-
-      if (typeof clickOption === 'function') {
-        // the returned value can be an atomic option
-        clickOption = calendar.publiclyTrigger('eventLimitClick', [
-          {
-            date: dateEnv.toDate(date),
-            allDay: true,
-            dayEl: dayEl,
-            moreEl: moreEl,
-            segs: reslicedAllSegs,
-            hiddenSegs: reslicedHiddenSegs,
-            jsEvent: ev,
-            view
-          }
-        ])
-      }
-
-      if (clickOption === 'popover') {
-        let date = cells[row][col].date
-        let title = dateEnv.format(date, createFormatter(options.dayPopoverFormat)) // TODO: cache formatter
-
-        this.setState({
-          segPopover: {
-            origFgSegs: this.props.fgEventSegs,
-            date,
-            title,
-            fgSegs: reslicedAllSegs,
-            alignmentEl: dayEl
-          }
-        })
-
-      } else if (typeof clickOption === 'string') { // a view name
-        calendar.zoomTo(date, clickOption)
-      }
-    })
-
-    return a
-  }
-
-}
-
-
-// Given the events within an array of segment objects, reslice them to be in a single day
-function resliceDaySegs(segs, dayDate) {
-  let dayStart = dayDate
-  let dayEnd = addDays(dayStart, 1)
-  let dayRange = { start: dayStart, end: dayEnd }
-  let newSegs = []
-
-  for (let seg of segs) {
-    let eventRange = seg.eventRange
-    let origRange = eventRange.range
-    let slicedRange = intersectRanges(origRange, dayRange)
-
-    if (slicedRange) {
-      newSegs.push({
-        ...seg,
-        eventRange: {
-          def: eventRange.def,
-          ui: { ...eventRange.ui, durationEditable: false }, // hack to disable resizing
-          instance: eventRange.instance,
-          range: slicedRange
-        } as EventRenderRange,
-        isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(),
-        isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf()
-      })
-    }
-  }
-
-  return newSegs
-}
-
-
-// Generates the text that should be inside a "more" link, given the number of events it represents
-function getMoreLinkText(num, options) {
-  let opt = options.eventLimitText
-
-  if (typeof opt === 'function') {
-    return opt(num)
-  } else {
-    return '+' + num + ' ' + opt
-  }
-}
-
-
-// Returns segments within a given cell.
-// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
-function getCellSegs(rowStruct, col, startLevel?) {
-  let segMatrix = rowStruct.segMatrix
-  let level = startLevel || 0
-  let segs = []
-  let seg
-
-  while (level < segMatrix.length) {
-    seg = segMatrix[level][col]
-    if (seg) {
-      segs.push(seg)
-    }
-    level++
-  }
-
-  return segs
 }

+ 165 - 0
packages/daygrid/src/TableCell.tsx

@@ -0,0 +1,165 @@
+import {
+  createFormatter,
+  Ref,
+  ComponentChildren,
+  h,
+  DateMarker,
+  DateComponent,
+  ComponentContext,
+  GotoAnchor,
+  CssDimValue,
+  setRef,
+  MountHook,
+  ClassNamesHook,
+  InnerContentHook,
+  getDayClassNames,
+  DateProfile,
+  DateRange,
+  getDayMeta
+} from '@fullcalendar/core'
+
+
+export interface TableCellProps extends TableCellModel {
+  elRef?: Ref<HTMLTableCellElement>
+  innerElRef?: Ref<HTMLDivElement>
+  bgContent: ComponentChildren
+  fgContentElRef?: Ref<HTMLDivElement>
+  fgContent: ComponentChildren
+  fgPaddingBottom: CssDimValue
+  hasEvents: boolean
+  moreCnt: number
+  moreMarginTop: number
+  showDayNumber: boolean
+  showWeekNumber: boolean
+  dateProfile: DateProfile
+  todayRange: DateRange
+  onMoreClick?: (arg: MoreLinkArg) => void
+}
+
+export interface TableCellModel {
+  date: DateMarker
+  htmlAttrs?: object
+}
+
+export interface MoreLinkArg {
+  date: DateMarker
+  moreCnt: number
+  dayEl: HTMLElement
+  ev: UIEvent
+}
+
+export interface HookProps {
+  date: Date
+  isPast: boolean
+  isFuture: boolean
+  isToday: boolean
+  hasEvents: boolean
+}
+
+const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
+const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
+
+
+export default class TableCell extends DateComponent<TableCellProps> {
+
+  render(props: TableCellProps, state: {}, context: ComponentContext) {
+    let { dateEnv, options } = context
+    let { date } = props
+    let dateStr = dateEnv.formatIso(date, { omitTime: true })
+    let zonedDate = dateEnv.toDate(date)
+    let dayMeta = getDayMeta(date, props.todayRange, props.dateProfile)
+    let staticProps = { date: zonedDate }
+    let dynamicProps = {
+      ...staticProps,
+      ...dayMeta,
+      hasEvents: props.hasEvents
+    }
+
+    let standardClassNames = [ 'fc-daygrid-day' ].concat(
+      getDayClassNames(dayMeta, context.theme)
+    )
+
+    return (
+      <MountHook
+        name='dateCell'
+        handlerProps={staticProps}
+        content={(rootElRef: Ref<HTMLTableCellElement>) => (
+          <ClassNamesHook
+            name='dateCell'
+            handlerProps={dynamicProps}
+            content={(customClassNames) => (
+              <td
+                class={standardClassNames.concat(customClassNames).join(' ')}
+                {...props.htmlAttrs}
+                data-date={dateStr}
+                ref={(el: HTMLTableCellElement | null) => {
+                  setRef(props.elRef, el)
+                  setRef(rootElRef, el)
+                }}
+              >
+                <div class='fc-daygrid-day-inner' ref={props.innerElRef /* different from hook system! */}>
+                  {props.showWeekNumber &&
+                    <div class='fc-daygrid-week-number'>
+                      <GotoAnchor
+                        navLinks={options.navLinks}
+                        gotoOptions={{ date, type: 'week' }}
+                        extraAttrs={{
+                          'data-fc-width-content': 1
+                        }}
+                      >{dateEnv.format(date, WEEK_NUM_FORMAT)}</GotoAnchor>
+                    </div>
+                  }
+                  {props.showDayNumber &&
+                    <div class='fc-daygrid-day-header'>
+                      <GotoAnchor
+                        navLinks={options.navLinks}
+                        gotoOptions={date}
+                        extraAttrs={{ 'class': 'fc-day-number' }}
+                      >{dateEnv.format(date, DAY_NUM_FORMAT)}</GotoAnchor>
+                    </div>
+                  }
+                  <InnerContentHook
+                    name='dateCell'
+                    innerProps={dynamicProps}
+                    outerContent={(innerContentParentRef, innerContent, anySpecified) => (
+                      anySpecified && (
+                        <div class='fc-daygrid-day-misc' ref={innerContentParentRef}>{innerContent}</div>
+                      )
+                    )}
+                  />
+                  <div
+                    class='fc-daygrid-day-events'
+                    ref={props.fgContentElRef}
+                    style={{ paddingBottom: props.fgPaddingBottom }}
+                  >
+                    {props.fgContent}
+                    {Boolean(props.moreCnt) &&
+                      <div class='fc-more' style={{ marginTop: props.moreMarginTop }}>
+                        <a onClick={this.handleMoreLink}>+{props.moreCnt} more</a>
+                      </div>
+                    }
+                  </div>
+                  {props.bgContent}
+                </div>
+              </td>
+            )}
+          />
+        )}
+      />
+    )
+  }
+
+
+  handleMoreLink = (ev: UIEvent) => {
+    let { props } = this
+    if (props.onMoreClick) {
+      props.onMoreClick({
+        date: props.date,
+        moreCnt: props.moreCnt,
+        dayEl: this.base as HTMLElement, // TODO: bad pattern
+        ev
+      })
+    }
+  }
+
+}

+ 26 - 0
packages/daygrid/src/TableEvent.tsx

@@ -0,0 +1,26 @@
+import { h, StandardEvent, BaseComponent, MinimalEventProps } from '@fullcalendar/core'
+
+
+const DEFAULT_TIME_FORMAT = {
+  hour: 'numeric',
+  minute: '2-digit',
+  omitZeroMinute: true,
+  meridiem: 'narrow'
+}
+
+
+export default class TableEvent extends BaseComponent<MinimalEventProps> {
+
+  render(props: MinimalEventProps) {
+    return (
+      <StandardEvent
+        {...props}
+        extraClassNames={[ 'fc-daygrid-event', 'fc-h-event' ]}
+        defaultTimeFormat={DEFAULT_TIME_FORMAT}
+        defaultDisplayEventEnd={false}
+        disableResizing={!props.seg.eventRange.def.allDay}
+      />
+    )
+  }
+
+}

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

@@ -1,284 +0,0 @@
-import {
-  VNode,
-  removeElement,
-  prependToElement,
-  Seg,
-  BaseFgEventRendererProps,
-  ComponentContext,
-  sortEventSegs,
-  subrenderer,
-  renderVNodes,
-  isArraysEqual
-} from '@fullcalendar/core'
-import CellEvents from './CellEvents'
-
-
-/* Event-rendering methods for the Table class
-----------------------------------------------------------------------------------------------------------------------*/
-
-export interface TableEventsProps extends BaseFgEventRendererProps {
-  rowEls: HTMLElement[]
-  colCnt: number
-  colGroupNode: VNode
-  renderIntro: () => VNode[]
-}
-
-export default class TableEvents extends CellEvents<TableEventsProps> {
-
-  protected attachSegs = subrenderer(attachSegs, detachSegs)
-
-  rowStructs: any
-
-
-  render(props: TableEventsProps, context: ComponentContext) {
-    let segs = this.renderSegs({
-      segs: props.segs,
-      selectedInstanceId: props.selectedInstanceId,
-      hiddenInstances: props.hiddenInstances,
-      isDragging: props.isDragging,
-      isResizing: props.isResizing,
-      isSelecting: props.isSelecting
-    }) // doesn't need interactingSeg
-
-    this.rowStructs = this.attachSegs({
-      segs,
-      rowEls: props.rowEls,
-      colCnt: props.colCnt,
-      colGroupNode: props.colGroupNode,
-      renderIntro: props.renderIntro,
-      isDragging: props.isDragging,
-      isResizing: props.isResizing,
-      isSelecting: props.isSelecting,
-      interactingSeg: props.interactingSeg
-    })
-  }
-
-
-  // Computes a default `displayEventEnd` value if one is not expliclty defined
-  computeDisplayEventEnd() {
-    return this.props.colCnt === 1 // we'll likely have space if there's only one day
-  }
-
-}
-
-TableEvents.addPropsEquality({
-  rowEls: isArraysEqual
-})
-
-
-// Renders the given foreground event segments onto the grid
-function attachSegs({ segs, rowEls, colCnt, renderIntro }: TableEventsProps, context: ComponentContext) {
-
-  let rowStructs = renderSegRows(segs, rowEls.length, colCnt, renderIntro, context)
-
-  // append to each row's content skeleton
-  rowEls.forEach(function(rowNode, i) {
-    rowNode.querySelector('.fc-content-skeleton > table').appendChild(
-      rowStructs[i].tbodyEl
-    )
-  })
-
-  return rowStructs
-}
-
-
-// Unrenders all currently rendered foreground event segments
-function detachSegs(rowStructs) {
-  for (let rowStruct of rowStructs) {
-    removeElement(rowStruct.tbodyEl)
-  }
-}
-
-
-// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
-// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
-// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
-export function renderSegRows(segs: Seg[], rowCnt: number, colCnt: number, renderIntro, context: ComponentContext) {
-  let rowStructs = []
-  let segRows
-  let row
-
-  segRows = groupSegRows(segs, rowCnt) // group into nested arrays
-
-  // iterate each row of segment groupings
-  for (row = 0; row < segRows.length; row++) {
-    rowStructs.push(
-      renderSegRow(row, segRows[row], colCnt, renderIntro, context)
-    )
-  }
-
-  return rowStructs
-}
-
-
-// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
-// the segments. Returns object with a bunch of internal data about how the render was calculated.
-// NOTE: modifies rowSegs
-function renderSegRow(row, rowSegs, colCnt: number, renderIntro, context: ComponentContext) {
-  let segLevels = buildSegLevels(rowSegs, context) // group into sub-arrays of levels
-  let levelCnt = Math.max(1, segLevels.length) // ensure at least one level
-  let tbody = document.createElement('tbody')
-  let segMatrix = [] // lookup for which segments are rendered into which level+col cells
-  let cellMatrix = [] // lookup for all <td> elements of the level+col matrix
-  let loneCellMatrix = [] // lookup for <td> elements that only take up a single column
-  let i
-  let levelSegs
-  let col
-  let tr: HTMLTableRowElement
-  let j
-  let seg
-  let td: HTMLTableCellElement
-
-  // populates empty cells from the current column (`col`) to `endCol`
-  function emptyCellsUntil(endCol) {
-    while (col < endCol) {
-      // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
-      td = (loneCellMatrix[i - 1] || [])[col]
-      if (td) {
-        td.rowSpan = (td.rowSpan || 1) + 1
-      } else {
-        td = document.createElement('td')
-        tr.appendChild(td)
-      }
-      cellMatrix[i][col] = td
-      loneCellMatrix[i][col] = td
-      col++
-    }
-  }
-
-  for (i = 0; i < levelCnt; i++) { // iterate through all levels
-    levelSegs = segLevels[i]
-    col = 0
-    tr = document.createElement('tr')
-
-    segMatrix.push([])
-    cellMatrix.push([])
-    loneCellMatrix.push([])
-
-    // levelCnt might be 1 even though there are no actual levels. protect against this.
-    // this single empty row is useful for styling.
-    if (levelSegs) {
-      for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
-        seg = levelSegs[j]
-        let { firstCol, lastCol } = seg
-
-        emptyCellsUntil(firstCol)
-
-        // create a container that occupies or more columns. append the event element.
-        td = document.createElement('td')
-        td.className = 'fc-event-container'
-        td.appendChild(seg.el)
-        if (firstCol !== lastCol) {
-          td.colSpan = lastCol - firstCol + 1
-        } else { // a single-column segment
-          loneCellMatrix[i][col] = td
-        }
-
-        while (col <= lastCol) {
-          cellMatrix[i][col] = td
-          segMatrix[i][col] = seg
-          col++
-        }
-
-        tr.appendChild(td)
-      }
-    }
-
-    emptyCellsUntil(colCnt) // finish off the row
-
-    let introEls = renderVNodes(renderIntro(), context)
-    prependToElement(tr, introEls)
-
-    tbody.appendChild(tr)
-  }
-
-  return { // a "rowStruct"
-    row: row, // the row number
-    tbodyEl: tbody,
-    cellMatrix: cellMatrix,
-    segMatrix: segMatrix,
-    segLevels: segLevels,
-    segs: rowSegs
-  }
-}
-
-
-// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
-// NOTE: modifies segs
-function buildSegLevels(segs: Seg[], context: ComponentContext) {
-  let levels = []
-  let i
-  let seg
-  let j
-
-  // Give preference to elements with certain criteria, so they have
-  // a chance to be closer to the top.
-  segs = sortEventSegs(segs, context.eventOrderSpecs)
-
-  for (i = 0; i < segs.length; i++) {
-    seg = segs[i]
-
-    // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
-    for (j = 0; j < levels.length; j++) {
-      if (!isDaySegCollision(seg, levels[j])) {
-        break
-      }
-    }
-
-    // `j` now holds the desired subrow index
-    seg.level = j
-
-    // create new level array if needed and append segment
-    ;(levels[j] || (levels[j] = [])).push(seg)
-  }
-
-  // order segments left-to-right. very important if calendar is RTL
-  for (j = 0; j < levels.length; j++) {
-    levels[j].sort(compareDaySegCols)
-  }
-
-  return levels
-}
-
-
-// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
-function groupSegRows(segs: Seg[], rowCnt: number) {
-  let segRows = []
-  let i
-
-  for (i = 0; i < rowCnt; i++) {
-    segRows.push([])
-  }
-
-  for (i = 0; i < segs.length; i++) {
-    segRows[segs[i].row].push(segs[i])
-  }
-
-  return segRows
-}
-
-
-// Computes whether two segments' columns collide. They are assumed to be in the same row.
-function isDaySegCollision(seg: Seg, otherSegs: Seg) {
-  let i
-  let otherSeg
-
-  for (i = 0; i < otherSegs.length; i++) {
-    otherSeg = otherSegs[i]
-
-    if (
-      otherSeg.firstCol <= seg.lastCol &&
-      otherSeg.lastCol >= seg.firstCol
-    ) {
-      return true
-    }
-  }
-
-  return false
-}
-
-
-// A cmp function for determining the first event
-function compareDaySegCols(a: Seg, b: Seg) {
-  return a.firstCol - b.lastCol
-}

+ 0 - 132
packages/daygrid/src/TableFills.tsx

@@ -1,132 +0,0 @@
-import {
-  VNode,
-  appendToElement,
-  prependToElement,
-  FillRenderer,
-  Seg,
-  ComponentContext,
-  removeElement,
-  BaseFillRendererProps,
-  subrenderer,
-  renderVNodes,
-  h,
-  isArraysEqual
-} from '@fullcalendar/core'
-
-
-const EMPTY_CELL_HTML = '<td style="pointer-events:none"></td>'
-
-
-export interface TableFillsProps extends BaseFillRendererProps {
-  type: string
-  rowEls: HTMLElement[]
-  colCnt: number
-  renderIntro: () => VNode[]
-  colGroupNode: VNode
-}
-
-export default class TableFills extends FillRenderer<TableFillsProps> {
-
-  fillSegTag: string = 'td' // override the default tag name
-
-  private attachSegs = subrenderer(attachSegs, detachSegs)
-
-
-  render(props: TableFillsProps) {
-    let segs = props.segs
-
-    // don't render timed background events
-    if (props.type === 'bgEvent') {
-      segs = segs.filter(function(seg) {
-        return seg.eventRange.def.allDay
-      })
-    }
-
-    segs = this.renderSegs({
-      type: props.type,
-      segs
-    })
-
-    this.attachSegs({
-      type: props.type,
-      segs,
-      rowEls: props.rowEls,
-      colCnt: props.colCnt,
-      renderIntro: props.renderIntro,
-      colGroupNode: props.colGroupNode
-    })
-  }
-
-}
-
-TableFills.addPropsEquality({
-  rowEls: isArraysEqual
-})
-
-
-function attachSegs(props: TableFillsProps, context: ComponentContext) {
-  let { segs, rowEls } = props
-  let els = []
-  let i
-  let seg
-  let skeletonEl: HTMLElement
-
-  for (i = 0; i < segs.length; i++) {
-    seg = segs[i]
-    skeletonEl = renderFillRow(seg, props, context)
-    rowEls[seg.row].appendChild(skeletonEl)
-    els.push(skeletonEl)
-  }
-
-  return els
-}
-
-
-function detachSegs(els: HTMLElement[]) {
-  els.forEach(removeElement)
-}
-
-
-// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
-function renderFillRow(seg: Seg, { colCnt, type, renderIntro, colGroupNode }: TableFillsProps, context: ComponentContext): HTMLElement {
-  let startCol = seg.firstCol
-  let endCol = seg.lastCol + 1
-  let className
-  let skeletonEl: HTMLElement
-  let trEl: HTMLTableRowElement
-
-  if (type === 'businessHours') {
-    className = 'bgevent'
-  } else {
-    className = type.toLowerCase()
-  }
-
-  skeletonEl = renderVNodes(
-    <div class={'fc-' + className + '-skeleton'}>
-      <table>
-        {colGroupNode}
-        <tr />
-      </table>
-    </div>,
-    context
-  )[0] as HTMLElement
-  trEl = skeletonEl.getElementsByTagName('tr')[0]
-
-  if (startCol > 0) {
-    let emptyCellHtml = new Array(startCol + 1).join(EMPTY_CELL_HTML) // will create (startCol + 1) td's
-    appendToElement(trEl, emptyCellHtml)
-  }
-
-  ;(seg.el as HTMLTableCellElement).colSpan = endCol - startCol
-  trEl.appendChild(seg.el)
-
-  if (endCol < colCnt) {
-    let emptyCellHtml = new Array(colCnt - endCol + 1).join(EMPTY_CELL_HTML)
-    appendToElement(trEl, emptyCellHtml)
-  }
-
-  let introEls = renderVNodes(renderIntro(), context)
-  prependToElement(trEl, introEls)
-
-  return skeletonEl
-}

+ 0 - 60
packages/daygrid/src/TableMirrorEvents.tsx

@@ -1,60 +0,0 @@
-import {
-  subrenderer, ComponentContext, removeElement, renderVNodes, h
-} from '@fullcalendar/core'
-import TableEvents, { renderSegRows, TableEventsProps } from './TableEvents'
-
-
-export default class TableMirrorEvents extends TableEvents {
-
-  protected attachSegs = subrenderer(attachSegs, detachSegs)
-
-}
-
-
-// Renders the given foreground event segments onto the grid
-function attachSegs({ segs, rowEls, colCnt, colGroupNode, renderIntro, interactingSeg }: TableEventsProps, context: ComponentContext) {
-
-  let rowStructs = renderSegRows(segs, rowEls.length, colCnt, renderIntro, context)
-  let skeletonEls: HTMLElement[] = []
-
-  // inject each new event skeleton into each associated row
-  rowEls.forEach(function(rowNode, row) {
-    let skeletonEl = renderVNodes(
-      <div class='fc-mirror-skeleton'>
-        <table>
-          {colGroupNode}
-        </table>
-      </div>,
-      context
-    )[0] as HTMLElement
-    let skeletonTopEl: HTMLElement
-    let skeletonTop
-
-    // If there is an original segment, match the top position. Otherwise, put it at the row's top level
-    if (interactingSeg && interactingSeg.row === row) {
-      skeletonTopEl = interactingSeg.el
-    } else {
-      skeletonTopEl = rowNode.querySelector('.fc-content-skeleton tbody')
-
-      if (!skeletonTopEl) { // when no events
-        skeletonTopEl = rowNode.querySelector('.fc-content-skeleton table')
-      }
-    }
-
-    skeletonTop = skeletonTopEl.getBoundingClientRect().top -
-      rowNode.getBoundingClientRect().top // the offsetParent origin
-
-    skeletonEl.style.top = skeletonTop + 'px'
-    skeletonEl.querySelector('table').appendChild(rowStructs[row].tbodyEl)
-
-    rowNode.appendChild(skeletonEl)
-    skeletonEls.push(skeletonEl)
-  })
-
-  return skeletonEls
-}
-
-
-function detachSegs(skeletonEls: HTMLElement[]) {
-  skeletonEls.forEach(removeElement)
-}

+ 382 - 0
packages/daygrid/src/TableRow.tsx

@@ -0,0 +1,382 @@
+import {
+  EventSegUiInteractionState,
+  VNode,
+  DateComponent,
+  h,
+  PositionCache,
+  isPropsEqual,
+  RefMap,
+  mapHash,
+  CssDimValue,
+  intersectRanges,
+  addDays,
+  EventRenderRange,
+  DateRange,
+  ComponentContext,
+  getSegMeta,
+  DateProfile,
+  Fragment
+} from '@fullcalendar/core'
+import TableSeg, { splitSegsByFirstCol } from './TableSeg'
+import TableCell, { TableCellModel, MoreLinkArg } from './TableCell'
+import TableEvent from './TableEvent'
+import { computeFgSegPlacement } from './event-placement'
+
+
+// TODO: attach to window resize?
+
+
+export interface TableRowProps {
+  cells: TableCellModel[]
+  renderIntro?: () => VNode
+  businessHourSegs: TableSeg[]
+  bgEventSegs: TableSeg[]
+  fgEventSegs: TableSeg[]
+  dateSelectionSegs: TableSeg[]
+  eventSelection: string
+  eventDrag: EventSegUiInteractionState | null
+  eventResize: EventSegUiInteractionState | null
+  eventLimit: boolean | number
+  clientWidth: CssDimValue
+  onMoreClick?: (arg: RowMoreLinkArg) => void
+  dateProfile: DateProfile
+  todayRange: DateRange
+  enableNumbers: boolean
+}
+
+export interface RowMoreLinkArg extends MoreLinkArg {
+  allSegs: TableSeg[]
+  hiddenSegs: TableSeg[]
+}
+
+interface TableRowState {
+  cellInnerPositions: PositionCache
+  cellContentPositions: PositionCache
+  maxContentHeight: number | null
+  segHeights: { [instanceId: string]: number } | null
+}
+
+
+export default class TableRow extends DateComponent<TableRowProps, TableRowState> {
+
+  public cellElRefs = new RefMap<HTMLTableCellElement>()
+  private cellInnerElRefs = new RefMap<HTMLElement>()
+  private cellContentElRefs = new RefMap<HTMLDivElement>()
+  private segHarnessRefs = new RefMap<HTMLDivElement>()
+
+
+  render(props: TableRowProps, state: TableRowState, context: ComponentContext) {
+    let enableWeekNumbers = context.options.weekNumbers
+    let colCnt = props.cells.length
+
+    let businessHoursByCol = splitSegsByFirstCol(props.businessHourSegs, colCnt)
+    let bgEventSegsByCol = splitSegsByFirstCol(props.bgEventSegs, colCnt)
+    let highlightSegsByCol = splitSegsByFirstCol(this.getHighlightSegs(), colCnt)
+    let mirrorSegsByCol = splitSegsByFirstCol(this.getMirrorSegs(), colCnt)
+
+    let { paddingBottoms, segsByCol, segIsNoDisplay, segTops, segMarginTops, moreCnts, moreTops } = computeFgSegPlacement(
+      props.fgEventSegs,
+      props.eventLimit,
+      state.segHeights,
+      state.maxContentHeight,
+      colCnt,
+      context.eventOrderSpecs
+    )
+
+    let interactionAffectedInstances = // TODO: messy way to compute this
+      (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
+      (props.eventResize ? props.eventResize.affectedInstances : null) ||
+      {}
+
+    return (
+      <tr>
+        {props.renderIntro && props.renderIntro()}
+        {props.cells.map((cell, col) => {
+          let normalFgNodes = this.renderFgSegs(
+            segsByCol[col],
+            segIsNoDisplay,
+            segTops,
+            segMarginTops,
+            interactionAffectedInstances,
+            props.todayRange
+          )
+
+          let mirrorFgNodes = this.renderFgSegs(
+            mirrorSegsByCol[col],
+            {},
+            segTops, // use same tops as real rendering
+            {},
+            {},
+            props.todayRange,
+            Boolean(props.eventDrag && props.eventDrag.segs.length), // messy check
+            Boolean(props.eventResize && props.eventResize.segs.length), // messy check
+            false // date-selecting (because mirror is never drawn for date selection)
+          )
+
+          return (
+            <TableCell
+              elRef={this.cellElRefs.createRef(col)}
+              innerElRef={this.cellInnerElRefs.createRef(col) /* rename */}
+              date={cell.date}
+              showDayNumber={props.enableNumbers}
+              showWeekNumber={props.enableNumbers && enableWeekNumbers && col === 0}
+              dateProfile={props.dateProfile}
+              todayRange={props.todayRange}
+              htmlAttrs={cell.htmlAttrs}
+              moreCnt={moreCnts[col]}
+              moreMarginTop={moreTops[col] /* rename */}
+              onMoreClick={this.handleMoreClick}
+              hasEvents={Boolean(normalFgNodes.length)}
+              fgPaddingBottom={paddingBottoms[col]}
+              fgContentElRef={this.cellContentElRefs.createRef(col)}
+              fgContent={[
+                <Fragment>{normalFgNodes}</Fragment>, // Fragment scopes the keys
+                <Fragment>{mirrorFgNodes}</Fragment>
+              ]}
+              bgContent={[
+                <Fragment>{this.renderFillSegs(highlightSegsByCol[col], 'fc-highlight')}</Fragment>, // Fragment scopes the keys
+                <Fragment>{this.renderFillSegs(businessHoursByCol[col], 'fc-nonbusiness')}</Fragment>,
+                <Fragment>{this.renderFillSegs(bgEventSegsByCol[col], 'fc-bgevent')}</Fragment>
+              ]}
+            />
+          )
+        })}
+      </tr>
+    )
+  }
+
+
+  componentDidMount() {
+    this.updateSizing(true, false)
+  }
+
+
+  componentDidUpdate(prevProps: TableRowProps, prevState: TableRowState) {
+    this.updateSizing(
+      !isPropsEqual(prevProps, this.props),
+      prevState.cellContentPositions !== this.state.cellContentPositions
+    )
+  }
+
+
+  handleMoreClick = (arg: MoreLinkArg) => {
+    if (this.props.onMoreClick) {
+      let allSegs = resliceDaySegs(this.props.fgEventSegs, arg.date)
+      let hiddenSegs = allSegs.slice(allSegs.length - arg.moreCnt)
+
+      this.props.onMoreClick({
+        ...arg,
+        allSegs,
+        hiddenSegs
+      })
+    }
+  }
+
+
+  getHighlightSegs(): TableSeg[] {
+    let { props } = this
+
+    if (props.eventDrag && props.eventDrag.segs.length) { // messy check
+      return props.eventDrag.segs as TableSeg[]
+
+    } else if (props.eventResize && props.eventResize.segs.length) { // messy check
+      return props.eventResize.segs as TableSeg[]
+
+    } else {
+      return props.dateSelectionSegs
+    }
+  }
+
+
+  getMirrorSegs(): TableSeg[] {
+    let { props } = this
+
+    if (props.eventResize && props.eventResize.segs.length) { // messy check
+      return props.eventResize.segs as TableSeg[]
+
+    } else {
+      return []
+    }
+  }
+
+
+  renderFgSegs(
+    segs: TableSeg[],
+    segIsNoDisplay: { [instanceId: string]: boolean },
+    segTops: { [instanceId: string]: number },
+    segMarginTops: { [instanceId: string]: number },
+    segIsInvisible: { [instanceId: string]: any },
+    todayRange: DateRange,
+    isDragging?: boolean,
+    isResizing?: boolean,
+    isDateSelecting?: boolean
+  ) {
+    let { context } = this
+    let { eventSelection } = this.props
+    let { cellInnerPositions, cellContentPositions } = this.state
+    let nodes: VNode[] = []
+
+    if (cellInnerPositions && cellContentPositions) {
+      for (let seg of segs) {
+        let { eventRange } = seg
+        let instanceId = eventRange.instance.instanceId
+        let isMirror = isDragging || isResizing || isDateSelecting
+        let isAbsolute = isMirror || seg.firstCol !== seg.lastCol || !seg.isStart || !seg.isEnd // TODO: simpler way? NOT DRY
+        let marginTop: CssDimValue
+        let top: CssDimValue
+        let left: CssDimValue
+        let right: CssDimValue
+
+        if (!isAbsolute) {
+          marginTop = segMarginTops[instanceId]
+
+        } else {
+          top = segTops[instanceId]
+
+          // TODO: cache these left/rights so that when vertical coords come around, don't need to recompute?
+          if (context.isRtl) {
+            right = seg.isStart ? 0 : cellContentPositions.rights[seg.firstCol] - cellInnerPositions.rights[seg.firstCol]
+            left = (seg.isEnd ? cellContentPositions.lefts[seg.lastCol] : cellInnerPositions.lefts[seg.lastCol])
+              - cellContentPositions.lefts[seg.firstCol]
+          } else {
+            left = seg.isStart ? 0 : cellInnerPositions.lefts[seg.firstCol] - cellContentPositions.lefts[seg.firstCol]
+            right = cellContentPositions.rights[seg.firstCol]
+              - (seg.isEnd ? cellContentPositions.rights[seg.lastCol] : cellInnerPositions.rights[seg.lastCol])
+          }
+        }
+
+        nodes.push(
+          <div
+            class={'fc-daygrid-event-harness' + (isAbsolute ? ' fc-daygrid-event-harness-abs' : '')}
+            key={instanceId}
+            ref={isMirror ? null : this.segHarnessRefs.createRef(instanceId)}
+            style={{
+              display: segIsNoDisplay[instanceId] ? 'none' : '',
+              visibility: segIsInvisible[instanceId] ? 'hidden' : '',
+              marginTop: marginTop || '',
+              top: top || '',
+              left: left || '',
+              right: right || ''
+            }}
+          >
+            <TableEvent
+              seg={seg}
+              isDragging={isDragging}
+              isResizing={isResizing}
+              isDateSelecting={isDateSelecting}
+              isSelected={instanceId === eventSelection}
+              {...getSegMeta(seg, todayRange)}
+            />
+          </div>
+        )
+      }
+    }
+
+    return nodes
+  }
+
+
+  renderFillSegs(segs: TableSeg[], className: string) {
+    let { isRtl } = this.context
+    let { cellInnerPositions } = this.state
+    let nodes: VNode[] = []
+
+    if (cellInnerPositions) {
+      for (let seg of segs) {
+
+        let leftRightCss = isRtl ? {
+          right: '',
+          left: cellInnerPositions.lefts[seg.lastCol] - cellInnerPositions.lefts[seg.firstCol]
+        } : {
+          left: '',
+          right: cellInnerPositions.rights[seg.firstCol] - cellInnerPositions.rights[seg.lastCol],
+        }
+
+        nodes.push(
+          <div class={className} style={leftRightCss} />
+        )
+      }
+    }
+
+    return nodes
+  }
+
+
+  updateSizing(isExternalChange, isHorizontalChange) {
+    if (
+      isExternalChange &&
+      this.props.clientWidth // positioning ready?
+    ) {
+      let cellInnerEls = this.cellInnerElRefs.collect()
+      let cellContentEls = this.cellContentElRefs.collect()
+      let offsetParent = cellContentEls[0].offsetParent as HTMLElement
+
+      this.setState({
+        cellInnerPositions: new PositionCache(
+          offsetParent,
+          cellInnerEls,
+          true, // isHorizontal
+          false
+        ),
+        cellContentPositions: new PositionCache(
+          offsetParent,
+          cellContentEls,
+          true, // isHorizontal (for computeFgSegPlacement)
+          true // isVertical (for computeMaxContentHeight)
+        )
+      })
+
+    } else if (isHorizontalChange) {
+      let oldSegHeights = this.state.segHeights
+
+      this.setState({
+        maxContentHeight: this.props.eventLimit === true ? this.computeMaxContentHeight() : null,
+        segHeights: mapHash(this.segHarnessRefs.currentMap, (eventHarnessEl: HTMLElement, instanceId) => (
+          eventHarnessEl.getBoundingClientRect().height ||
+            oldSegHeights[instanceId] || 0 // if seg is hidden for +more link, use previously queried height
+        ))
+      })
+    }
+  }
+
+
+  computeMaxContentHeight() {
+    let contentEl = this.cellContentElRefs.currentMap[0]
+    let cellEl = this.cellElRefs.currentMap[0]
+
+    // contentEl guaranteed not to have bottom margin
+    return cellEl.getBoundingClientRect().bottom - contentEl.getBoundingClientRect().top
+  }
+
+}
+
+
+// Given the events within an array of segment objects, reslice them to be in a single day
+function resliceDaySegs(segs, dayDate) {
+  let dayStart = dayDate
+  let dayEnd = addDays(dayStart, 1)
+  let dayRange = { start: dayStart, end: dayEnd }
+  let newSegs = []
+
+  for (let seg of segs) {
+    let eventRange = seg.eventRange
+    let origRange = eventRange.range
+    let slicedRange = intersectRanges(origRange, dayRange)
+
+    if (slicedRange) {
+      newSegs.push({
+        ...seg,
+        eventRange: {
+          def: eventRange.def,
+          ui: { ...eventRange.ui, durationEditable: false }, // hack to disable resizing
+          instance: eventRange.instance,
+          range: slicedRange
+        } as EventRenderRange,
+        isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(),
+        isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf()
+      })
+    }
+  }
+
+  return newSegs
+}

+ 67 - 0
packages/daygrid/src/TableSeg.ts

@@ -0,0 +1,67 @@
+import { EventSegUiInteractionState, Seg } from '@fullcalendar/core'
+
+
+// this is a DATA STRUCTURE, not a component
+
+export default interface TableSeg extends Seg {
+  row: number
+  firstCol: number
+  lastCol: number
+}
+
+
+export function splitSegsByRow(segs: TableSeg[], rowCnt: number) {
+  let byRow: TableSeg[][] = []
+
+  for (let i = 0; i < rowCnt; i++) {
+    byRow[i] = []
+  }
+
+  for (let seg of segs) {
+    byRow[seg.row].push(seg)
+  }
+
+  return byRow
+}
+
+
+export function splitSegsByFirstCol(segs: TableSeg[], colCnt: number) {
+  let byCol: TableSeg[][] = []
+
+  for (let i = 0; i < colCnt; i++) {
+    byCol[i] = []
+  }
+
+  for (let seg of segs) {
+    byCol[seg.firstCol].push(seg)
+  }
+
+  return byCol
+}
+
+
+export function splitInteractionByRow(ui: EventSegUiInteractionState | null, rowCnt: number) {
+  let byRow: EventSegUiInteractionState[] = []
+
+  if (!ui) {
+    for (let i = 0; i < rowCnt; i++) {
+      byRow[i] = null
+    }
+
+  } else {
+    for (let i = 0; i < rowCnt; i++) {
+      byRow[i] = {
+        affectedInstances: ui.affectedInstances,
+        isEvent: ui.isEvent,
+        interactingSeg: ui.interactingSeg,
+        segs: []
+      }
+    }
+
+    for (let seg of ui.segs) {
+      byRow[seg.row].segs.push(seg)
+    }
+  }
+
+  return byRow
+}

+ 0 - 153
packages/daygrid/src/TableSkeleton.tsx

@@ -1,153 +0,0 @@
-import {
-  VNode, h,
-  DateProfile,
-  DateMarker,
-  BaseComponent,
-  RefMap,
-  Ref,
-  CssDimValue
-} from '@fullcalendar/core'
-import DayBgRow from './DayBgRow'
-import TableSkeletonDayCell from './TableSkeletonDayCell'
-
-
-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
-    // isRigid determines whether the individual rows should ignore the contents and be a constant height.
-    // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
-  renderNumberIntro: (row: number, cells: any) => VNode[]
-  renderBgIntro: () => VNode[]
-  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
-  vGrow: boolean
-  clientWidth: CssDimValue
-}
-
-export interface CellModel {
-  date: DateMarker
-  htmlAttrs?: object
-}
-
-
-export default class TableSkeleton extends BaseComponent<TableSkeletonProps> {
-
-
-  render(props: TableSkeletonProps) {
-    let rowCnt = this.props.cells.length
-    let rowNodes: VNode[] = []
-
-    for (let row = 0; row < rowCnt; row++) {
-      rowNodes.push(
-        this.renderDayRow(row)
-      )
-    }
-
-    return (
-      <div class={'fc-day-grid' + (props.vGrow ? ' vgrow' : '')} ref={props.rootElRef} style={{ width: props.clientWidth }}>
-        {rowNodes}
-      </div>
-    )
-  }
-
-
-  // Generates the HTML for a single row, which is a div that wraps a table.
-  // `row` is the row number.
-  renderDayRow(row) {
-    let { theme } = this.context
-    let { props } = this
-    let classes = [ 'fc-row', 'fc-week', theme.getClass('bordered') ]
-
-    if (props.isRigid) {
-      classes.push('fc-rigid')
-    }
-
-    return (
-      <div class={classes.join(' ')} ref={props.rowElRefs.createRef(row)}>
-        <div class='fc-bg'>
-          <table class={theme.getClass('table')}>
-            {props.colGroupNode}
-            <tbody>
-              <DayBgRow
-                cells={props.cells[row]}
-                dateProfile={props.dateProfile}
-                renderIntro={props.renderBgIntro}
-                cellElRefs={props.cellElRefs[row]}
-              />
-            </tbody>
-          </table>
-        </div>
-        <div class='fc-content-skeleton'>
-          <table>
-            {props.colGroupNode}
-            {this.getIsNumbersVisible() &&
-              <thead>
-                {this.renderNumberTr(row)}
-              </thead>
-            }
-          </table>
-        </div>
-      </div>
-    )
-  }
-
-
-  getIsNumbersVisible() {
-    let { props } = this
-
-    return this.getIsDayNumbersVisible(props.cells.length) ||
-      props.cellWeekNumbersVisible ||
-      props.colWeekNumbersVisible
-  }
-
-
-  getIsDayNumbersVisible(rowCnt) {
-    return rowCnt > 1
-  }
-
-
-  renderNumberTr(row: number) {
-    let { props } = this
-    let intro = props.renderNumberIntro(row, props.cells)
-
-    return (
-      <tr>
-        {intro}
-        {this.renderNumberCells(row)}
-      </tr>
-    )
-  }
-
-
-  // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
-  // The number row will only exist if either day numbers or week numbers are turned on.
-  renderNumberCells(row) {
-    let { cells, dateProfile, cellWeekNumbersVisible } = this.props
-    let isDayNumbersVisible =this.getIsDayNumbersVisible(cells.length)
-    let colCnt = cells[row].length
-    let parts: VNode[] = []
-
-    for (let col = 0; col < colCnt; col++) {
-      parts.push(
-        <TableSkeletonDayCell
-          date={cells[row][col].date}
-          dateProfile={dateProfile}
-          isDayNumbersVisible={isDayNumbersVisible}
-          cellWeekNumbersVisible={cellWeekNumbersVisible}
-        />
-      )
-    }
-
-    return parts
-  }
-
-}

+ 0 - 77
packages/daygrid/src/TableSkeletonDayCell.tsx

@@ -1,77 +0,0 @@
-import {
-  h,
-  DateProfile,
-  DateMarker,
-  BaseComponent,
-  rangeContainsMarker,
-  getDayClasses,
-  GotoAnchor,
-  ComponentContext,
-  createFormatter
-} from '@fullcalendar/core'
-
-
-export interface TableSkeletonDayCellProps {
-  date: DateMarker
-  dateProfile: DateProfile
-  isDayNumbersVisible: boolean
-  cellWeekNumbersVisible: boolean
-}
-
-const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
-const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
-
-
-export default class TableSkeletonDayCell extends BaseComponent<TableSkeletonDayCellProps> {
-
-
-  render(props: TableSkeletonDayCellProps, state: {}, context: ComponentContext) {
-    let { dateEnv, options } = context
-    let { date, dateProfile, cellWeekNumbersVisible } = this.props
-    let isDateValid = rangeContainsMarker(dateProfile.activeRange, date) // TODO: called too frequently. cache somehow.
-    let isDayNumberVisible = props.isDayNumbersVisible && isDateValid
-    let weekCalcFirstDow
-
-    if (!isDayNumberVisible && !cellWeekNumbersVisible) {
-      // no numbers in day cell (week number must be along the side)
-      return (<td></td>) //  will create an empty space above events :(
-    }
-
-    let classNames = getDayClasses(date, dateProfile, this.context)
-    classNames.unshift('fc-day-top')
-
-    let dateStr = dateEnv.formatIso(date, { omitTime: true })
-    let attrs = {} as any
-    if (isDateValid) {
-      attrs['data-date'] = dateStr
-    }
-
-    if (cellWeekNumbersVisible) {
-      weekCalcFirstDow = dateEnv.weekDow
-    }
-
-    return (
-      <td
-        key={dateStr /* fresh rerender for new date, mostly because of dayRender */}
-        class={classNames.join(' ')}
-        {...attrs}
-      >
-        {(cellWeekNumbersVisible && (date.getUTCDay() === weekCalcFirstDow)) &&
-          <GotoAnchor
-            navLinks={options.navLinks}
-            gotoOptions={{ date, type: 'week' }}
-            extraAttrs={{ 'class': 'fc-week-number' }}
-          >{dateEnv.format(date, WEEK_NUM_FORMAT)}</GotoAnchor>
-        }
-        {isDayNumberVisible &&
-          <GotoAnchor
-            navLinks={options.navLinks}
-            gotoOptions={date}
-            extraAttrs={{ 'class': 'fc-day-number' }}
-          >{dateEnv.format(date, DAY_NUM_FORMAT)}</GotoAnchor>
-        }
-      </td>
-    )
-  }
-
-}

+ 1 - 127
packages/daygrid/src/TableView.tsx

@@ -1,10 +1,7 @@
 import {
   VNode, h,
-  createFormatter,
   View,
-  memoize,
   getViewClassNames,
-  GotoAnchor,
   SimpleScrollGrid,
   SimpleScrollGridSection,
   ChunkContentCallbackArgs,
@@ -13,9 +10,6 @@ import {
 import TableDateProfileGenerator from './TableDateProfileGenerator'
 
 
-const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
-
-
 /* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells.
 ----------------------------------------------------------------------------------------------------------------------*/
 // It is a manager for a Table subcomponent, which does most of the heavy lifting.
@@ -24,22 +18,12 @@ const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
 
 export default abstract class TableView<State={}> extends View<State> {
 
-  protected processOptions = memoize(this._processOptions)
   protected headerElRef = createRef<HTMLTableCellElement>()
-  private colWeekNumbersVisible: boolean // computed option
 
 
   renderLayout(headerRowContent: VNode | null, bodyContent: (contentArg: ChunkContentCallbackArgs) => VNode) {
     let { props } = this
     let classNames = getViewClassNames(props.viewSpec).concat('fc-dayGrid-view')
-
-    this.processOptions(this.context.options)
-
-    let cols = []
-    if (this.colWeekNumbersVisible) {
-      cols.push({ width: 'shrink' })
-    }
-
     let sections: SimpleScrollGridSection[] = []
 
     if (headerRowContent) {
@@ -65,123 +49,13 @@ export default abstract class TableView<State={}> extends View<State> {
         <SimpleScrollGrid
           vGrow={!props.isHeightAuto}
           forPrint={props.forPrint}
-          cols={cols}
+          cols={[] /* TODO: make optional? */}
           sections={sections}
         />
       </div>
     )
   }
 
-
-  private _processOptions(options) {
-    let cellWeekNumbersVisible: boolean
-    let colWeekNumbersVisible: boolean
-
-    if (options.weekNumbers) {
-      if (options.weekNumbersWithinDays) {
-        cellWeekNumbersVisible = true
-        colWeekNumbersVisible = false
-      } else {
-        cellWeekNumbersVisible = false
-        colWeekNumbersVisible = true
-      }
-    } else {
-      colWeekNumbersVisible = false
-      cellWeekNumbersVisible = false
-    }
-
-    this.colWeekNumbersVisible = colWeekNumbersVisible
-
-    return { cellWeekNumbersVisible, colWeekNumbersVisible }
-  }
-
-
-  /* Header Rendering
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  // Generates the HTML that will go before the day-of week header cells
-  renderHeadIntro = (): VNode[] => {
-    let { options } = this.context
-
-    if (this.colWeekNumbersVisible) {
-      return [
-        <th class='shrink fc-week-number'>
-          <div data-fc-width-all={1}>
-            <span data-fc-width-content={1}>
-              {options.weekLabel}
-            </span>
-          </div>
-        </th>
-      ]
-    }
-
-    return []
-  }
-
-
-  /* Table Rendering
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
-  renderNumberIntro = (row: number, cells: any): VNode[] => {
-    let { options, dateEnv } = this.context
-    let weekStart = cells[row][0].date
-    let colCnt = cells[0].length
-
-    if (this.colWeekNumbersVisible) {
-      return [
-        <td class='fc-week-number shrink'>
-          <div data-fc-width-all={1}>
-            <GotoAnchor
-              navLinks={options.navLinks}
-              gotoOptions={{ date: weekStart, type: 'week', forceOff: colCnt === 1 }}
-              extraAttrs={{
-                'data-fc-width-content': 1
-              }}
-            >{dateEnv.format(weekStart, WEEK_NUM_FORMAT)}</GotoAnchor>
-          </div>
-        </td>
-      ]
-    }
-
-    return []
-  }
-
-
-  // Generates the HTML that goes before the day bg cells for each day-row
-  renderBgIntro = (): VNode[] => {
-    if (this.colWeekNumbersVisible) {
-      return [
-        <td class='fc-week-number'></td>
-      ]
-    }
-
-    return []
-  }
-
-
-  // Generates the HTML that goes before every other type of row generated by Table.
-  // Affects mirror-skeleton and highlight-skeleton rows.
-  renderIntro = (): VNode[] => {
-
-    if (this.colWeekNumbersVisible) {
-      return [
-        <td class='fc-week-number '></td>
-      ]
-    }
-
-    return []
-  }
-
 }
 
 TableView.prototype.dateProfileGeneratorClass = TableDateProfileGenerator
-
-
-export function isEventLimitAuto(options) { // TODO: use in other places?
-  let eventLimit = options.eventLimit
-
-  return eventLimit && typeof eventLimit !== 'number'
-}

+ 199 - 0
packages/daygrid/src/event-placement.ts

@@ -0,0 +1,199 @@
+import TableSeg, { splitSegsByFirstCol } from './TableSeg'
+import { sortEventSegs } from '@fullcalendar/core'
+
+
+interface TableSegPlacement {
+  seg: TableSeg
+  top: number
+  bottom: number
+}
+
+
+export function computeFgSegPlacement( // for one row. TODO: print mode?
+  segs: TableSeg[],
+  eventLimit: boolean | number,
+  eventHeights: { [instanceId: string]: number } | null,
+  maxContentHeight: number | null,
+  colCnt: number,
+  eventOrderSpecs: any
+) {
+  let colPlacements: TableSegPlacement[][] = []
+  let moreCnts: number[] = [] // by-col
+  let segIsNoDisplay: { [instanceId: string]: boolean } = {}
+  let segTops: { [instanceId: string]: number } = {} // always populated for each seg
+  let segMarginTops: { [instanceId: string]: number } = {} // simetimes populated for each seg
+  let moreTops: { [col: string]: number } = {}
+  let paddingBottoms: { [col: string]: number } = {} // for each cell's inner-wrapper div
+  let segsByCol: TableSeg[][]
+
+  for (let i = 0; i < colCnt; i++) {
+    colPlacements.push([])
+    moreCnts.push(0)
+  }
+
+  segs = sortEventSegs(segs, eventOrderSpecs) as TableSeg[]
+
+  if (eventHeights) {
+
+    // TODO: try all seg placements and choose the topmost! dont quit after first
+    // SOLUTION: when placed, insert into colPlacements
+    for (let seg of segs) {
+      placeSeg(seg, eventHeights[seg.eventRange.instance.instanceId])
+    }
+
+    // sort. for eventLimit and segTops computation
+    for (let placements of colPlacements) {
+      placements.sort(cmpPlacements)
+    }
+
+    segsByCol = colPlacements.map(extractFirstColSegs) // operates on the sorted cols
+
+    if (eventLimit === true) { // assumes maxContentHeight
+
+      for (let col = 0; col < colCnt; col++) {
+        let placements = colPlacements[col]
+        let hiddenCnt = 0
+        let i
+
+        for (i = placements.length - 1; i >= 0; i--) {
+          if (placements[i].bottom > maxContentHeight) {
+            segIsNoDisplay[placements[i].seg.eventRange.instance.instanceId] = true
+            hiddenCnt++
+          } else {
+            break
+          }
+        }
+
+        // remove the lowest remaining, to make space for the +more link
+        // `i` is on the lowest valid seg
+        if (hiddenCnt && i > 0) {
+          segIsNoDisplay[placements[i].seg.eventRange.instance.instanceId] = true
+          hiddenCnt++
+        }
+
+        moreCnts[col] = hiddenCnt
+      }
+
+    } else if (eventLimit) {
+
+      for (let col = 0; col < colCnt; col++) {
+        let placements = colPlacements[col]
+        let hiddenCnt = 0
+
+        for (let i = eventLimit; i < placements.length; i++) {
+          segIsNoDisplay[placements[i].seg.eventRange.instance.instanceId] = true
+          hiddenCnt++
+        }
+
+        moreCnts[col] = hiddenCnt
+      }
+    }
+
+    // computes segTops/segMarginTops/moreTops/paddingBottoms
+    for (let col = 0; col < colCnt; col++) {
+      let placements = colPlacements[col]
+      let currentBottom = 0
+      let currentExtraSpace = 0
+
+      for (let placement of placements) {
+        let seg = placement.seg
+
+        if (!segIsNoDisplay[seg.eventRange.instance.instanceId]) {
+
+          segTops[seg.eventRange.instance.instanceId] = placement.top // from top of container
+
+          if (seg.firstCol === seg.lastCol && seg.isStart && seg.isEnd) { // TODO: simpler way? NOT DRY
+
+            segMarginTops[seg.eventRange.instance.instanceId] =
+              placement.top - currentBottom // from previous seg bottom
+              + currentExtraSpace
+
+            currentExtraSpace = 0
+
+          } else { // multi-col event, abs positioned
+            currentExtraSpace += placement.bottom - placement.top // for future non-abs segs
+          }
+
+          currentBottom = placement.bottom
+        }
+      }
+
+      if (currentExtraSpace) {
+        if (moreCnts[col]) {
+          moreTops[col] = currentExtraSpace
+        } else {
+          paddingBottoms[col] = currentExtraSpace
+        }
+      }
+    }
+
+  } else {
+    segsByCol = splitSegsByFirstCol(segs, colCnt)
+  }
+
+  function placeSeg(seg, segHeight) {
+    if (!tryPlaceSegAt(seg, segHeight, 0)) {
+      for (let col = seg.firstCol; col <= seg.lastCol; col++) {
+        for (let placement of colPlacements[col]) { // will repeat multi-day segs!!!!!!! bad!!!!!!
+          if (tryPlaceSegAt(seg, segHeight, placement.bottom)) {
+            return
+          }
+        }
+      }
+    }
+  }
+
+  function tryPlaceSegAt(seg, segHeight, top) {
+    if (canPlaceSegAt(seg, segHeight, top)) {
+      for (let col = seg.firstCol; col <= seg.lastCol; col++) {
+        colPlacements[col].push({
+          seg,
+          top,
+          bottom: top + segHeight
+        })
+      }
+      return true
+    } else {
+      return false
+    }
+  }
+
+  function canPlaceSegAt(seg, segHeight, top) {
+    for (let col = seg.firstCol; col <= seg.lastCol; col++) {
+      for (let placement of colPlacements[col]) {
+        if (top < placement.bottom && top + segHeight > placement.top) { // collide?
+          return false
+        }
+      }
+    }
+    return true
+  }
+
+  return {
+    segsByCol,
+    segIsNoDisplay,
+    segTops,
+    segMarginTops,
+    moreCnts,
+    moreTops,
+    paddingBottoms
+  }
+}
+
+
+function extractFirstColSegs(oneColPlacements: TableSegPlacement[], col: number) {
+  let segs: TableSeg[] = []
+
+  for (let placement of oneColPlacements) {
+    if (placement.seg.firstCol === col) {
+      segs.push(placement.seg)
+    }
+  }
+
+  return segs
+}
+
+
+function cmpPlacements(placement0, placement1) {
+  return placement0.top - placement1.top
+}

+ 1 - 2
packages/daygrid/src/main.scss

@@ -1,4 +1,3 @@
 
-@import './styles/day-grid';
+@import './styles/daygrid';
 @import './styles/event';
-@import './styles/event-limit';

+ 4 - 3
packages/daygrid/src/main.ts

@@ -3,10 +3,11 @@ import DayTableView from './DayTableView'
 import './main.scss'
 
 export { default as DayTable, DayTableSlicer } from './DayTable'
-export { default as Table, TableSeg } from './Table'
-export { default as TableView, isEventLimitAuto } from './TableView'
+export { default as Table } from './Table'
+export { default as TableSeg } from './TableSeg'
+export { TableCellModel } from './TableCell'
+export { default as TableView } from './TableView'
 export { buildDayTableModel } from './DayTableView'
-export { default as DayBgRow, DayBgCellModel } from './DayBgRow'
 export { DayTableView as DayGridView } // export as old name!
 
 export default createPlugin({

+ 0 - 169
packages/daygrid/src/styles/_day-grid.scss

@@ -1,169 +0,0 @@
-
-// for the VIEW (TODO: put in other file
-
-.fc-dayGrid-view .fc-week-number {
-  white-space: nowrap;
-}
-
-
-/* DayGridV
---------------------------------------------------------------------------------------------------*/
-
-.fc-day-grid {
-  position: relative; // because containers z-index'd children
-  z-index: 1;         // "
-  display: flex;
-  flex-direction: column;
-}
-
-.fc-day-grid .fc-row {
-  flex-grow: 1;
-  position: relative; // for housing the skeletons
-}
-
-.fc-day-grid .fc-row.fc-rigid { // TODO: make this a modifier class on fc-day-grid
-  // a "rigid" row will take up a constant amount of height because content-skeleton is absolute
-  overflow: hidden;
-}
-
-
-// BORDER
-
-.fc-day-grid .fc-row {
-  border-width: 1px 0 0;
-
-  &:first-child {
-    border-top-width: 0;
-  }
-}
-
-.fc-unthemed .fc-day-grid .fc-row {
-  border-style: solid;
-  border-color: $fc-unthemed-border-color;
-  border-color: var(--fc-unthemed-border-color, $fc-unthemed-border-color);
-}
-
-
-// SKELETONS
-// the order:
-// .fc-bg
-// .fc-bgevent-skeleton
-// .fc-highlight-skeleton
-// .fc-content-skeleton
-// .fc-mirror-skeleton
-
-.fc-day-grid .fc-row .fc-content-skeleton {
-  position: relative;
-}
-
-.fc-day-grid .fc-row > .fc-bg,
-.fc-day-grid .fc-row > .fc-bgevent-skeleton,
-.fc-day-grid .fc-row > .fc-highlight-skeleton,
-.fc-day-grid .fc-row.fc-rigid > .fc-content-skeleton,
-.fc-day-grid .fc-row > .fc-mirror-skeleton {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-}
-
-.fc-day-grid .fc-row > .fc-bg,
-.fc-day-grid .fc-row > .fc-bgevent-skeleton,
-.fc-day-grid .fc-row > .fc-highlight-skeleton,
-.fc-day-grid .fc-row > .fc-mirror-skeleton {
-  & > table {
-    height: 100%; // everything but the content-skeleton creates height
-
-    td, th {
-      height: 100%;
-    }
-  }
-}
-
-.fc-day-grid .fc-row > .fc-bg { z-index: 1; }
-.fc-day-grid .fc-row > .fc-bgevent-skeleton { z-index: 2;}
-.fc-day-grid .fc-row > .fc-highlight-skeleton { z-index: 3; }
-.fc-day-grid .fc-row > .fc-content-skeleton { z-index: 4; }
-.fc-day-grid .fc-row > .fc-mirror-skeleton { z-index: 5; }
-
-
-// CONTENT SKELETON
-
-.fc-day-grid .fc-row.fc-rigid > .fc-content-skeleton {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-}
-
-.fc-day-grid .fc-week-number,
-.fc-day-grid .fc-day-number {
-  padding: 2px;
-}
-
-.fc-day-grid .fc-day-top.fc-other-month { // TODO: why is other-month not on fc-day?
-  opacity: 0.3;
-}
-
-.fc-day-grid .fc-day-top .fc-week-number {
-  min-width: 1.5em;
-  text-align: center;
-  background-color: #f2f2f2;
-  color: #808080;
-}
-
-.fc-ltr .fc-day-grid .fc-day-top .fc-day-number { float: right; }
-.fc-rtl .fc-day-grid .fc-day-top .fc-day-number { float: left; }
-
-.fc-ltr .fc-day-grid .fc-day-top .fc-week-number { float: left; border-radius: 0 0 3px 0; }
-.fc-rtl .fc-day-grid .fc-day-top .fc-week-number { float: right; border-radius: 0 0 0 3px; }
-
-.fc-day-grid .fc-row > .fc-bgevent-skeleton,
-.fc-day-grid .fc-row > .fc-highlight-skeleton,
-.fc-day-grid .fc-row > .fc-content-skeleton,
-.fc-day-grid .fc-row > .fc-mirror-skeleton {
-  th, td {
-    border-top: 0;
-    border-bottom: 0;
-  }
-}
-
-
-.fc-screen {
-
-  .fc-day-grid .fc-row {
-    min-height: 4em; // ensure that all rows are at least this tall
-  }
-
-  .fc-day-grid .fc-row > .fc-content-skeleton {
-    padding-bottom: 1em; // ensure a space at bottom of cell for user selecting/clicking
-  }
-
-  .fc-day-grid .fc-row > .fc-bgevent-skeleton,
-  .fc-day-grid .fc-row > .fc-highlight-skeleton,
-  .fc-day-grid .fc-row > .fc-content-skeleton,
-  .fc-day-grid .fc-row > .fc-mirror-skeleton {
-    th, td {
-      border-color: transparent;
-    }
-  }
-
-}
-
-
-.fc-print {
-
-  .fc-day-grid .fc-row > .fc-content-skeleton > table {
-    height: 4em;
-  }
-
-  // kill the overlaid, absolutely-positioned components
-  .fc-day-grid .fc-row > .fc-bg,
-  .fc-day-grid .fc-row > .fc-bgevent-skeleton,
-  .fc-day-grid .fc-row > .fc-highlight-skeleton,
-  .fc-day-grid .fc-row > .fc-mirror-skeleton {
-    display: none;
-  }
-
-}

+ 113 - 0
packages/daygrid/src/styles/_daygrid.scss

@@ -0,0 +1,113 @@
+
+.fc-daygrid {
+  position: relative;
+  z-index: 1;
+}
+
+.fc-daygrid-day-inner {
+  position: relative;
+  min-height: 100%; // works better than height:100% for some reason
+}
+
+.scrollgrid-vgrow-cell-hack .fc-daygrid-day-inner {
+  position: static;
+}
+
+.fc-daygrid-day-header {
+  padding: 4px;
+  text-align: right;
+}
+
+.fc-daygrid-day-events:before {
+  content: "";
+  clear: both;
+  display: table;
+}
+
+.fc-daygrid-day-events {
+  margin: 0 4px;
+  margin-bottom: 0 !important; // put bottom padding on fc-daygrid-day-inner instead
+  position: relative;
+  min-height: 3em;
+}
+
+.fc-daygrid-constantrowheight .fc-daygrid-day-events {
+  position: absolute;
+  left: 0;
+  right: 0;
+  min-height: 0;
+}
+
+.fc-daygrid-event-harness { // dont do margins either!!!
+  padding-bottom: 2px;
+  // margin-top: 0 !important; // was overriding JS :(
+}
+
+.fc-daygrid-event-harness-abs {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+}
+
+
+.fc-daygrid-day {
+  .fc-bgevent,
+  .fc-highlight,
+  .fc-nonbusiness {
+    z-index: 1; // TODO: more specific
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+  }
+}
+
+.fc-daygrid .fc-event {
+  position: relative;
+  z-index: 2;
+}
+
+.fc-daygrid .fc-event.fc-event-mirror {
+  position: relative;
+  z-index: 3;
+}
+
+.fc-daygrid .fc-more {
+  position: relative;
+  z-index: 2;
+  font-size: .85em;
+
+  a {
+    cursor: pointer; // TODO: add reset?
+  }
+}
+
+.fc-daygrid .fc-popover {
+  z-index: 4;
+}
+
+.fc-more-popover-content {
+  padding: 10px;
+  min-width: 220px;
+}
+
+
+.fc-daygrid-week-number {
+  padding: 2px;
+  min-width: 1.5em;
+  text-align: center;
+  background-color: #f2f2f2;
+  color: #808080;
+}
+
+.fc-ltr .fc-daygrid-week-number {
+  float: left;
+  border-radius: 0 0 3px 0;
+}
+
+.fc-rtl .fc-daygrid-week-number {
+  float: right;
+  border-radius: 0 0 0 3px;
+}

+ 0 - 48
packages/daygrid/src/styles/_event-limit.scss

@@ -1,48 +0,0 @@
-
-// the "more" link that represents hidden events
-// TODO: have fc-daygrid-popover be a subclass
-
-a.fc-more {
-  margin: 1px 3px;
-  font-size: .85em;
-  cursor: pointer;
-  text-decoration: none;
-}
-
-a.fc-more:hover {
-  text-decoration: underline;
-}
-
-.fc-limited { // rows and cells that are hidden because of a "more" link
-  display: none;
-}
-
-.fc-more-popover {
-  z-index: 2;
-  width: 220px;
-}
-
-.fc-more-popover .fc-event-container {
-  padding: 10px;
-}
-
-
-.fc-print {
-
-  /* Undo month-view event limiting. Display all events and hide the "more" links
-  --------------------------------------------------------------------------------------------------*/
-
-  .fc-more-cell,
-  .fc-more {
-    display: none !important;
-  }
-
-  .fc tr.fc-limited {
-    display: table-row !important;
-  }
-
-  .fc td.fc-limited {
-    display: table-cell !important;
-  }
-
-}

+ 2 - 36
packages/daygrid/src/styles/_event.scss

@@ -1,38 +1,4 @@
 
-/* DayGrid events
-----------------------------------------------------------------------------------------------------
-We use the full "fc-day-grid-event" class instead of using descendants because the event won't
-be a descendant of the grid when it is being dragged.
-*/
-
-.fc-day-grid-event {
-  margin: 1px 2px 0; /* spacing between events and edges */
-  padding: 0 1px;
-}
-
-.fc-mirror-skeleton tr:first-child > td > .fc-day-grid-event {
-  margin-top: 0; /* except for mirror skeleton */
-}
-
-.fc-day-grid-event .fc-content { /* force events to be one-line tall */
-  white-space: nowrap;
-  overflow: hidden;
-}
-
-.fc-day-grid-event .fc-time {
-  font-weight: bold;
-}
-
-/* resizer (cursor devices) */
-
-/* left resizer  */
-.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,
-.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer {
-  margin-left: -2px; /* to the day cell's edge */
-}
-
-/* right resizer */
-.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,
-.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer {
-  margin-right: -2px; /* to the day cell's edge */
+.fc-daygrid-event {
+  border-radius: 3px;
 }