Adam Shaw 6 лет назад
Родитель
Сommit
d0cd9ec2c2

+ 62 - 74
packages/core/src/Toolbar.ts

@@ -1,13 +1,17 @@
 import { htmlEscape } from './util/html'
-import { htmlToElement, appendToElement, findElements, createElement, removeElement } from './util/dom-manip'
-import Component from './component/Component'
+import { htmlToElement, appendToElement, findElements, createElement } from './util/dom-manip'
+import ComponentContext from './component/ComponentContext'
+import { Component, renderer } from './view-framework'
 import { ViewSpec } from './structs/view-spec'
-import { memoizeRendering } from './component/memoized-rendering'
+import Calendar from './Calendar'
+import Theme from './theme/Theme'
+
 
 /* Toolbar with buttons and title
 ----------------------------------------------------------------------------------------------------------------------*/
 
 export interface ToolbarRenderProps {
+  extraClassName: string
   layout: any
   title: string
   activeButton: string
@@ -18,60 +22,52 @@ export interface ToolbarRenderProps {
 
 export default class Toolbar extends Component<ToolbarRenderProps> {
 
-  el: HTMLElement
-  viewsWithButtons: any
-
-  private _renderLayout = memoizeRendering(this.renderLayout, this.unrenderLayout)
-  private _updateTitle = memoizeRendering(this.updateTitle, null, [ this._renderLayout ])
-  private _updateActiveButton = memoizeRendering(this.updateActiveButton, null, [ this._renderLayout ])
-  private _updateToday = memoizeRendering(this.updateToday, null, [ this._renderLayout ])
-  private _updatePrev = memoizeRendering(this.updatePrev, null, [ this._renderLayout ])
-  private _updateNext = memoizeRendering(this.updateNext, null, [ this._renderLayout ])
+  private renderBase = renderer(this._renderBase)
+  private renderTitle = renderer(renderTitle)
+  private renderActiveButton = renderer(renderActiveButton, unrenderActiveButton)
+  private renderToday = renderer(toggleButtonEnabled.bind(null, 'today'))
+  private renderPrev = renderer(toggleButtonEnabled.bind(null, 'prev'))
+  private renderNext = renderer(toggleButtonEnabled.bind(null, 'next'))
 
+  public viewsWithButtons: string[]
 
-  constructor(extraClassName) {
-    super()
 
-    this.el = createElement('div', { className: 'fc-toolbar ' + extraClassName })
-  }
+  render(props: ToolbarRenderProps) {
 
+    let el = this.renderBase({
+      extraClassName: props.extraClassName,
+      layout: props.layout
+    })
 
-  destroy() {
-    super.destroy()
+    this.renderTitle({ el, text: props.title })
+    this.renderActiveButton({ el, buttonName: props.activeButton })
+    this.renderToday({ el, isEnabled: props.isTodayEnabled })
+    this.renderPrev({ el, isEnabled: props.isPrevEnabled })
+    this.renderNext({ el, isEnabled: props.isNextEnabled })
 
-    this._renderLayout.unrender() // should unrender everything else
-    removeElement(this.el)
+    return [ el ]
   }
 
 
-  render(props: ToolbarRenderProps) {
-    this._renderLayout(props.layout)
-    this._updateTitle(props.title)
-    this._updateActiveButton(props.activeButton)
-    this._updateToday(props.isTodayEnabled)
-    this._updatePrev(props.isPrevEnabled)
-    this._updateNext(props.isNextEnabled)
-  }
-
+  /*
+  the wrapper el and the left/center/right layout
+  */
+  _renderBase({ extraClassName , layout }, context: ComponentContext) {
+    let { theme, calendar } = context
 
-  renderLayout(layout) {
-    let { el } = this
+    let el = createElement('div', { className: 'fc-toolbar ' + extraClassName }, [
+      this.renderSection('left', layout.left, theme, calendar),
+      this.renderSection('center', layout.center, theme, calendar),
+      this.renderSection('right', layout.right, theme, calendar)
+    ])
 
     this.viewsWithButtons = []
 
-    appendToElement(el, this.renderSection('left', layout.left))
-    appendToElement(el, this.renderSection('center', layout.center))
-    appendToElement(el, this.renderSection('right', layout.right))
-  }
-
-
-  unrenderLayout() {
-    this.el.innerHTML = ''
+    return el
   }
 
 
-  renderSection(position, buttonStr) {
-    let { theme, calendar } = this.context
+  renderSection(position, buttonStr, theme: Theme, calendar: Calendar) {
     let optionsManager = calendar.optionsManager
     let viewSpecs = calendar.viewSpecs
     let sectionEl = createElement('div', { className: 'fc-' + position })
@@ -175,47 +171,39 @@ export default class Toolbar extends Component<ToolbarRenderProps> {
     return sectionEl
   }
 
-
-  updateToday(isTodayEnabled) {
-    this.toggleButtonEnabled('today', isTodayEnabled)
-  }
-
-
-  updatePrev(isPrevEnabled) {
-    this.toggleButtonEnabled('prev', isPrevEnabled)
-  }
+}
 
 
-  updateNext(isNextEnabled) {
-    this.toggleButtonEnabled('next', isNextEnabled)
-  }
+function renderTitle(props: { el: HTMLElement, text: string }) {
+  findElements(props.el, 'h2').forEach(function(titleEl) {
+    titleEl.innerText = props.text
+  })
+}
 
 
-  updateTitle(text) {
-    findElements(this.el, 'h2').forEach(function(titleEl) {
-      titleEl.innerText = text
-    })
-  }
+function renderActiveButton(props: { el: HTMLElement, buttonName: string }, context: ComponentContext) {
+  let { buttonName } = props
+  let className = context.theme.getClass('buttonActive')
 
+  findElements(props.el, 'button').forEach((buttonEl) => { // fyi, themed buttons don't have .fc-button
+    if (buttonEl.classList.contains('fc-' + buttonName + '-button')) {
+      buttonEl.classList.add(className)
+    }
+  })
+}
 
-  updateActiveButton(buttonName?) {
-    let { theme } = this.context
-    let className = theme.getClass('buttonActive')
 
-    findElements(this.el, 'button').forEach((buttonEl) => { // fyi, themed buttons don't have .fc-button
-      if (buttonName && buttonEl.classList.contains('fc-' + buttonName + '-button')) {
-        buttonEl.classList.add(className)
-      } else {
-        buttonEl.classList.remove(className)
-      }
-    })
-  }
+function unrenderActiveButton(props: { el: HTMLElement, buttonName: string }, context: ComponentContext) {
+  let className = context.theme.getClass('buttonActive')
 
+  findElements(props.el, 'button').forEach((buttonEl) => { // fyi, themed buttons don't have .fc-button
+    buttonEl.classList.remove(className)
+  })
+}
 
-  toggleButtonEnabled(buttonName, bool) {
-    findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl: HTMLButtonElement) => {
-      buttonEl.disabled = !bool
-    })
-  }
 
+function toggleButtonEnabled(buttonName: string, props: { el: HTMLElement, isEnabled: boolean }) {
+  findElements(props.el, '.fc-' + buttonName + '-button').forEach((buttonEl: HTMLButtonElement) => {
+    buttonEl.disabled = !props.isEnabled
+  })
 }

+ 22 - 26
packages/core/src/common/ScrollComponent.ts

@@ -1,6 +1,7 @@
 import { computeEdges } from '../util/dom-geom'
-import { removeElement, createElement, applyStyle, applyStyleProp } from '../util/dom-manip'
+import { createElement, applyStyle, applyStyleProp } from '../util/dom-manip'
 import { ElementScrollController } from './scroll-controller'
+import { Component } from '../view-framework'
 
 export interface ScrollbarWidths {
   left: number
@@ -8,35 +9,30 @@ export interface ScrollbarWidths {
   bottom: number
 }
 
+export interface ScrollComponentProps {
+  overflowX: string
+  overflowY: string
+}
+
 /*
 Embodies a div that has potential scrollbars
 */
-export default class ScrollComponent extends ElementScrollController {
+export default class ScrollComponent extends Component<ScrollComponentProps> {
+
+  el = createElement('div', { className: 'fc-scroller' })
+  controller = new ElementScrollController(this.el)
 
-  overflowX: string
-  overflowY: string
 
-  constructor(overflowX: string, overflowY: string) {
-    super(
-      createElement('div', {
-        className: 'fc-scroller'
-      })
-    )
-    this.overflowX = overflowX
-    this.overflowY = overflowY
-    this.applyOverflow()
+  render(props: ScrollComponentProps) {
+    this.applyOverflow(props)
+    return [ this.el ]
   }
 
 
   // sets to natural height, unlocks overflow
   clear() {
     this.setHeight('auto')
-    this.applyOverflow()
-  }
-
-
-  destroy() {
-    removeElement(this.el)
+    this.applyOverflow(this.props)
   }
 
 
@@ -44,10 +40,10 @@ export default class ScrollComponent extends ElementScrollController {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  applyOverflow() {
+  applyOverflow(props: ScrollComponentProps) {
     applyStyle(this.el, {
-      overflowX: this.overflowX,
-      overflowY: this.overflowY
+      overflowX: props.overflowX,
+      overflowY: props.overflowY
     })
   }
 
@@ -56,22 +52,22 @@ export default class ScrollComponent extends ElementScrollController {
   // Useful for preserving scrollbar widths regardless of future resizes.
   // Can pass in scrollbarWidths for optimization.
   lockOverflow(scrollbarWidths: ScrollbarWidths) {
-    let overflowX = this.overflowX
-    let overflowY = this.overflowY
+    let { controller } = this
+    let { overflowX, overflowY } = this.props
 
     scrollbarWidths = scrollbarWidths || this.getScrollbarWidths()
 
     if (overflowX === 'auto') {
       overflowX = (
           scrollbarWidths.bottom || // horizontal scrollbars?
-          this.canScrollHorizontally() // OR scrolling pane with massless scrollbars?
+          controller.canScrollHorizontally() // OR scrolling pane with massless scrollbars?
         ) ? 'scroll' : 'hidden'
     }
 
     if (overflowY === 'auto') {
       overflowY = (
           scrollbarWidths.left || scrollbarWidths.right || // horizontal scrollbars?
-          this.canScrollVertically() // OR scrolling pane with massless scrollbars?
+          controller.canScrollVertically() // OR scrolling pane with massless scrollbars?
         ) ? 'scroll' : 'hidden'
     }
 

+ 99 - 68
packages/core/src/component/renderers/FgEventRenderer.ts

@@ -5,77 +5,96 @@ import { compareByFieldSpecs } from '../../util/misc'
 import { EventUi } from '../event-ui'
 import { EventRenderRange, filterSegsViaEls, triggerRenderedSegs, triggerWillRemoveSegs } from '../event-rendering'
 import { Seg } from '../DateComponent'
+import { Component } from '../../view-framework'
 import ComponentContext from '../ComponentContext'
+import { memoize } from '../../util/memoize'
+import { renderer } from '../../view-framework'
 
 
-export default abstract class FgEventRenderer {
-
-  context: ComponentContext
+export interface BaseFgEventRendererProps {
+  segs: Seg[]
+  mirrorInfo?: any
+  selectedInstanceId?: string
+  hiddenInstances: { [instanceId: string]: any }
+}
 
-  // derived from options
-  eventTimeFormat: DateFormatter
-  displayEventTime: boolean
-  displayEventEnd: boolean
+export default abstract class FgEventRenderer<
+  FgEventRendererProps extends BaseFgEventRendererProps = BaseFgEventRendererProps
+> extends Component<FgEventRendererProps> {
 
-  segs: Seg[] = []
-  isSizeDirty: boolean = false
+  private updateComputedOptions = memoize(this._updateComputedOptions)
+  private renderSegsPlain = renderer(this._renderSegsPlain, this._unrenderSegsPlain)
+  private renderSelectedInstance = renderer(renderSelectedInstance, unrenderSelectedInstance)
+  private renderHiddenInstances = renderer(renderHiddenInstances, unrenderHiddenInstances)
 
+  // internal state
+  private segs: Seg[] = [] // for sizing funcs
+  private isSizeDirty: boolean = false // NOTE: should also flick this when attaching segs to new containers
 
-  renderSegs(context: ComponentContext, segs: Seg[], mirrorInfo?) {
-    this.context = context
+  // computed options
+  protected eventTimeFormat: DateFormatter
+  protected displayEventTime: boolean
+  protected displayEventEnd: boolean
 
-    this.rangeUpdated() // called too frequently :(
 
-    // render an `.el` on each seg
-    // returns a subset of the segs. segs that were actually rendered
-    segs = this.renderSegEls(segs, mirrorInfo)
+  renderSeg(props: FgEventRendererProps, context: ComponentContext) {
+    this.updateComputedOptions(context.options)
 
-    this.segs = segs
-    this.attachSegs(segs, mirrorInfo)
+    let segs = this.segs = this.renderSegsPlain({
+      segs: props.segs,
+      mirrorInfo: props.mirrorInfo
+    })
 
-    this.isSizeDirty = true
-    triggerRenderedSegs(this.context, this.segs, Boolean(mirrorInfo))
-  }
+    this.renderSelectedInstance({
+      segs,
+      instanceId: props.selectedInstanceId
+    })
 
+    this.renderHiddenInstances({
+      segs,
+      hiddenInstances: props.hiddenInstances
+    })
 
-  unrender(context: ComponentContext, _segs: Seg[], mirrorInfo?) {
-    triggerWillRemoveSegs(this.context, this.segs, Boolean(mirrorInfo))
-    this.detachSegs(this.segs)
-    this.segs = []
+    return segs
   }
 
 
-  abstract renderSegHtml(seg: Seg, mirrorInfo): string
-  abstract attachSegs(segs: Seg[], mirrorInfo)
-  abstract detachSegs(segs: Seg[])
-
-
-  // Updates values that rely on options and also relate to range
-  rangeUpdated() {
-    let { options } = this.context
-    let displayEventTime
-    let displayEventEnd
-
-    this.eventTimeFormat = createFormatter(
+  _updateComputedOptions(options: any) {
+    let eventTimeFormat = createFormatter(
       options.eventTimeFormat || this.computeEventTimeFormat(),
       options.defaultRangeSeparator
     )
 
-    displayEventTime = options.displayEventTime
+    let displayEventTime = options.displayEventTime
     if (displayEventTime == null) {
       displayEventTime = this.computeDisplayEventTime() // might be based off of range
     }
 
-    displayEventEnd = options.displayEventEnd
+    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, mirrorInfo } : { segs: Seg[], mirrorInfo: any }, context: ComponentContext) {
+    segs = this.renderSegEls(segs, mirrorInfo)
+    this.isSizeDirty = true
+    triggerRenderedSegs(context, segs, Boolean(mirrorInfo)) // will use publiclyTriggerAfterSizing, will fire later
+    return segs
+  }
+
+
+  _unrenderSegsPlain({ mirrorInfo }: { segs: Seg[], mirrorInfo: any }, context: ComponentContext, segs: Seg[]) {
+    triggerWillRemoveSegs(context, segs, Boolean(mirrorInfo))
+  }
+
+
   // Renders and assigns an `el` property for each foreground event segment.
   // Only returns segments that successfully rendered.
   renderSegEls(segs: Seg[], mirrorInfo) {
@@ -106,7 +125,11 @@ export default abstract class FgEventRenderer {
   }
 
 
+  abstract renderSegHtml(seg: Seg, mirrorInfo): string
+
+
   // Generic utility for generating the HTML classNames for an event segment's element
+  // TODO: move to outside func
   getSegClasses(seg: Seg, isDraggable, isResizable, mirrorInfo) {
     let classes = [
       'fc-event',
@@ -214,6 +237,7 @@ export default abstract class FgEventRenderer {
 
 
   // Utility for generating event skin-related CSS properties
+  // TODO: move to outside func
   getSkinCss(ui: EventUi) {
     return {
       'background-color': ui.backgroundColor,
@@ -223,6 +247,7 @@ export default abstract class FgEventRenderer {
   }
 
 
+  // TODO: move to outside func
   sortEventSegs(segs): Seg[] {
     let specs = this.context.eventOrderSpecs
     let objs = segs.map(buildSegCompareObj)
@@ -237,6 +262,10 @@ export default abstract class FgEventRenderer {
   }
 
 
+  // Sizing
+  // ----------------------------------------------------------------------------------------------------
+
+
   computeSizes(force: boolean) {
     if (force || this.isSizeDirty) {
       this.computeSegSizes(this.segs)
@@ -259,57 +288,59 @@ export default abstract class FgEventRenderer {
   assignSegSizes(segs: Seg[]) {
   }
 
+}
+
 
-  // Manipulation on rendered segs
+// Manipulation on rendered segs
+// ----------------------------------------------------------------------------------------------------
+// TODO: slow. use more hashes to quickly reference relevant elements
 
 
-  hideByHash(hash) {
-    if (hash) {
-      for (let seg of this.segs) {
-        if (hash[seg.eventRange.instance.instanceId]) {
-          seg.el.style.visibility = 'hidden'
-        }
+function renderHiddenInstances({ 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 = 'hidden'
       }
     }
   }
+}
 
 
-  showByHash(hash) {
-    if (hash) {
-      for (let seg of this.segs) {
-        if (hash[seg.eventRange.instance.instanceId]) {
-          seg.el.style.visibility = ''
-        }
+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 = ''
       }
     }
   }
+}
 
 
-  selectByInstanceId(instanceId: string) {
-    if (instanceId) {
-      for (let seg of this.segs) {
-        let eventInstance = seg.eventRange.instance
-        if (
-          eventInstance && eventInstance.instanceId === instanceId &&
-          seg.el // necessary?
-        ) {
-          seg.el.classList.add('fc-selected')
-        }
+function renderSelectedInstance({ segs, instanceId }: { segs: Seg[], instanceId: string }) {
+  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')
       }
     }
   }
+}
 
 
-  unselectByInstanceId(instanceId: string) {
-    if (instanceId) {
-      for (let seg of this.segs) {
-        if (seg.el) { // necessary?
-          seg.el.classList.remove('fc-selected')
-        }
+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')
       }
     }
   }
-
 }
 
 

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

@@ -106,7 +106,7 @@ export { default as ComponentContext } from './component/ComponentContext'
 export { default as DateComponent, Seg, EventSegUiInteractionState } from './component/DateComponent'
 export { default as Calendar, DatePointTransform, DateSpanTransform, DateSelectionApi } from './Calendar'
 export { default as View, ViewProps } from './View'
-export { default as FgEventRenderer, buildSegCompareObj } from './component/renderers/FgEventRenderer'
+export { default as FgEventRenderer, buildSegCompareObj, BaseFgEventRendererProps } from './component/renderers/FgEventRenderer'
 export { default as FillRenderer } from './component/renderers/FillRenderer'
 
 export { default as DateProfileGenerator, DateProfile } from './DateProfileGenerator'

+ 131 - 18
packages/list/src/ListEventRenderer.ts

@@ -3,33 +3,42 @@ import {
   FgEventRenderer,
   Seg,
   isMultiDayRange,
-  getAllDayHtml
+  getAllDayHtml,
+  BaseFgEventRendererProps,
+  ComponentContext,
+  createFormatter,
+  createElement,
+  buildGotoAnchorHtml,
+  htmlToElement,
+  renderer,
+  sortEventSegs
 } from '@fullcalendar/core'
-import ListView from './ListView'
 
 
-export default class ListEventRenderer extends FgEventRenderer {
+export interface ListEventRendererProps extends BaseFgEventRendererProps {
+  contentEl: HTMLElement
+  dayDates: Date[]
+}
 
-  listView: ListView
 
+export default class ListEventRenderer extends FgEventRenderer<ListEventRendererProps> {
 
-  constructor(listView: ListView) {
-    super()
+  attachSegs = renderer(attachSegs)
 
-    this.listView = listView
-  }
 
+  render(props: ListEventRendererProps, context: ComponentContext) {
+    let segs = this.renderSegs({
+      segs: props.segs,
+      mirrorInfo: props.mirrorInfo,
+      selectedInstanceId: props.selectedInstanceId,
+      hiddenInstances: props.hiddenInstances
+    }, context)
 
-  attachSegs(segs: Seg[]) {
-    if (!segs.length) {
-      this.listView.renderEmptyMessage()
-    } else {
-      this.listView.renderSegList(segs)
-    }
-  }
-
-
-  detachSegs() {
+    this.attachSegs({
+      segs,
+      dayDates: props.dayDates,
+      contentEl: props.contentEl
+    })
   }
 
 
@@ -104,3 +113,107 @@ export default class ListEventRenderer extends FgEventRenderer {
   }
 
 }
+
+
+function attachSegs(props: { segs, dayDates: Date[], contentEl: HTMLElement }, context: ComponentContext) {
+  if (props.segs.length) {
+    renderSegList(props.segs, props.dayDates, props.contentEl, context)
+  } else {
+    renderEmptyMessage(props.contentEl, context)
+  }
+}
+
+
+function renderEmptyMessage(contentEl: HTMLElement, context: ComponentContext) {
+  contentEl.innerHTML =
+    '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
+    '<div class="fc-list-empty-wrap1">' +
+    '<div class="fc-list-empty">' +
+      htmlEscape(context.options.noEventsMessage) +
+    '</div>' +
+    '</div>' +
+    '</div>'
+}
+
+
+function renderSegList(allSegs, dayDates: Date[], contentEl: HTMLElement, context: ComponentContext) {
+  let { theme } = context
+  let segsByDay = groupSegsByDay(allSegs) // sparse array
+  let dayIndex
+  let daySegs
+  let i
+  let tableEl = htmlToElement('<table class="fc-list-table ' + theme.getClass('tableList') + '"><tbody></tbody></table>')
+  let tbodyEl = tableEl.querySelector('tbody')
+
+  for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
+    daySegs = segsByDay[dayIndex]
+
+    if (daySegs) { // sparse array, so might be undefined
+
+      // append a day header
+      tbodyEl.appendChild(
+        buildDayHeaderRow(dayDates[dayIndex], context)
+      )
+
+      daySegs = sortEventSegs(daySegs, context.eventOrderSpecs)
+
+      for (i = 0; i < daySegs.length; i++) {
+        tbodyEl.appendChild(daySegs[i].el) // append event row
+      }
+    }
+  }
+
+  contentEl.innerHTML = '' // will unrender previous renders
+  contentEl.appendChild(tableEl)
+}
+
+
+// generates the HTML for the day headers that live amongst the event rows
+function buildDayHeaderRow(dayDate, context: ComponentContext) {
+  let { theme, dateEnv, options } = context
+  let mainFormat = createFormatter(options.listDayFormat) // TODO: cache
+  let altFormat = createFormatter(options.listDayAltFormat) // TODO: cache
+
+  return createElement('tr', {
+    className: 'fc-list-heading',
+    'data-date': dateEnv.formatIso(dayDate, { omitTime: true })
+  }, '<td class="' + (
+    theme.getClass('tableListHeading') ||
+    theme.getClass('widgetHeader')
+  ) + '" colspan="3">' +
+    (mainFormat ?
+      buildGotoAnchorHtml(
+        options,
+        dateEnv,
+        dayDate,
+        { 'class': 'fc-list-heading-main' },
+        htmlEscape(dateEnv.format(dayDate, mainFormat)) // inner HTML
+      ) :
+      '') +
+    (altFormat ?
+      buildGotoAnchorHtml(
+        options,
+        dateEnv,
+        dayDate,
+        { 'class': 'fc-list-heading-alt' },
+        htmlEscape(dateEnv.format(dayDate, altFormat)) // inner HTML
+      ) :
+      '') +
+  '</td>') as HTMLTableRowElement
+}
+
+
+// Returns a sparse array of arrays, segs grouped by their dayIndex
+function groupSegsByDay(segs) {
+  let segsByDay = [] // sparse array
+  let i
+  let seg
+
+  for (i = 0; i < segs.length; i++) {
+    seg = segs[i];
+    (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
+      .push(seg)
+  }
+
+  return segsByDay
+}

+ 47 - 164
packages/list/src/ListView.ts

@@ -1,7 +1,4 @@
 import {
-  htmlToElement,
-  createElement,
-  htmlEscape,
   subtractInnerElHeight,
   View,
   ViewProps,
@@ -9,21 +6,19 @@ import {
   DateMarker,
   addDays,
   startOfDay,
-  createFormatter,
   DateRange,
   intersectRanges,
   DateProfile,
-  buildGotoAnchorHtml,
   ComponentContext,
   EventUiHash,
   EventRenderRange,
   sliceEventStore,
   EventStore,
   memoize,
-  MemoizedRendering,
-  memoizeRendering,
   Seg,
-  ViewSpec
+  ViewSpec,
+  renderer,
+  renderViewEl
 } from '@fullcalendar/core'
 import ListEventRenderer from './ListEventRenderer'
 
@@ -32,88 +27,55 @@ Responsible for the scroller, and forwarding event-related actions into the "gri
 */
 export default class ListView extends View {
 
-  scroller: ScrollComponent
-  contentEl: HTMLElement
-
-  dayDates: DateMarker[] // TOOD: kill this. only have it because ListEventRenderer
-
   private computeDateVars = memoize(computeDateVars)
   private eventStoreToSegs = memoize(this._eventStoreToSegs)
-  private renderSkeleton = memoizeRendering(this._renderSkeleton, this._unrenderSkeleton)
-  private renderContent: MemoizedRendering<[ComponentContext, Seg[]]>
-
-
-  constructor(viewSpec: ViewSpec, parentEl: HTMLElement) {
-    super(viewSpec, parentEl)
-
-    let eventRenderer = this.eventRenderer = new ListEventRenderer(this)
-    this.renderContent = memoizeRendering(
-      eventRenderer.renderSegs.bind(eventRenderer),
-      eventRenderer.unrender.bind(eventRenderer),
-      [ this.renderSkeleton ]
-    )
-  }
+  private renderSkeleton = renderer(renderSkeleton)
+  private renderScroller = renderer(ScrollComponent)
+  private renderEvents = renderer(ListEventRenderer)
 
-
-  firstContext(context: ComponentContext) {
-    context.calendar.registerInteractiveComponent(this, {
-      el: this.el
-      // TODO: make aware that it doesn't do Hits
-    })
-  }
+  // for sizing
+  private eventRenderer: ListEventRenderer
+  private scroller: ScrollComponent
 
 
-  render(props: ViewProps, context: ComponentContext) {
-    super.render(props, context)
-
+  render(props: ViewProps) {
     let { dayDates, dayRanges } = this.computeDateVars(props.dateProfile)
-    this.dayDates = dayDates
-
-    this.renderSkeleton(context)
-
-    this.renderContent(
-      context,
-      this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges)
-    )
-  }
 
+    let rootEl = this.renderSkeleton({
+      viewSpec: props.viewSpec
+    })
 
-  destroy() {
-    super.destroy()
+    this.scroller = this.renderScroller({
+      overflowX: 'hidden',
+      overflowY: 'auto'
+    }, {
+      parent: rootEl
+    })
 
-    this.renderSkeleton.unrender()
-    this.renderContent.unrender()
+    this.eventRenderer = this.renderEvents({
+      segs: this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges),
+      dayDates,
+      contentEl: this.scroller.el,
+      selectedInstanceId: props.eventSelection, // TODO: rename
+      hiddenInstances: // TODO: more convenient
+        (props.eventDrag ? props.eventDrag.affectedEvents : null) ||
+        (props.eventResize ? props.eventResize.affectedEvents : null)
+    })
 
-    this.context.calendar.unregisterInteractiveComponent(this)
+    return [ rootEl ]
   }
 
 
-  _renderSkeleton(context: ComponentContext) {
-    let { theme } = context
-
-    this.el.classList.add('fc-list-view')
-
-    let listViewClassNames = (theme.getClass('listView') || '').split(' ') // wish we didn't have to do this
-    for (let listViewClassName of listViewClassNames) {
-      if (listViewClassName) { // in case input was empty string
-        this.el.classList.add(listViewClassName)
-      }
-    }
-
-    this.scroller = new ScrollComponent(
-      'hidden', // overflow x
-      'auto' // overflow y
-    )
-
-    this.el.appendChild(this.scroller.el)
-    this.contentEl = this.scroller.el // shortcut
+  componentDidMount() {
+    this.context.calendar.registerInteractiveComponent(this, {
+      el: this.mountedEls[0]
+      // TODO: make aware that it doesn't do Hits
+    })
   }
 
 
-  _unrenderSkeleton() {
-    // TODO: remove classNames
-
-    this.scroller.destroy() // will remove the Grid too
+  componentWillUnmount() {
+    this.context.calendar.unregisterInteractiveComponent(this)
   }
 
 
@@ -133,7 +95,7 @@ export default class ListView extends View {
 
   computeScrollerHeight(viewHeight) {
     return viewHeight -
-      subtractInnerElHeight(this.el, this.scroller.el) // everything that's NOT the scroller
+      subtractInnerElHeight(this.mountedEls[0], this.scroller.el) // everything that's NOT the scroller
   }
 
 
@@ -207,104 +169,25 @@ export default class ListView extends View {
     return segs
   }
 
+}
 
-  renderEmptyMessage() {
-    this.contentEl.innerHTML =
-      '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
-      '<div class="fc-list-empty-wrap1">' +
-      '<div class="fc-list-empty">' +
-        htmlEscape(this.context.options.noEventsMessage) +
-      '</div>' +
-      '</div>' +
-      '</div>'
-  }
-
-
-  // called by ListEventRenderer
-  renderSegList(allSegs) {
-    let { theme } = this.context
-    let segsByDay = this.groupSegsByDay(allSegs) // sparse array
-    let dayIndex
-    let daySegs
-    let i
-    let tableEl = htmlToElement('<table class="fc-list-table ' + theme.getClass('tableList') + '"><tbody></tbody></table>')
-    let tbodyEl = tableEl.querySelector('tbody')
-
-    for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
-      daySegs = segsByDay[dayIndex]
-
-      if (daySegs) { // sparse array, so might be undefined
-
-        // append a day header
-        tbodyEl.appendChild(this.buildDayHeaderRow(this.dayDates[dayIndex]))
-
-        daySegs = this.eventRenderer.sortEventSegs(daySegs)
-
-        for (i = 0; i < daySegs.length; i++) {
-          tbodyEl.appendChild(daySegs[i].el) // append event row
-        }
-      }
-    }
-
-    this.contentEl.innerHTML = ''
-    this.contentEl.appendChild(tableEl)
-  }
+ListView.prototype.fgSegSelector = '.fc-list-item' // which elements accept event actions
 
 
-  // Returns a sparse array of arrays, segs grouped by their dayIndex
-  groupSegsByDay(segs) {
-    let segsByDay = [] // sparse array
-    let i
-    let seg
+function renderSkeleton(props: { viewSpec: ViewSpec }, context: ComponentContext) {
+  let rootEl = renderViewEl(props.viewSpec.type)
+  rootEl.classList.add('fc-list-view')
 
-    for (i = 0; i < segs.length; i++) {
-      seg = segs[i];
-      (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
-        .push(seg)
+  let listViewClassNames = (context.theme.getClass('listView') || '').split(' ') // wish we didn't have to do this
+  for (let listViewClassName of listViewClassNames) {
+    if (listViewClassName) { // in case input was empty string
+      rootEl.classList.add(listViewClassName)
     }
-
-    return segsByDay
-  }
-
-
-  // generates the HTML for the day headers that live amongst the event rows
-  buildDayHeaderRow(dayDate) {
-    let { theme, dateEnv, options } = this.context
-    let mainFormat = createFormatter(options.listDayFormat) // TODO: cache
-    let altFormat = createFormatter(options.listDayAltFormat) // TODO: cache
-
-    return createElement('tr', {
-      className: 'fc-list-heading',
-      'data-date': dateEnv.formatIso(dayDate, { omitTime: true })
-    }, '<td class="' + (
-      theme.getClass('tableListHeading') ||
-      theme.getClass('widgetHeader')
-    ) + '" colspan="3">' +
-      (mainFormat ?
-        buildGotoAnchorHtml(
-          options,
-          dateEnv,
-          dayDate,
-          { 'class': 'fc-list-heading-main' },
-          htmlEscape(dateEnv.format(dayDate, mainFormat)) // inner HTML
-        ) :
-        '') +
-      (altFormat ?
-        buildGotoAnchorHtml(
-          options,
-          dateEnv,
-          dayDate,
-          { 'class': 'fc-list-heading-alt' },
-          htmlEscape(dateEnv.format(dayDate, altFormat)) // inner HTML
-        ) :
-        '') +
-    '</td>') as HTMLTableRowElement
   }
 
+  return rootEl
 }
 
-ListView.prototype.fgSegSelector = '.fc-list-item' // which elements accept event actions
-
 
 function computeDateVars(dateProfile: DateProfile) {
   let dayStart = startOfDay(dateProfile.renderRange.start)