Browse Source

ViewSpecManager -> view-spec

Adam Shaw 7 years ago
parent
commit
9cce850488

+ 37 - 11
src/Calendar.ts

@@ -5,7 +5,6 @@ import { capitaliseFirstLetter, debounce } from './util/misc'
 import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 import Toolbar from './Toolbar'
 import OptionsManager from './OptionsManager'
-import ViewSpecManager from './ViewSpecManager'
 import View from './View'
 import Theme from './theme/Theme'
 import { getThemeSystemClass } from './theme/ThemeRegistry'
@@ -32,6 +31,8 @@ import { computeEventDefUis, EventUiHash } from './component/event-rendering'
 import { BusinessHoursInput, parseBusinessHours } from './structs/business-hours'
 import PointerDragging, { PointerDragEvent } from './dnd/PointerDragging'
 import EventDragging from './interactions/EventDragging'
+import { buildViewSpecs, ViewSpecHash, ViewSpec } from './structs/view-spec'
+import * as exportHooks from './exports'
 
 
 export default class Calendar {
@@ -54,7 +55,7 @@ export default class Calendar {
   parseBusinessHours: (input: BusinessHoursInput) => EventStore
 
   optionsManager: OptionsManager
-  viewSpecManager: ViewSpecManager
+  viewSpecs: ViewSpecHash
   theme: Theme
   dateEnv: DateEnv
   defaultAllDayEventDuration: Duration
@@ -73,7 +74,7 @@ export default class Calendar {
   removeNavLinkListener: any
   windowResizeProxy: any
 
-  viewsByType: { [viewName: string]: View } // holds all instantiated view instances, current or not
+  viewsByType: { [viewType: string]: View } // holds all instantiated view instances, current or not
   view: View // the latest view, internal state, regardless of whether rendered or not
   renderedView: View // the view that is currently RENDERED, though it might not be most recent from internal state
   header: Toolbar
@@ -99,7 +100,6 @@ export default class Calendar {
     this.viewsByType = {}
 
     this.optionsManager = new OptionsManager(overrides)
-    this.viewSpecManager = new ViewSpecManager(this.optionsManager)
 
     this.buildDateEnv = reselector(buildDateEnv)
     this.buildTheme = reselector(buildTheme)
@@ -517,8 +517,10 @@ export default class Calendar {
       options.weekLabel,
       options.cmdFormatter
     )
-
-    this.viewSpecManager.clearCache()
+    this.viewSpecs = buildViewSpecs( // ineffecient to do every time?
+      (exportHooks as any).views,
+      this.optionsManager
+    )
   }
 
 
@@ -627,7 +629,7 @@ export default class Calendar {
 
   // Given a view name for a custom view or a standard view, creates a ready-to-go View object
   instantiateView(viewType: string): View {
-    let spec = this.viewSpecManager.getViewSpec(viewType)
+    let spec = this.viewSpecs[viewType]
 
     if (!spec) {
       throw new Error(`View type "${viewType}" is not valid`)
@@ -639,11 +641,11 @@ export default class Calendar {
 
   // Returns a boolean about whether the view is okay to instantiate at some point
   isValidViewType(viewType: string): boolean {
-    return Boolean(this.viewSpecManager.getViewSpec(viewType))
+    return Boolean(this.viewSpecs[viewType])
   }
 
 
-  changeView(viewType: string, dateOrRange: DateRangeInput | DateInput) {
+  changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput) {
     let dateMarker = null
 
     if (dateOrRange) {
@@ -665,8 +667,8 @@ export default class Calendar {
     let spec
 
     viewType = viewType || 'day' // day is default zoom
-    spec = this.viewSpecManager.getViewSpec(viewType) ||
-      this.viewSpecManager.getUnitViewSpec(viewType, this)
+    spec = this.viewSpecs[viewType] ||
+      this.getUnitViewSpec(viewType)
 
     if (spec) {
       this.setViewType(spec.type, dateMarker)
@@ -676,6 +678,30 @@ export default class Calendar {
   }
 
 
+  // 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: string): ViewSpec | null {
+    let viewTypes
+    let i
+    let spec
+
+    // put views that have buttons first. there will be duplicates, but oh well
+    viewTypes = this.header.getViewsWithButtons() // TODO: include footer as well?
+    for (let viewType in this.viewSpecs) {
+      viewTypes.push(viewType)
+    }
+
+    for (i = 0; i < viewTypes.length; i++) {
+      spec = this.viewSpecs[viewTypes[i]]
+      if (spec) {
+        if (spec.singleUnit === unit) {
+          return spec
+        }
+      }
+    }
+  }
+
+
   // internal use only
   // does not cause a render
   setViewType(viewType: string, dateMarker?: DateMarker) {

+ 6 - 4
src/Toolbar.ts

@@ -1,6 +1,8 @@
 import { htmlEscape } from './util/html'
 import { htmlToElement, appendToElement, findElements, createElement } from './util/dom-manip'
 import { default as Component, RenderForceFlags } from './component/Component'
+import Calendar from './Calendar'
+import { ViewSpec } from './structs/view-spec'
 
 /* Toolbar with buttons and title
 ----------------------------------------------------------------------------------------------------------------------*/
@@ -16,7 +18,7 @@ export interface ToolbarRenderProps {
 
 export default class Toolbar extends Component {
 
-  calendar: any
+  calendar: Calendar
   el: HTMLElement = null
   viewsWithButtons: any
 
@@ -126,7 +128,7 @@ export default class Toolbar extends Component {
     let calendar = this.calendar
     let theme = calendar.theme
     let optionsManager = calendar.optionsManager
-    let viewSpecManager = calendar.viewSpecManager
+    let viewSpecs = calendar.viewSpecs
     let sectionEl = createElement('div', { className: 'fc-' + position })
     let calendarCustomButtons = optionsManager.computed.customButtons || {}
     let calendarButtonTextOverrides = optionsManager.overrides.buttonText || {}
@@ -140,7 +142,7 @@ export default class Toolbar extends Component {
 
         buttonGroupStr.split(',').forEach((buttonName, j) => {
           let customButtonProps
-          let viewSpec
+          let viewSpec: ViewSpec
           let buttonClick
           let buttonIcon // only one of these will be set
           let buttonText // "
@@ -163,7 +165,7 @@ export default class Toolbar extends Component {
               (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
               (buttonIcon = theme.getIconClass(buttonName)) ||
               (buttonText = customButtonProps.text)
-            } else if ((viewSpec = viewSpecManager.getViewSpec(buttonName))) {
+            } else if ((viewSpec = viewSpecs[buttonName])) {
               this.viewsWithButtons.push(buttonName)
               buttonClick = function() {
                 calendar.changeView(buttonName)

+ 3 - 2
src/View.ts

@@ -8,6 +8,7 @@ import { createDuration } from './datelib/duration'
 import { createFormatter } from './datelib/formatting'
 import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 import { OpenDateRange, parseRange, DateRange, rangesEqual } from './datelib/date-range'
+import { ViewSpec } from './structs/view-spec'
 
 
 /* An abstract class from which other views inherit from
@@ -26,7 +27,7 @@ export default abstract class View extends DateComponent {
   title: string // the text that will be displayed in the header's title
 
   calendar: Calendar // owner Calendar object
-  viewSpec: any
+  viewSpec: ViewSpec
   options: any // hash containing all options. already merged with view-specific-options
 
   queuedScroll: any
@@ -51,7 +52,7 @@ export default abstract class View extends DateComponent {
   usesMinMaxTime: boolean
 
 
-  constructor(calendar, viewSpec) {
+  constructor(calendar, viewSpec: ViewSpec) {
     super(null, viewSpec.options)
 
     this.calendar = calendar

+ 4 - 4
src/ViewRegistry.ts

@@ -5,11 +5,11 @@ export const viewHash = {};
 (exportHooks as any).views = viewHash
 
 
-export function defineView(viewName, viewConfig) {
-  viewHash[viewName] = viewConfig
+export function defineView(viewType: string, viewConfig) {
+  viewHash[viewType] = viewConfig
 }
 
 
-export function getViewConfig(viewName) {
-  return viewHash[viewName]
+export function getViewConfig(viewType: string) {
+  return viewHash[viewType]
 }

+ 0 - 181
src/ViewSpecManager.ts

@@ -1,181 +0,0 @@
-import { viewHash } from './ViewRegistry'
-import { mergeProps } from './util/object'
-import { mergeOptions, globalDefaults } from './options'
-import { Duration, createDuration, getWeeksFromInput, greatestDurationDenominator } from './datelib/duration'
-
-
-export default class ViewSpecManager {
-
-  optionsManager: any
-  viewSpecCache: any // cache of view definitions (initialized in Calendar.js)
-
-
-  constructor(optionsManager) {
-    this.optionsManager = optionsManager
-    this.clearCache()
-  }
-
-
-  clearCache() {
-    this.viewSpecCache = {}
-  }
-
-
-  // Gets information about how to create a view. Will use a cache.
-  getViewSpec(viewType) {
-    let cache = this.viewSpecCache
-
-    return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType))
-  }
-
-
-  // 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, calendar) {
-    let viewTypes
-    let i
-    let spec
-
-    // put views that have buttons first. there will be duplicates, but oh well
-    viewTypes = calendar.header.getViewsWithButtons() // TODO: include footer as well?
-    for (let viewType in viewHash) {
-      viewTypes.push(viewType)
-    }
-
-    for (i = 0; i < viewTypes.length; i++) {
-      spec = this.getViewSpec(viewTypes[i])
-      if (spec) {
-        if (spec.singleUnit === unit) {
-          return spec
-        }
-      }
-    }
-  }
-
-
-  // Builds an object with information on how to create a given view
-  buildViewSpec(requestedViewType) {
-    let viewOverrides = this.optionsManager.overrides.views || {}
-    let specChain = [] // for the view. lowest to highest priority
-    let defaultsChain = [] // for the view. lowest to highest priority
-    let overridesChain = [] // for the view. lowest to highest priority
-    let viewType = requestedViewType
-    let spec // for the view
-    let overrides // for the view
-    let durationInput
-    let duration: Duration
-
-    // iterate from the specific view definition to a more general one until we hit an actual View class
-    while (viewType) {
-      spec = viewHash[viewType]
-      overrides = viewOverrides[viewType]
-      viewType = null // clear. might repopulate for another iteration
-
-      if (typeof spec === 'function') { // TODO: deprecate
-        spec = { 'class': spec }
-      }
-
-      if (spec) {
-        specChain.unshift(spec)
-        defaultsChain.unshift(spec.defaults || {})
-        durationInput = durationInput || spec.duration
-        viewType = viewType || spec.type
-      }
-
-      if (overrides) {
-        overridesChain.unshift(overrides) // view-specific option hashes have options at zero-level
-        durationInput = durationInput || overrides.duration
-        viewType = viewType || overrides.type
-      }
-    }
-
-    spec = mergeProps(specChain)
-    spec.type = requestedViewType
-    if (!spec['class']) {
-      return false
-    }
-
-    // fall back to top-level `duration` option
-    durationInput = durationInput ||
-      this.optionsManager.dynamicOverrides.duration ||
-      this.optionsManager.overrides.duration
-
-    if (durationInput) {
-      duration = createDuration(durationInput)
-
-      if (duration) { // valid?
-
-        let denom = greatestDurationDenominator(
-          duration,
-          !getWeeksFromInput(durationInput)
-        )
-
-        spec.duration = duration
-        spec.durationUnit = denom.unit
-
-        // view is a single-unit duration, like "week" or "day"
-        // incorporate options for this. lowest priority
-        if (denom.value === 1) {
-          spec.singleUnit = denom.unit
-          overridesChain.unshift(viewOverrides[denom.unit] || {})
-        }
-      }
-    }
-
-    spec.defaults = mergeOptions(defaultsChain)
-    spec.overrides = mergeOptions(overridesChain)
-
-    this.buildViewSpecOptions(spec)
-    this.buildViewSpecButtonText(spec, requestedViewType)
-
-    return spec
-  }
-
-
-  // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
-  buildViewSpecOptions(spec) {
-    let optionsManager = this.optionsManager
-
-    spec.options = mergeOptions([ // lowest to highest priority
-      globalDefaults,
-      spec.defaults, // view's defaults (from ViewSubclass.defaults)
-      optionsManager.dirDefaults,
-      optionsManager.localeDefaults, // locale and dir take precedence over view's defaults!
-      optionsManager.overrides, // calendar's overrides (options given to constructor)
-      spec.overrides, // view's overrides (view-specific options)
-      optionsManager.dynamicOverrides // dynamically set via setter. highest precedence
-    ])
-  }
-
-
-  // Computes and assigns a view spec's buttonText-related options
-  buildViewSpecButtonText(spec, requestedViewType) {
-    let optionsManager = this.optionsManager
-
-    // given an options object with a possible `buttonText` hash, lookup the buttonText for the
-    // requested view, falling back to a generic unit entry like "week" or "day"
-    function queryButtonText(options) {
-      let buttonText = options.buttonText || {}
-      return buttonText[requestedViewType] ||
-        // view can decide to look up a certain key
-        (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
-        // a key like "month"
-        (spec.singleUnit ? buttonText[spec.singleUnit] : null)
-    }
-
-    // highest to lowest priority
-    spec.buttonTextOverride =
-      queryButtonText(optionsManager.dynamicOverrides) ||
-      queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence
-      spec.overrides.buttonText // `buttonText` for view-specific options is a string
-
-    // highest to lowest priority. mirrors buildViewSpecOptions
-    spec.buttonTextDefault =
-      queryButtonText(optionsManager.localeDefaults) ||
-      queryButtonText(optionsManager.dirDefaults) ||
-      spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
-      queryButtonText(globalDefaults) ||
-      requestedViewType // fall back to given view name
-  }
-
-}

+ 4 - 6
src/agenda/config.ts

@@ -2,12 +2,10 @@ import { defineView } from '../ViewRegistry'
 import AgendaView from './AgendaView'
 
 defineView('agenda', {
-  'class': AgendaView,
-  defaults: {
-    allDaySlot: true,
-    slotDuration: '00:30:00',
-    slotEventOverlap: true // a bad name. confused with overlap/constraint system
-  }
+  class: AgendaView,
+  allDaySlot: true,
+  slotDuration: '00:30:00',
+  slotEventOverlap: true // a bad name. confused with overlap/constraint system
 })
 
 defineView('agendaDay', {

+ 2 - 6
src/basic/config.ts

@@ -2,9 +2,7 @@ import { defineView } from '../ViewRegistry'
 import BasicView from './BasicView'
 import MonthView from './MonthView'
 
-defineView('basic', {
-  'class': BasicView
-})
+defineView('basic', BasicView)
 
 defineView('basicDay', {
   type: 'basic',
@@ -19,7 +17,5 @@ defineView('basicWeek', {
 defineView('month', {
   'class': MonthView,
   duration: { months: 1 }, // important for prev/next
-  defaults: {
-    fixedWeekCount: true
-  }
+  fixedWeekCount: true
 })

+ 7 - 18
src/list/config.ts

@@ -2,42 +2,31 @@ import { defineView } from '../ViewRegistry'
 import ListView from './ListView'
 
 defineView('list', {
-  'class': ListView,
-  buttonTextKey: 'list', // what to lookup in locale files
-  defaults: {
-    listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' } // like "January 1, 2016"
-  }
+  class: ListView,
+  listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' } // like "January 1, 2016"
 })
 
 defineView('listDay', {
   type: 'list',
   duration: { days: 1 },
-  defaults: {
-    listDayFormat: { weekday: 'long' } // day-of-week is all we need. full date is probably in header
-  }
+  listDayFormat: { weekday: 'long' } // day-of-week is all we need. full date is probably in header
 })
 
 defineView('listWeek', {
   type: 'list',
   duration: { weeks: 1 },
-  defaults: {
-    listDayFormat: { weekday: 'long' }, // day-of-week is more important
-    listDayAltFormat: { month: 'long', day: 'numeric', year: 'numeric' }
-  }
+  listDayFormat: { weekday: 'long' }, // day-of-week is more important
+  listDayAltFormat: { month: 'long', day: 'numeric', year: 'numeric' }
 })
 
 defineView('listMonth', {
   type: 'list',
   duration: { month: 1 },
-  defaults: {
-    listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
-  }
+  listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
 })
 
 defineView('listYear', {
   type: 'list',
   duration: { year: 1 },
-  defaults: {
-    listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
-  }
+  listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
 })

+ 139 - 0
src/structs/view-def.ts

@@ -0,0 +1,139 @@
+import { assignTo, mapHash } from '../util/object'
+import { refineProps } from '../util/misc'
+
+export type ViewClass = any
+
+export interface ViewDefInputObject {
+  type?: string
+  class?: ViewClass
+  [optionName: string]: any
+}
+
+export type ViewDefInput = ViewClass | ViewDefInputObject
+export type ViewDefInputHash = { [viewType: string]: ViewDefInput }
+
+export interface ViewDefParse {
+  superType: string
+  class: ViewClass | null
+  options: any
+}
+
+export type ViewDefParseHash = { [viewType: string]: ViewDefParse }
+
+export interface ViewDef {
+  type: string
+  class: ViewClass
+  overrides: any
+  defaults: any
+}
+
+export type ViewDefHash = { [viewType: string]: ViewDef }
+
+export function parseViewDefInputs(inputs: ViewDefInputHash): ViewDefParseHash {
+  return mapHash(inputs, parseViewDefInput)
+}
+
+export function compileViewDefs(defaults: ViewDefParseHash, overrides: ViewDefParseHash): ViewDefHash {
+  let hash: ViewDefHash = {}
+  let viewType: string
+
+  for (viewType in defaults) {
+    ensureViewDef(viewType, hash, defaults, overrides)
+  }
+
+  for (viewType in overrides) {
+    ensureViewDef(viewType, hash, defaults, overrides)
+  }
+
+  return hash
+}
+
+const VIEW_DEF_PROPS = {
+  type: String,
+  class: null
+}
+
+function parseViewDefInput(input: ViewDefInput): ViewDefParse {
+  if (typeof input === 'function') {
+    input = { class: input }
+  }
+
+  let options = {}
+  let props = refineProps(input, VIEW_DEF_PROPS, {}, options)
+
+  return {
+    superType: props.type,
+    class: props.class,
+    options
+  }
+}
+
+function ensureViewDef(viewType: string, hash: ViewDefHash, defaults: ViewDefParseHash, overrides: ViewDefParseHash): ViewDef | null {
+  if (hash[viewType]) {
+    return hash[viewType]
+  }
+
+  let viewDef = buildViewDef(viewType, hash, defaults, overrides)
+
+  if (viewDef) {
+    hash[viewType] = viewDef
+  }
+
+  return viewDef
+}
+
+function buildViewDef(viewType: string, hash: ViewDefHash, defaults: ViewDefParseHash, overrides: ViewDefParseHash): ViewDef | null {
+  let defaultParse = defaults[viewType]
+  let overrideParse = overrides[viewType]
+
+  let queryProp = function(name) {
+    return (defaultParse && defaultParse[name] !== null) ? defaultParse[name] :
+      ((overrideParse && overrideParse[name] !== null) ? overrideParse[name] : null)
+  }
+
+  let theClass = queryProp('class') as ViewClass
+  let superType = queryProp('superType') as string
+
+  if (!superType && theClass) {
+    superType =
+      findViewNameBySubclass(theClass, overrides) ||
+      findViewNameBySubclass(theClass, defaults)
+  }
+
+  let superDef = superType ? ensureViewDef(superType, hash, defaults, overrides) : null
+
+  if (!theClass && superDef) {
+    theClass = superDef.class
+  }
+
+  if (!theClass) {
+    return null // don't throw a warning, might be settings for a single-unit view
+  }
+
+  return {
+    type: viewType,
+    class: theClass,
+    defaults: assignTo(
+      {},
+      superDef ? superDef.defaults : {},
+      defaultParse ? defaultParse.options : {}
+    ),
+    overrides: assignTo(
+      {},
+      superDef ? superDef.overrides : {},
+      overrideParse ? overrideParse.options : {}
+    )
+  }
+}
+
+function findViewNameBySubclass(viewSubclass: ViewClass, parseHash: ViewDefParseHash): string {
+  for (let viewType in parseHash) {
+    let parsed = parseHash[viewType]
+
+    if (parsed.class && viewSubclass.prototype instanceof parsed.class) {
+      return viewType
+    }
+  }
+
+  return ''
+}

+ 102 - 0
src/structs/view-spec.ts

@@ -0,0 +1,102 @@
+import { ViewDef, ViewClass, ViewDefParseHash, ViewDefInputHash, parseViewDefInputs, compileViewDefs } from './view-def'
+import { Duration, createDuration, greatestDurationDenominator, getWeeksFromInput } from '../datelib/duration'
+import OptionsManager from '../OptionsManager'
+import { assignTo, mapHash } from '../util/object'
+import { globalDefaults } from '../options'
+
+export interface ViewSpec {
+  type: string
+  class: ViewClass
+  duration: Duration
+  durationUnit: string
+  singleUnit: string
+  options: any,
+  buttonTextOverride: string
+  buttonTextDefault: string
+}
+
+export type ViewSpecHash = { [viewType: string]: ViewSpec }
+
+export function buildViewSpecs(defaultViewInputs: ViewDefInputHash, optionsManager: OptionsManager): ViewSpecHash {
+  let defaultParses = parseViewDefInputs(defaultViewInputs)
+  let overrideParses = parseViewDefInputs(optionsManager.overrides.views)
+  let viewDefs = compileViewDefs(defaultParses, overrideParses)
+
+  return mapHash(viewDefs, function(viewDef) {
+    return buildViewSpec(viewDef, overrideParses, optionsManager)
+  })
+}
+
+function buildViewSpec(viewDef: ViewDef, viewParses: ViewDefParseHash, optionsManager: OptionsManager): ViewSpec {
+  let durationInput =
+    viewDef.overrides.duration ||
+    viewDef.defaults.duration ||
+    optionsManager.dynamicOverrides.duration ||
+    optionsManager.overrides.duration
+
+  let duration = null
+  let durationUnit = ''
+  let singleUnit = ''
+  let singleUnitOverrides = {}
+
+  if (durationInput) {
+    duration = createDuration(durationInput)
+
+    if (duration) { // valid?
+      let denom = greatestDurationDenominator(
+        duration,
+        !getWeeksFromInput(durationInput)
+      )
+
+      durationUnit = denom.unit
+
+      if (denom.value === 1) {
+        singleUnit = durationUnit
+        singleUnitOverrides = viewParses[durationUnit] ? viewParses[durationUnit].options : {}
+      }
+    }
+  }
+
+  let queryButtonText = function(options) {
+    let buttonText = options.buttonText || {}
+
+    if (buttonText[viewDef.type] != null) {
+      return buttonText[viewDef.type]
+    }
+    if (buttonText[singleUnit] != null) {
+      return buttonText[singleUnit]
+    }
+  }
+
+  return {
+    type: viewDef.type,
+    class: viewDef.class,
+    duration,
+    durationUnit,
+    singleUnit,
+
+    options: assignTo(
+      {},
+      globalDefaults,
+      viewDef.defaults,
+      optionsManager.dirDefaults,
+      optionsManager.localeDefaults,
+      optionsManager.overrides,
+      singleUnitOverrides,
+      viewDef.overrides,
+      optionsManager.dynamicOverrides
+    ),
+
+    buttonTextOverride:
+      queryButtonText(optionsManager.dynamicOverrides) ||
+      queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence
+      viewDef.overrides.buttonText, // `buttonText` for view-specific options is a string
+
+    buttonTextDefault:
+      queryButtonText(optionsManager.localeDefaults) ||
+      queryButtonText(optionsManager.dirDefaults) ||
+      viewDef.defaults.buttonText || // a single string. from ViewSubclass.defaults
+      queryButtonText(globalDefaults) ||
+      viewDef.type // fall back to given view name
+  }
+}

+ 1 - 0
tests/automated/legacy/custom-view-duration.js

@@ -1,4 +1,5 @@
 describe('custom view', function() {
+
   it('renders a 4 day basic view', function() {
     var options = {
       views: {}