Browse Source

tons of awesome react-style refactoring

Adam Shaw 7 năm trước cách đây
mục cha
commit
10336fe8fe

+ 387 - 314
src/Calendar.ts

@@ -1,9 +1,8 @@
-import { createElement, removeElement, applyStyle, prependToElement, forceClassName } from './util/dom-manip'
+import { createElement, removeElement, applyStyle, prependToElement } from './util/dom-manip'
 import { computeHeightAndMargins } from './util/dom-geom'
 import { listenBySelector } from './util/dom-event'
 import { capitaliseFirstLetter, debounce } from './util/misc'
 import { globalDefaults, rtlDefaults } from './options'
-import Iterator from './common/Iterator'
 import GlobalEmitter from './common/GlobalEmitter'
 import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 import { default as ListenerMixin, ListenerInterface } from './common/ListenerMixin'
@@ -22,6 +21,8 @@ import { createFormatter } from './datelib/formatting'
 import { Duration, createDuration } from './datelib/duration'
 import { CalendarState, reduce } from './reducers/main'
 import { parseSelection, SelectionInput } from './reducers/selection'
+import reselector from './util/reselector'
+import { assignTo } from './util/object'
 
 export default class Calendar {
 
@@ -43,50 +44,51 @@ export default class Calendar {
   listenTo: ListenerInterface['listenTo']
   stopListeningTo: ListenerInterface['stopListeningTo']
 
-  view: View // current View object
-  viewsByType: { [viewName: string]: View } // holds all instantiated view instances, current or not
-  theme: Theme
+  buildDateEnv: any
+  buildTheme: any
+
   optionsManager: OptionsManager
   viewSpecManager: ViewSpecManager
-
+  theme: Theme
+  dateEnv: DateEnv
   defaultAllDayEventDuration: Duration
   defaultTimedEventDuration: Duration
-  dateEnv: DateEnv
 
   el: HTMLElement
+  elThemeClassName: string
+  elDirClassName: string
   contentEl: HTMLElement
+
   suggestedViewHeight: number
   ignoreUpdateViewSize: number = 0
-  freezeContentHeightDepth: number = 0
   removeNavLinkListener: any
   windowResizeProxy: any
 
+  isRendered: boolean = false
+  isSkeletonRendered: boolean = false
+
+  view: View // current View object
+  viewsByType: { [viewName: string]: View } // holds all instantiated view instances, current or not
   header: Toolbar
   footer: Toolbar
-  toolbarsManager: Iterator
 
   state: CalendarState
-  isReducing: boolean = false
   actionQueue = []
-  isSkeletonRendered: boolean = false
+  isReducing: boolean = false
   renderingPauseDepth: number = 0
 
 
   constructor(el: HTMLElement, overrides: OptionsInput) {
-
-    // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
-    // unneeded() is called in destroy.
-    GlobalEmitter.needed()
-
     this.el = el
     this.viewsByType = {}
 
-    this.optionsManager = new OptionsManager(this, overrides)
-    this.viewSpecManager = new ViewSpecManager(this.optionsManager, this)
-    this.initDateEnv() // needs to happen after options hash initialized
-    this.watchTheme()
-    this.initToolbars()
+    this.optionsManager = new OptionsManager(overrides)
+    this.viewSpecManager = new ViewSpecManager(this.optionsManager)
+
+    this.buildDateEnv = reselector(buildDateEnv)
+    this.buildTheme = reselector(buildTheme)
 
+    this.handleOptions(this.optionsManager.computed)
     this.constructed()
     this.hydrate()
   }
@@ -102,24 +104,221 @@ export default class Calendar {
   }
 
 
-  publiclyTrigger(name: string, args) {
-    let optHandler = this.opt(name)
+  // Public API for rendering
+  // -----------------------------------------------------------------------------------------------------------------
 
-    this.triggerWith(name, this, args) // Emitter's method
 
-    if (optHandler) {
-      return optHandler.apply(this, args)
+  render() {
+    if (!this.isRendered) {
+      this.bindGlobalHandlers()
+      this.el.classList.add('fc')
+      this._render()
+      this.isRendered = true
+      this.trigger('initialRender')
+      Calendar.trigger('initialRender', this)
+    } else if (this.elementVisible()) {
+      // mainly for the public API
+      this.calcSize()
+      this.updateViewSize()
     }
   }
 
 
-  hasPublicHandlers(name: string): boolean {
-    return this.hasHandlers(name) ||
-      this.opt(name) // handler specified in options
+  destroy() {
+    if (this.isRendered) {
+      this._destroy()
+      this.el.classList.remove('fc')
+      this.isRendered = false
+      this.unbindGlobalHandlers()
+      this.trigger('destroy')
+      Calendar.trigger('destroy', this)
+    }
+  }
+
+
+  // General Rendering
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  _render(forces?) {
+    if (!forces) {
+      forces = {}
+    }
+
+    this.applyElClassNames()
+
+    if (!this.isSkeletonRendered) {
+      this.renderSkeleton()
+      this.isSkeletonRendered = true
+    }
+
+    this.freezeContentHeight() // do after contentEl is created in renderSkeleton
+
+    this.renderToolbars(forces)
+
+    let view = this.view
+
+    if (!view.el) {
+      view.setElement(
+        createElement('div', { className: 'fc-view fc-' + view.type + '-view' })
+      )
+    }
+
+    if (!view.el.parentNode) {
+      this.contentEl.appendChild(view.el)
+    } else {
+      this.view.addScroll(view.queryScroll())
+    }
+
+    this.view.render(this.state, forces)
+
+    if (this.updateViewSize()) { // success? // TODO: respect isSizeDirty
+      view.popScroll()
+    }
+
+    this.thawContentHeight()
+  }
+
+
+  _destroy() {
+    if (this.view) {
+      this.view.removeElement()
+      this.view = null
+    }
+
+    if (this.header) {
+      this.header.removeElement()
+      this.header = null
+    }
+
+    if (this.footer) {
+      this.footer.removeElement()
+      this.footer = null
+    }
+
+    this.unrenderSkeleton()
+    this.isSkeletonRendered = false
+
+    this.removeElClassNames()
   }
 
 
-  // Dispatcher
+  elementVisible(): boolean {
+    return Boolean(this.el.offsetWidth)
+  }
+
+
+  // Classnames on root elements
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  applyElClassNames() {
+    let classList = this.el.classList
+    let elDirClassName = this.opt('isRTL') ? 'fc-rtl' : 'fc-ltr'
+    let elThemeClassName = this.theme.getClass('widget')
+
+    if (elDirClassName !== this.elDirClassName) {
+      if (this.elDirClassName) {
+        classList.remove(this.elDirClassName)
+      }
+      classList.add(elDirClassName)
+      this.elDirClassName = elDirClassName
+    }
+
+    if (elThemeClassName !== this.elThemeClassName) {
+      if (this.elThemeClassName) {
+        classList.remove(this.elThemeClassName)
+      }
+      classList.add(elThemeClassName)
+      this.elThemeClassName = elThemeClassName
+    }
+  }
+
+
+  removeElClassNames() {
+    let classList = this.el.classList
+
+    if (this.elDirClassName) {
+      classList.remove(this.elDirClassName)
+      this.elDirClassName = null
+    }
+
+    if (this.elThemeClassName) {
+      classList.remove(this.elThemeClassName)
+      this.elThemeClassName = null
+    }
+  }
+
+
+  // Skeleton Rendering
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  renderSkeleton() {
+
+    prependToElement(
+      this.el,
+      this.contentEl = createElement('div', { className: 'fc-view-container' })
+    )
+
+    // event delegation for nav links
+    this.removeNavLinkListener = listenBySelector(this.el, 'click', 'a[data-goto]', (ev, anchorEl) => {
+      let gotoOptions: any = anchorEl.getAttribute('data-goto')
+      gotoOptions = gotoOptions ? JSON.parse(gotoOptions) : {}
+
+      let date = this.dateEnv.createMarker(gotoOptions.date)
+      let viewType = gotoOptions.type
+
+      // property like "navLinkDayClick". might be a string or a function
+      let customAction = this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click')
+
+      if (typeof customAction === 'function') {
+        customAction(date, ev)
+      } else {
+        if (typeof customAction === 'string') {
+          viewType = customAction
+        }
+        this.zoomTo(date, viewType)
+      }
+    })
+  }
+
+  unrenderSkeleton() {
+    removeElement(this.contentEl)
+    this.contentEl = null
+
+    this.removeNavLinkListener()
+  }
+
+
+  // Global Handlers
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  bindGlobalHandlers() {
+    GlobalEmitter.needed()
+
+    if (this.opt('handleWindowResize')) {
+      window.addEventListener('resize',
+        this.windowResizeProxy = debounce( // prevents rapid calls
+          this.windowResize.bind(this),
+          this.opt('windowResizeDelay')
+        )
+      )
+    }
+  }
+
+  unbindGlobalHandlers() {
+    GlobalEmitter.unneeded()
+
+    if (this.windowResizeProxy) {
+      window.removeEventListener('resize', this.windowResizeProxy)
+      this.windowResizeProxy = null
+    }
+  }
+
+
+  // Dispatcher / Render Queue
   // -----------------------------------------------------------------------------------------------------------------
 
 
@@ -188,7 +387,7 @@ export default class Calendar {
       }
 
       if (!this.renderingPauseDepth) {
-        this.renderView()
+        this._render()
       }
     }
   }
@@ -201,7 +400,7 @@ export default class Calendar {
 
   resumeRendering() {
     if (!(--this.renderingPauseDepth)) {
-      this.renderView()
+      this._render()
     }
   }
 
@@ -211,31 +410,93 @@ export default class Calendar {
   }
 
 
-  // Options Public API
+  // Options
   // -----------------------------------------------------------------------------------------------------------------
 
 
   // public getter/setter
   option(name: string | object, value?) {
-    let newOptionHash
-
     if (typeof name === 'string') {
       if (value === undefined) { // getter
-        return this.optionsManager.get(name)
+        return this.opt(name)
       } else { // setter for individual option
-        newOptionHash = {}
-        newOptionHash[name] = value
-        this.optionsManager.add(newOptionHash)
+        this.setOptions({
+          [name]: value
+        })
       }
     } else if (typeof name === 'object' && name) { // compound setter with object input (non-null)
-      this.optionsManager.add(name)
+      this.setOptions(name)
+    }
+  }
+
+
+  setOptions(optionsHash) {
+    this.optionsManager.add(optionsHash)
+    this.handleOptions(this.optionsManager.computed)
+
+    let optionCnt = 0
+    let optionName
+
+    for (optionName in optionsHash) {
+      optionCnt++
+    }
+
+    if (optionCnt === 1) {
+      if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
+        this.updateViewSize(true) // isResize=true
+        return
+      } else if (optionName === 'defaultDate') {
+        return // can't change date this way. use gotoDate instead
+      } else if (/^(event|select)(Overlap|Constraint|Allow)$/.test(optionName)) {
+        return // doesn't affect rendering. only interactions.
+      }
+    }
+
+    this._render(true) // force rerender
+  }
+
+
+  handleOptions(options) {
+    this.defaultAllDayEventDuration = createDuration(options.defaultAllDayEventDuration)
+    this.defaultTimedEventDuration = createDuration(options.defaultTimedEventDuration)
+
+    this.dateEnv = this.buildDateEnv(
+      options.locale,
+      options.timezone,
+      options.firstDay,
+      options.weekNumberCalculation,
+      options.weekLabel
+    )
+
+    this.theme = this.buildTheme(options)
+
+    this.viewSpecManager.clearCache()
+  }
+
+
+  opt(optName) {
+    return this.optionsManager.computed[optName]
+  }
+
+
+  // Trigger
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  publiclyTrigger(name: string, args) {
+    let optHandler = this.opt(name)
+
+    this.triggerWith(name, this, args) // Emitter's method
+
+    if (optHandler) {
+      return optHandler.apply(this, args)
     }
   }
 
 
-  // private getter
-  opt(name: string) {
-    return this.optionsManager.get(name)
+  hasPublicHandlers(name: string): boolean {
+    return this.hasHandlers(name) ||
+      this.opt(name) // handler specified in options
   }
 
 
@@ -243,16 +504,19 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  installNewView(viewType) {
+  installNewView(viewType: string) {
 
-    if (this.view) {
+    if (this.view && this.view.type !== viewType) {
+      this.freezeContentHeight() // hack
       this.view.removeElement()
-      this.toolbarsManager.proxyCall('deactivateButton', this.view.type)
+      this.view = null
     }
 
-    this.view =
-      this.viewsByType[viewType] ||
-      (this.viewsByType[viewType] = this.instantiateView(viewType))
+    if (!this.view) {
+      this.view =
+        this.viewsByType[viewType] ||
+        (this.viewsByType[viewType] = this.instantiateView(viewType))
+    }
   }
 
 
@@ -279,7 +543,7 @@ export default class Calendar {
 
     if (dateOrRange) {
       if ((dateOrRange as RangeInput).start && (dateOrRange as RangeInput).end) { // a range
-        this.optionsManager.recordOverrides({ // will not rerender
+        this.optionsManager.add({ // will not rerender
           visibleRange: dateOrRange
         })
       } else { // a date
@@ -299,7 +563,7 @@ export default class Calendar {
 
     viewType = viewType || 'day' // day is default zoom
     spec = this.viewSpecManager.getViewSpec(viewType) ||
-      this.viewSpecManager.getUnitViewSpec(viewType)
+      this.viewSpecManager.getUnitViewSpec(viewType, this)
 
     if (spec) {
       this.dispatch({ type: 'SET_VIEW_TYPE', viewType: spec.type, dateMarker })
@@ -399,167 +663,8 @@ export default class Calendar {
   }
 
 
-  // Rendering
-  // -----------------------------------------------------------------------------------
-
-
-  watchTheme() {
-    this.optionsManager.watch('settingTheme', [ '?theme', '?themeSystem' ], (opts) => {
-      let themeClass = getThemeSystemClass(opts.themeSystem || opts.theme)
-      let theme = new themeClass(this.optionsManager)
-      this.theme = theme
-    })
-  }
-
-
-  render() {
-    if (!this.isSkeletonRendered) {
-      this.renderSkeleton()
-      this.attachHeader()
-      this.attachFooter()
-      this.isSkeletonRendered = true
-      this.renderView()
-      this.trigger('initialRender')
-      Calendar.trigger('initialRender', this)
-    } else if (this.elementVisible()) {
-      // mainly for the public API
-      this.calcSize()
-      this.updateViewSize()
-    }
-  }
-
-
-  renderView(forces?) {
-    let { view } = this
-
-    if (view && this.isSkeletonRendered) {
-
-      if (!view.el) {
-        view.setElement(
-          createElement('div', { className: 'fc-view fc-' + view.type + '-view' })
-        )
-      }
-
-      if (!view.el.parentNode) {
-        this.contentEl.appendChild(view.el)
-      } else {
-        view.addScroll(view.queryScroll())
-      }
-
-      this.freezeContentHeight()
-      view.render(this.state, forces)
-      this.thawContentHeight()
-
-      if (this.updateViewSize()) { // success? // TODO: respect isSizeDirty
-        view.popScroll()
-      }
-    }
-  }
-
-
-  destroy() {
-    if (this.isSkeletonRendered) {
-      this.unrenderSkeleton()
-      this.isSkeletonRendered = false
-      this.view.removeElement()
-      this.view = null
-      this.trigger('destroy')
-      Calendar.trigger('destroy', this)
-    }
-  }
-
-
-  renderSkeleton() {
-    let el = this.el
-
-    el.classList.add('fc')
-
-    // event delegation for nav links
-    this.removeNavLinkListener = listenBySelector(el, 'click', 'a[data-goto]', (ev, anchorEl) => {
-      let gotoOptions: any = anchorEl.getAttribute('data-goto')
-      gotoOptions = gotoOptions ? JSON.parse(gotoOptions) : {}
-
-      let date = this.dateEnv.createMarker(gotoOptions.date)
-      let viewType = gotoOptions.type
-
-      // property like "navLinkDayClick". might be a string or a function
-      let customAction = this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click')
-
-      if (typeof customAction === 'function') {
-        customAction(date, ev)
-      } else {
-        if (typeof customAction === 'string') {
-          viewType = customAction
-        }
-        this.zoomTo(date, viewType)
-      }
-    })
-
-    // called immediately, and upon option change
-    this.optionsManager.watch('applyingThemeClasses', [ '?theme', '?themeSystem' ], (opts) => {
-      let widgetClass = this.theme.getClass('widget')
-      if (widgetClass) {
-        el.classList.add(widgetClass)
-      }
-    }, () => {
-      let widgetClass = this.theme.getClass('widget')
-      if (widgetClass) {
-        el.classList.remove(widgetClass)
-      }
-    })
-
-    // called immediately, and upon option change.
-    // HACK: locale often affects isRTL, so we explicitly listen to that too.
-    this.optionsManager.watch('applyingDirClasses', [ '?isRTL', '?locale' ], (opts) => {
-      forceClassName(el, 'fc-ltr', !opts.isRTL)
-      forceClassName(el, 'fc-rtl', opts.isRTL)
-    })
-
-    prependToElement(el, this.contentEl = createElement('div', { className: 'fc-view-container' }))
-
-    if (this.opt('handleWindowResize')) {
-      window.addEventListener('resize',
-        this.windowResizeProxy = debounce( // prevents rapid calls
-          this.windowResize.bind(this),
-          this.opt('windowResizeDelay')
-        )
-      )
-    }
-  }
-
-
-  unrenderSkeleton() {
-
-    this.toolbarsManager.proxyCall('removeElement')
-    removeElement(this.contentEl)
-    this.el.classList.remove('fc')
-    this.el.classList.remove('fc-ltr')
-    this.el.classList.remove('fc-rtl')
-
-    // removes theme-related root className
-    this.optionsManager.unwatch('applyingThemeClasses')
-
-    if (this.removeNavLinkListener) {
-      this.removeNavLinkListener()
-      this.removeNavLinkListener = null
-    }
-
-    if (this.windowResizeProxy) {
-      window.removeEventListener('resize', this.windowResizeProxy)
-      this.windowResizeProxy = null
-    }
-
-    GlobalEmitter.unneeded()
-  }
-
-
-  elementVisible(): boolean {
-    return Boolean(this.el.offsetWidth)
-  }
-
-
   // Resizing
-  // -----------------------------------------------------------------------------------
+  // -----------------------------------------------------------------------------------------------------------------
 
 
   getSuggestedViewHeight(): number {
@@ -650,8 +755,8 @@ export default class Calendar {
   }
 
 
-  /* Height "Freezing"
-  -----------------------------------------------------------------------------*/
+  // Height "Freezing"
+  // -----------------------------------------------------------------------------------------------------------------
 
 
   freezeContentHeight() {
@@ -676,100 +781,69 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  initToolbars() {
-    this.header = new Toolbar(this, this.computeHeaderOptions())
-    this.header.render()
-
-    this.footer = new Toolbar(this, this.computeFooterOptions())
-    this.footer.render()
-
-    this.toolbarsManager = new Iterator([ this.header, this.footer ])
-  }
-
-
-  computeHeaderOptions() {
-    return {
-      extraClasses: 'fc-header-toolbar',
-      layout: this.opt('header')
+  renderToolbars(forces) {
+    let headerLayout = this.opt('header')
+    let footerLayout = this.opt('footer')
+    let now = this.getNow()
+    let dateProfile = this.state.dateProfile
+    let view = this.view
+    let todayInfo = view.dateProfileGenerator.build(now)
+    let prevInfo = view.dateProfileGenerator.buildPrev(dateProfile)
+    let nextInfo = view.dateProfileGenerator.buildNext(dateProfile)
+    let props = {
+      title: this.view.title,
+      activeButton: this.view.type,
+      isTodayEnabled: todayInfo.isValid && !dateProfile.currentUnzonedRange.containsDate(now),
+      isPrevEnabled: prevInfo.isValid,
+      isNextEnabled: nextInfo.isValid
     }
-  }
-
 
-  computeFooterOptions() {
-    return {
-      extraClasses: 'fc-footer-toolbar',
-      layout: this.opt('footer')
+    if ((!headerLayout || forces === true) && this.header) {
+      this.header.removeElement()
+      this.header = null
     }
-  }
-
 
-  // can be called repeatedly and Header will rerender
-  attachHeader() {
-    if (this.header.el) {
-      prependToElement(this.el, this.header.el)
+    if (headerLayout) {
+      if (!this.header) {
+        this.header = new Toolbar(this, 'fc-header-toolbar')
+        prependToElement(this.el, this.header.el)
+      }
+      this.header.render(
+        assignTo({ layout: headerLayout }, props),
+        forces
+      )
     }
-  }
 
-
-  // can be called repeatedly and Footer will rerender
-  attachFooter() {
-    if (this.footer.el) {
-      this.el.appendChild(this.footer.el)
+    if ((!footerLayout || forces === true) && this.footer) {
+      this.footer.removeElement()
+      this.footer = null
     }
-  }
-
 
-  onDateProfileChange(dateProfile) {
-    this.view.updateMiscDateProps(dateProfile)
-    this.setToolbarsTitle(this.view.title) // view.title set via updateMiscDateProps
-    this.updateToolbarButtons(dateProfile)
-    this.toolbarsManager.proxyCall('activateButton', this.view.type)
-  }
-
-
-  setToolbarsTitle(title: string) {
-    this.toolbarsManager.proxyCall('updateTitle', title)
+    if (footerLayout) {
+      if (!this.footer) {
+        this.footer = new Toolbar(this, 'fc-footer-toolbar')
+        prependToElement(this.el, this.footer.el)
+      }
+      this.footer.render(
+        assignTo({ layout: footerLayout }, props),
+        forces
+      )
+    }
   }
 
 
-  updateToolbarButtons(dateProfile) {
-    let now = this.getNow()
-    let view = this.view
-    let todayInfo = view.dateProfileGenerator.build(now)
-    let prevInfo = view.dateProfileGenerator.buildPrev(dateProfile)
-    let nextInfo = view.dateProfileGenerator.buildNext(dateProfile)
-
-    this.toolbarsManager.proxyCall(
-      (todayInfo.isValid && !dateProfile.currentUnzonedRange.containsDate(now)) ?
-        'enableButton' :
-        'disableButton',
-      'today'
-    )
-
-    this.toolbarsManager.proxyCall(
-      prevInfo.isValid ?
-        'enableButton' :
-        'disableButton',
-      'prev'
-    )
-
-    this.toolbarsManager.proxyCall(
-      nextInfo.isValid ?
-        'enableButton' :
-        'disableButton',
-      'next'
-    )
-  }
+  queryToolbarsHeight() {
+    let height = 0
 
+    if (this.header) {
+      height += computeHeightAndMargins(this.header.el)
+    }
 
-  queryToolbarsHeight() {
-    return this.toolbarsManager.items.reduce(function(accumulator, toolbar) {
-      let toolbarHeight = toolbar.el ?
-        computeHeightAndMargins(toolbar.el) :
-        0
+    if (this.footer) {
+      height += computeHeightAndMargins(this.footer.el)
+    }
 
-      return accumulator + toolbarHeight
-    }, 0)
+    return height
   }
 
 
@@ -837,26 +911,6 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  initDateEnv() {
-    // not really date-env
-    this.defaultAllDayEventDuration = createDuration(this.opt('defaultAllDayEventDuration'))
-    this.defaultTimedEventDuration = createDuration(this.opt('defaultTimedEventDuration'))
-
-    this.optionsManager.watch('buildDateEnv', [
-      '?locale', '?timezone', '?firstDay', '?weekNumberCalculation', '?weekLabel'
-    ], (opts) => {
-      this.dateEnv = new DateEnv({
-        calendarSystem: 'gregory',
-        timeZone: opts.timezone,
-        locale: getLocale(opts.locale),
-        weekNumberCalculation: opts.weekNumberCalculation,
-        firstDay: opts.firstDay,
-        weekLabel: opts.weekLabel
-      })
-    })
-  }
-
-
   // Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option
   getNow(): DateMarker {
     let now = this.opt('now')
@@ -923,7 +977,7 @@ export default class Calendar {
 
 
   rerenderEvents() { // API method. destroys old events if previously rendered.
-    this.view.flash('displayingEvents')
+    this._render({ events: true }) // TODO: test this
   }
 
 
@@ -1005,3 +1059,22 @@ export default class Calendar {
 EmitterMixin.mixIntoObj(Calendar) // for global registry
 EmitterMixin.mixInto(Calendar)
 ListenerMixin.mixInto(Calendar)
+
+
+
+function buildDateEnv(locale, timezone, firstDay, weekNumberCalculation, weekLabel) {
+  return new DateEnv({
+    calendarSystem: 'gregory',
+    timeZone: timezone,
+    locale: getLocale(locale),
+    weekNumberCalculation: weekNumberCalculation,
+    firstDay: firstDay,
+    weekLabel: weekLabel
+  })
+}
+
+
+function buildTheme(calendarOptions) {
+  let themeClass = getThemeSystemClass(calendarOptions.themeSystem || calendarOptions.theme)
+  return new themeClass(calendarOptions)
+}

+ 6 - 58
src/OptionsManager.ts

@@ -1,22 +1,19 @@
 import { assignTo } from './util/object'
 import { firstDefined } from './util/misc'
 import { globalDefaults, rtlDefaults, mergeOptions } from './options'
-import Model from './common/Model'
 import { getLocale } from './datelib/locale'
 
 
-export default class OptionsManager extends Model {
+export default class OptionsManager {
 
-  _calendar: any // avoid
   dirDefaults: any // option defaults related to LTR or RTL
   localeDefaults: any // option defaults related to current locale
   overrides: any // option overrides given to the fullCalendar constructor
   dynamicOverrides: any // options set with dynamic setter method. higher precedence than view overrides.
+  computed: any
 
 
-  constructor(_calendar, overrides) {
-    super()
-    this._calendar = _calendar
+  constructor(overrides) {
     this.overrides = assignTo({}, overrides) // make a copy
     this.dynamicOverrides = {}
     this.compute()
@@ -24,40 +21,8 @@ export default class OptionsManager extends Model {
 
 
   add(newOptionHash) {
-    let optionCnt = 0
-    let optionName
-
-    this.recordOverrides(newOptionHash) // will trigger this model's watchers
-
-    for (optionName in newOptionHash) {
-      optionCnt++
-    }
-
-    // special-case handling of single option change.
-    // if only one option change, `optionName` will be its name.
-    if (optionCnt === 1) {
-      if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
-        this._calendar.updateViewSize(true) // isResize=true
-        return
-      } else if (optionName === 'defaultDate') {
-        return // can't change date this way. use gotoDate instead
-      } else if (optionName === 'businessHours') {
-        this._calendar.view.flash('displayingBusinessHours')
-        return
-      } else if (/^(event|select)(Overlap|Constraint|Allow)$/.test(optionName)) {
-        return // doesn't affect rendering. only interactions.
-      }
-    }
-
-    // catch-all. rerender the header and footer and rebuild/rerender the current view
-    this._calendar.renderHeader()
-    this._calendar.renderFooter()
-
-    // even non-current views will be affected by this option change. do before rerender
-    // TODO: detangle
-    this._calendar.viewsByType = {}
-
-    this._calendar.reinitView()
+    assignTo(this.dynamicOverrides, newOptionHash)
+    this.compute()
   }
 
 
@@ -68,7 +33,6 @@ export default class OptionsManager extends Model {
     let localeDefaults
     let isRTL
     let dirDefaults
-    let rawOptions
 
     locale = firstDefined( // explicit locale option given?
       this.dynamicOverrides.locale,
@@ -87,29 +51,13 @@ export default class OptionsManager extends Model {
     this.dirDefaults = dirDefaults
     this.localeDefaults = localeDefaults
 
-    rawOptions = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
+    this.computed = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
       globalDefaults, // global defaults
       dirDefaults,
       localeDefaults,
       this.overrides,
       this.dynamicOverrides
     ])
-
-    this.reset(rawOptions)
   }
 
-
-  // stores the new options internally, but does not rerender anything.
-  recordOverrides(newOptionHash) {
-    let optionName
-
-    for (optionName in newOptionHash) {
-      this.dynamicOverrides[optionName] = newOptionHash[optionName]
-    }
-
-    this._calendar.viewSpecManager.clearCache() // the dynamic override invalidates the options in this cache, so just clear it
-    this.compute() // this.options needs to be recomputed after the dynamic override
-  }
-
-
 }

+ 116 - 54
src/Toolbar.ts

@@ -8,56 +8,128 @@ import { htmlToElement, appendToElement, removeElement, findElements, createElem
 export default class Toolbar {
 
   calendar: any
-  toolbarOptions: any
-  el: HTMLElement = null // mirrors local `el`
-  viewsWithButtons: any = []
+  el: HTMLElement = null
+  viewsWithButtons: any
 
+  isLayoutRendered: boolean
+  layout: any
+  title: string
+  activeButton: string
+  isTodayEnabled: boolean
+  isPrevEnabled: boolean
+  isNextEnabled: boolean
 
-  constructor(calendar, toolbarOptions) {
+
+  constructor(calendar, extraClassName) {
     this.calendar = calendar
-    this.toolbarOptions = toolbarOptions
+    this.el = createElement('div', { className: 'fc-toolbar ' + extraClassName })
   }
 
 
-  // can be called repeatedly and will rerender
-  render() {
-    let sections = this.toolbarOptions.layout
-    let el = this.el
+  /*
+    renderProps: {
+      layout
+      title
+      activeButton
+      isTodayEnabled
+      isPrevEnabled
+      isNextEnabled
+    }
+  */
+  render(renderProps, forces) {
+
+    if (renderProps.layout !== this.layout || forces === true) {
+      if (this.isLayoutRendered) {
+        this.unrenderLayout()
+      }
+      this.renderLayout(renderProps.layout)
+      this.layout = renderProps.layout
+      this.isLayoutRendered = true
+    }
+
+    if (renderProps.title !== this.title || forces === true) {
+      this.updateTitle(renderProps.title)
+      this.title = renderProps.title
+    }
+
+    if (renderProps.activeButton !== this.activeButton || forces === true) {
+      if (this.activeButton) {
+        this.deactivateButton(this.activeButton)
+      }
+      this.activateButton(renderProps.activeButton)
+      this.activeButton = renderProps.activeButton
+    }
 
-    if (sections) {
-      if (!el) {
-        el = this.el = createElement('div', { className: 'fc-toolbar ' + this.toolbarOptions.extraClasses })
+    if (renderProps.isTodayEnabled !== this.isTodayEnabled || forces === true) {
+      if (renderProps.isTodayEnabled) {
+        this.enableButton('today')
       } else {
-        el.innerHTML = ''
+        this.disableButton('today')
       }
-      appendToElement(el, this.renderSection('left'))
-      appendToElement(el, this.renderSection('right'))
-      appendToElement(el, this.renderSection('center'))
-      appendToElement(el, '<div class="fc-clear"></div>')
-    } else {
-      this.removeElement()
+      this.isTodayEnabled = renderProps.isTodayEnabled
+    }
+
+    if (renderProps.isPrevEnabled !== this.isPrevEnabled || forces === true) {
+      if (renderProps.isPrevEnabled) {
+        this.enableButton('today')
+      } else {
+        this.disableButton('today')
+      }
+      this.isPrevEnabled = renderProps.isPrevEnabled
+    }
+
+    if (renderProps.isNextEnabled !== this.isNextEnabled || forces === true) {
+      if (renderProps.isNextEnabled) {
+        this.enableButton('today')
+      } else {
+        this.disableButton('today')
+      }
+      this.isNextEnabled = renderProps.isNextEnabled
     }
   }
 
 
+  renderLayout(layout) {
+    let el = this.el
+
+    this.viewsWithButtons = []
+
+    appendToElement(el, this.renderSection('left', layout.left))
+    appendToElement(el, this.renderSection('right', layout.right))
+    appendToElement(el, this.renderSection('center', layout.center))
+    appendToElement(el, '<div class="fc-clear"></div>')
+  }
+
+
+  unrenderLayout() {
+    this.el.innerHTML = ''
+  }
+
+
   removeElement() {
-    if (this.el) {
-      removeElement(this.el)
-      this.el = null
-    }
+    this.unrenderLayout()
+    this.isLayoutRendered = false
+
+    removeElement(this.el)
+
+    this.layout = null
+    this.title = null
+    this.activeButton = null
+    this.isTodayEnabled = null
+    this.isPrevEnabled = null
+    this.isNextEnabled = null
   }
 
 
-  renderSection(position) {
+  renderSection(position, buttonStr) {
     let calendar = this.calendar
     let theme = calendar.theme
     let optionsManager = calendar.optionsManager
     let viewSpecManager = calendar.viewSpecManager
     let sectionEl = createElement('div', { className: 'fc-' + position })
-    let buttonStr = this.toolbarOptions.layout[position]
-    let calendarCustomButtons = optionsManager.get('customButtons') || {}
+    let calendarCustomButtons = optionsManager.computed.customButtons || {}
     let calendarButtonTextOverrides = optionsManager.overrides.buttonText || {}
-    let calendarButtonText = optionsManager.get('buttonText') || {}
+    let calendarButtonText = optionsManager.computed.buttonText || {}
 
     if (buttonStr) {
       buttonStr.split(' ').forEach((buttonGroupStr, i) => {
@@ -235,49 +307,39 @@ export default class Toolbar {
 
 
   updateTitle(text) {
-    if (this.el) {
-      findElements(this.el, 'h2').forEach(function(titleEl) {
-        titleEl.innerText = text
-      })
-    }
+    findElements(this.el, 'h2').forEach(function(titleEl) {
+      titleEl.innerText = text
+    })
   }
 
 
   activateButton(buttonName) {
-    if (this.el) {
-      findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl) => {
-        buttonEl.classList.add(this.calendar.theme.getClass('stateActive'))
-      })
-    }
+    findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl) => {
+      buttonEl.classList.add(this.calendar.theme.getClass('stateActive'))
+    })
   }
 
 
   deactivateButton(buttonName) {
-    if (this.el) {
-      findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl) => {
-        buttonEl.classList.remove(this.calendar.theme.getClass('stateActive'))
-      })
-    }
+    findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl) => {
+      buttonEl.classList.remove(this.calendar.theme.getClass('stateActive'))
+    })
   }
 
 
   disableButton(buttonName) {
-    if (this.el) {
-      findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl: HTMLButtonElement) => {
-        buttonEl.disabled = true
-        buttonEl.classList.add(this.calendar.theme.getClass('stateDisabled'))
-      })
-    }
+    findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl: HTMLButtonElement) => {
+      buttonEl.disabled = true
+      buttonEl.classList.add(this.calendar.theme.getClass('stateDisabled'))
+    })
   }
 
 
   enableButton(buttonName) {
-    if (this.el) {
-      findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl: HTMLButtonElement) => {
-        buttonEl.disabled = false
-        buttonEl.classList.remove(this.calendar.theme.getClass('stateDisabled'))
-      })
-    }
+    findElements(this.el, '.fc-' + buttonName + '-button').forEach((buttonEl: HTMLButtonElement) => {
+      buttonEl.disabled = false
+      buttonEl.classList.remove(this.calendar.theme.getClass('stateDisabled'))
+    })
   }
 
 

+ 10 - 24
src/View.ts

@@ -5,13 +5,13 @@ import { parseFieldSpecs } from './util/misc'
 import Calendar from './Calendar'
 import { default as DateProfileGenerator, DateProfile } from './DateProfileGenerator'
 import InteractiveDateComponent from './component/InteractiveDateComponent'
-import GlobalEmitter from './common/GlobalEmitter'
 import UnzonedRange from './models/UnzonedRange'
 import { DateMarker, addDays, addMs, diffWholeDays } from './datelib/marker'
 import { createDuration } from './datelib/duration'
 import { createFormatter } from './datelib/formatting'
 import { EventInstance } from './reducers/event-store'
 import { Selection } from './reducers/selection'
+import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 
 
 /* An abstract class from which other views inherit from
@@ -19,6 +19,13 @@ import { Selection } from './reducers/selection'
 
 export default abstract class View extends InteractiveDateComponent {
 
+  on: EmitterInterface['on']
+  one: EmitterInterface['one']
+  off: EmitterInterface['off']
+  trigger: EmitterInterface['trigger']
+  triggerWith: EmitterInterface['triggerWith']
+  hasHandlers: EmitterInterface['hasHandlers']
+
   type: string // subclass' view name (string)
   name: string // deprecated. use `type` instead
   title: string // the text that will be displayed in the header's title
@@ -244,29 +251,6 @@ export default abstract class View extends InteractiveDateComponent {
   }
 
 
-  // Misc view rendering utils
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  // Binds DOM handlers to elements that reside outside the view container, such as the document
-  bindGlobalHandlers() {
-    super.bindGlobalHandlers()
-
-    this.listenTo(GlobalEmitter.get(), {
-      touchstart: this.processUnselect,
-      mousedown: this.handleDocumentMousedown
-    })
-  }
-
-
-  // Unbinds DOM handlers from elements that reside outside the view container
-  unbindGlobalHandlers() {
-    super.unbindGlobalHandlers()
-
-    this.stopListeningTo(GlobalEmitter.get())
-  }
-
-
   /* Now Indicator
   ------------------------------------------------------------------------------------------------------------------*/
 
@@ -697,5 +681,7 @@ export default abstract class View extends InteractiveDateComponent {
 
 }
 
+EmitterMixin.mixInto(View)
+
 View.prototype.usesMinMaxTime = false
 View.prototype.dateProfileGeneratorClass = DateProfileGenerator

+ 3 - 6
src/ViewSpecManager.ts

@@ -6,15 +6,12 @@ import { Duration, createDuration, getWeeksFromInput, greatestDurationDenominato
 
 export default class ViewSpecManager {
 
-  _calendar: any // avoid
   optionsManager: any
   viewSpecCache: any // cache of view definitions (initialized in Calendar.js)
 
 
-  constructor(optionsManager, _calendar) {
+  constructor(optionsManager) {
     this.optionsManager = optionsManager
-    this._calendar = _calendar
-
     this.clearCache()
   }
 
@@ -34,13 +31,13 @@ export default class ViewSpecManager {
 
   // Given a duration singular unit, like "week" or "day", finds a matching view spec.
   // Preference is given to views that have corresponding buttons.
-  getUnitViewSpec(unit) {
+  getUnitViewSpec(unit, calendar) {
     let viewTypes
     let i
     let spec
 
     // put views that have buttons first. there will be duplicates, but oh well
-    viewTypes = this._calendar.header.getViewsWithButtons() // TODO: include footer as well?
+    viewTypes = calendar.header.getViewsWithButtons() // TODO: include footer as well?
     for (let viewType in viewHash) {
       viewTypes.push(viewType)
     }

+ 0 - 21
src/common/Iterator.ts

@@ -1,21 +0,0 @@
-
-export default class Iterator {
-
-  items: any
-
-  constructor(items) {
-    this.items = items || []
-  }
-
-  /* Calls a method on every item passing the arguments through */
-  proxyCall(methodName, ...args) {
-    let results = []
-
-    this.items.forEach(function(item) {
-      results.push(item[methodName].apply(item, args))
-    })
-
-    return results
-  }
-
-}

+ 0 - 324
src/common/Model.ts

@@ -1,324 +0,0 @@
-import Class from './Class'
-import { default as EmitterMixin, EmitterInterface } from './EmitterMixin'
-import { default as ListenerMixin, ListenerInterface } from './ListenerMixin'
-
-
-export default class Model extends Class {
-
-  on: EmitterInterface['on']
-  one: EmitterInterface['one']
-  off: EmitterInterface['off']
-  trigger: EmitterInterface['trigger']
-  triggerWith: EmitterInterface['triggerWith']
-  hasHandlers: EmitterInterface['hasHandlers']
-  listenTo: ListenerInterface['listenTo']
-  stopListeningTo: ListenerInterface['stopListeningTo']
-
-  _props: any
-  _watchers: any
-  _globalWatchArgs: any // initialized after class
-
-  constructor() {
-    super()
-    this._watchers = {}
-    this._props = {}
-    this.applyGlobalWatchers()
-    this.constructed()
-  }
-
-  static watch(name, ...args) {
-    // subclasses should make a masked-copy of the superclass's map
-    // TODO: write test
-    if (!this.prototype.hasOwnProperty('_globalWatchArgs')) {
-      this.prototype._globalWatchArgs = Object.create(this.prototype._globalWatchArgs)
-    }
-
-    this.prototype._globalWatchArgs[name] = args
-  }
-
-  constructed() {
-    // useful for monkeypatching. TODO: BaseClass?
-  }
-
-  applyGlobalWatchers() {
-    let map = this._globalWatchArgs
-    let name
-
-    for (name in map) {
-      this.watch.apply(this, [ name ].concat(map[name]))
-    }
-  }
-
-  has(name) {
-    return name in this._props
-  }
-
-  get(name) {
-    if (name === undefined) {
-      return this._props
-    }
-
-    return this._props[name]
-  }
-
-  set(name, val) {
-    let newProps
-
-    if (typeof name === 'string') {
-      newProps = {}
-      newProps[name] = val === undefined ? null : val
-    } else {
-      newProps = name
-    }
-
-    this.setProps(newProps)
-  }
-
-  reset(newProps) {
-    let oldProps = this._props
-    let changeset = {} // will have undefined's to signal unsets
-    let name
-
-    for (name in oldProps) {
-      changeset[name] = undefined
-    }
-
-    for (name in newProps) {
-      changeset[name] = newProps[name]
-    }
-
-    this.setProps(changeset)
-  }
-
-  unset(name) { // accepts a string or array of strings
-    let newProps = {}
-    let names
-    let i
-
-    if (typeof name === 'string') {
-      names = [ name ]
-    } else {
-      names = name
-    }
-
-    for (i = 0; i < names.length; i++) {
-      newProps[names[i]] = undefined
-    }
-
-    this.setProps(newProps)
-  }
-
-  setProps(newProps) {
-    let changedProps = {}
-    let changedCnt = 0
-    let name
-    let val
-
-    for (name in newProps) {
-      val = newProps[name]
-
-      // a change in value?
-      // if an object, don't check equality, because might have been mutated internally.
-      // TODO: eventually enforce immutability.
-      if (
-        (typeof val === 'object' && val) || // non-null object
-        val !== this._props[name]
-      ) {
-        changedProps[name] = val
-        changedCnt++
-      }
-    }
-
-    if (changedCnt) {
-
-      this.trigger('before:batchChange', changedProps)
-
-      for (name in changedProps) {
-        val = changedProps[name]
-
-        this.trigger('before:change', name, val)
-        this.trigger('before:change:' + name, val)
-      }
-
-      for (name in changedProps) {
-        val = changedProps[name]
-
-        if (val === undefined) {
-          delete this._props[name]
-        } else {
-          this._props[name] = val
-        }
-
-        this.trigger('change:' + name, val)
-        this.trigger('change', name, val)
-      }
-
-      this.trigger('batchChange', changedProps)
-    }
-  }
-
-  watch(name, depList, startFunc, stopFunc?, isAsync = false) {
-    this.unwatch(name)
-    this._watchers[name] = this._watchDeps(depList, (deps) => {
-      if (isAsync) { // means startFunc accepts a callback function that it will call
-        this.unset(name) // put in an unset state while resolving
-        startFunc.call(this, deps, (val) => {
-          this.set(name, val)
-        })
-      } else {
-        let res = startFunc.call(this, deps)
-        this.set(name, res)
-      }
-    }, (deps) => {
-      this.unset(name)
-      if (stopFunc) {
-        stopFunc.call(this, deps)
-      }
-    })
-  }
-
-  unwatch(name) {
-    let watcher = this._watchers[name]
-
-    if (watcher) {
-      delete this._watchers[name]
-      watcher.teardown()
-    }
-  }
-
-  _watchDeps(depList, startFunc, stopFunc) {
-    let queuedChangeCnt = 0
-    let depCnt = depList.length
-    let satisfyCnt = 0
-    let values = {} // what's passed as the `deps` arguments
-    let bindTuples = [] // array of [ eventName, handlerFunc ] arrays
-    let isCallingStop = false
-
-    const onBeforeDepChange = (depName, val, isOptional) => {
-      queuedChangeCnt++
-      if (queuedChangeCnt === 1) { // first change to cause a "stop" ?
-        if (satisfyCnt === depCnt) { // all deps previously satisfied?
-          isCallingStop = true
-          stopFunc(values)
-          isCallingStop = false
-        }
-      }
-    }
-
-    const onDepChange = (depName, val, isOptional) => {
-
-      if (val === undefined) { // unsetting a value?
-
-        // required dependency that was previously set?
-        if (!isOptional && values[depName] !== undefined) {
-          satisfyCnt--
-        }
-
-        delete values[depName]
-      } else { // setting a value?
-
-        // required dependency that was previously unset?
-        if (!isOptional && values[depName] === undefined) {
-          satisfyCnt++
-        }
-
-        values[depName] = val
-      }
-
-      queuedChangeCnt--
-      if (!queuedChangeCnt) { // last change to cause a "start"?
-
-        // now finally satisfied or satisfied all along?
-        if (satisfyCnt === depCnt) {
-
-          // if the stopFunc initiated another value change, ignore it.
-          // it will be processed by another change event anyway.
-          if (!isCallingStop) {
-            startFunc(values)
-          }
-        }
-      }
-    }
-
-    // intercept for .on() that remembers handlers
-    const bind = (eventName, handler) => {
-      this.on(eventName, handler)
-      bindTuples.push([ eventName, handler ])
-    }
-
-    // listen to dependency changes
-    depList.forEach((depName) => {
-      let isOptional = false
-
-      if (depName.charAt(0) === '?') { // TODO: more DRY
-        depName = depName.substring(1)
-        isOptional = true
-      }
-
-      bind('before:change:' + depName, function(val) {
-        onBeforeDepChange(depName, val, isOptional)
-      })
-
-      bind('change:' + depName, function(val) {
-        onDepChange(depName, val, isOptional)
-      })
-    })
-
-    // process current dependency values
-    depList.forEach((depName) => {
-      let isOptional = false
-
-      if (depName.charAt(0) === '?') { // TODO: more DRY
-        depName = depName.substring(1)
-        isOptional = true
-      }
-
-      if (this.has(depName)) {
-        values[depName] = this.get(depName)
-        satisfyCnt++
-      } else if (isOptional) {
-        satisfyCnt++
-      }
-    })
-
-    // initially satisfied
-    if (satisfyCnt === depCnt) {
-      startFunc(values)
-    }
-
-    return {
-      teardown: () => {
-        // remove all handlers
-        for (let i = 0; i < bindTuples.length; i++) {
-          this.off(bindTuples[i][0], bindTuples[i][1])
-        }
-        bindTuples = null
-
-        // was satisfied, so call stopFunc
-        if (satisfyCnt === depCnt) {
-          stopFunc()
-        }
-      },
-      flash: () => {
-        if (satisfyCnt === depCnt) {
-          stopFunc()
-          startFunc(values)
-        }
-      }
-    }
-  }
-
-  flash(name) {
-    let watcher = this._watchers[name]
-
-    if (watcher) {
-      watcher.flash()
-    }
-  }
-
-}
-
-Model.prototype._globalWatchArgs = {} // mutation protection in Model.watch
-
-EmitterMixin.mixInto(Model)
-ListenerMixin.mixInto(Model)

+ 2 - 15
src/component/Component.ts

@@ -1,34 +1,21 @@
 import { removeElement } from '../util/dom-manip'
-import Model from '../common/Model'
 
-export default class Component extends Model {
+
+export default class Component {
 
   el: HTMLElement
 
 
   setElement(el: HTMLElement) {
     this.el = el
-    this.bindGlobalHandlers()
   }
 
 
   removeElement() {
-    this.unbindGlobalHandlers()
-
     removeElement(this.el)
     // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
     // We don't null-out the View's other element references upon destroy,
     //  so we shouldn't kill this.el either.
   }
 
-
-  bindGlobalHandlers() {
-    // subclasses can override
-  }
-
-
-  unbindGlobalHandlers() {
-    // subclasses can override
-  }
-
 }

+ 1 - 1
src/component/DateComponent.ts

@@ -211,7 +211,7 @@ export default abstract class DateComponent extends Component {
     assignTo(this, renderState)
 
     // rendering
-    if (isSkeletonDirty || !this.isSelectionRendered) {
+    if ((isSkeletonDirty || !this.isSkeletonRendered) || !this.isSkeletonRendered) {
       this.renderSkeleton()
       this.isSkeletonRendered = true
       this.isSizeDirty = true

+ 0 - 1
src/exports.ts

@@ -73,7 +73,6 @@ export {
 
 export { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 export { default as ListenerMixin, ListenerInterface } from './common/ListenerMixin'
-export { default as Model } from './common/Model'
 export { default as UnzonedRange } from './models/UnzonedRange'
 export { defineThemeSystem } from './theme/ThemeRegistry'
 export { default as Class } from './common/Class'

+ 1 - 1
src/reducers/main.ts

@@ -56,7 +56,7 @@ export function reduce(state: CalendarState, action: any, calendar: Calendar): C
         newState.dateProfile = action.dateProfile
         newState.currentDate = action.dateProfile.date // might have been constrained by view dates
 
-        calendar.onDateProfileChange(action.dateProfile)
+        calendar.view.updateMiscDateProps(action.dateProfile)
       }
       break
 

+ 4 - 4
src/theme/Theme.ts

@@ -3,7 +3,7 @@ import { assignTo } from '../util/object'
 
 export default class Theme {
 
-  optionsManager: any
+  calendarOptions: any
 
   // settings. default values are set after the class
   classes: any
@@ -14,8 +14,8 @@ export default class Theme {
   iconOverridePrefix: string
 
 
-  constructor(optionsManager) {
-    this.optionsManager = optionsManager
+  constructor(calendarOptions) {
+    this.calendarOptions = calendarOptions
     this.processIconOverride()
   }
 
@@ -23,7 +23,7 @@ export default class Theme {
   processIconOverride() {
     if (this.iconOverrideOption) {
       this.setIconOverride(
-        this.optionsManager.get(this.iconOverrideOption)
+        this.calendarOptions[this.iconOverrideOption]
       )
     }
   }

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

@@ -187,7 +187,7 @@ export function findChildren(parent: HTMLElement[] | HTMLElement, selector?: str
 // Attributes
 // ----------------------------------------------------------------------------------------------------------------
 
-export function forceClassName(el: HTMLElement, className: string, bool) {
+export function forceClassName(el: HTMLElement, className: string, bool) { // might not be used anywhere
   if (bool) {
     el.classList.add(className)
   } else {