capejs/capejs

View on GitHub
lib/cape/markup_builder.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict'

let virtualDom = require('virtual-dom')
let Inflector = require('inflected')
let Cape = require('./utilities')

// Cape.MarkupBuilder
//
// public properties:
//   component: the component that this builder works for.
//   formName: current form name.
//   selectBoxName: current select box name.
//   fieldNamePrefix: current field name prefix.
// private properties:
//   _: the object that holds internal methods and properties of this class.
class MarkupBuilder {
  constructor(component, options) {
    this._ = new _Internal(this)
    this.component = component
    if (options) {
      this.formName = options.formName
      this.selectBoxName = options.selectBoxName
      this.fieldNamePrefix = options.fieldNamePrefix
    }
  }

  markup(callback) {
    let root = this.component.root, formName, builder, attributes

    if (typeof callback !== 'function')
      throw new Error("The first agument must be a function.")
    if (callback.length === 0)
      throw new Error("Callback requires an argument.")
    if (root.tagName === 'form') formName = root.attributes.name
    builder = new MarkupBuilder(this.component, { formName: formName } )
    callback.call(this.component, builder)

    attributes = {}
    for (let i = root.attributes.length; i--;)
      attributes[root.attributes[i].nodeName] = root.attributes[i].value
    return this._.h(root.tagName, attributes, builder._.elements)
  }

  elem(tagName) {
    let args, options, content, callback, builder, attributes

    args = Array.prototype.slice.call(arguments, 1)
    content = this._.extractContent(args)
    options = this._.extractOptions(args)
    callback = this._.extractCallback(args)

    if (callback) {
      builder = new MarkupBuilder(this.component,
        { formName: this.formName,
          selectBoxName: this.selectBoxName,
          fieldNamePrefix: this.fieldNamePrefix })
      if (callback.length === 0) { throw new Error("Callback requires an argument.") }
      callback.call(this.component, builder)
      attributes = this._.generateAttributes(options)
      this._.elements.push(this._.h(tagName, attributes, builder._.elements))
    }
    else {
      content = content || ''
      attributes = this._.generateAttributes(options)
      this._.elements.push(this._.h(tagName, attributes, content))
    }
    return this
  }

  text(content) {
    this._.elements.push(content)
    return this
  }

  space() {
    this._.elements.push(' ')
    return this
  }

  sp() {
    this.space()
    return this
  }

  formFor(name) {
    let args, options, callback, builder, attributes

    args = Array.prototype.slice.call(arguments)
    options = this._.extractOptions(args) || {}
    callback = this._.extractCallback(args)

    if (typeof callback !== 'function')
      throw new Error("One of arguments must be a function.")
    if (callback.length === 0)
      throw new Error("Callback requires an argument.")

    builder = new MarkupBuilder(this.component, { formName: name })
    callback.call(this.component, builder)
    options = options || {}
    options.name = name
    if (options.onsubmit === undefined && this._.eventCallbacks.onsubmit === undefined) {
      options.onsubmit = function(e) { return false }
    }
    attributes = this._.generateAttributes(options)
    this._.elements.push(this._.h('form', attributes, builder._.elements))
    return this
  }

  fieldsFor(name) {
    let args, options, callback, prefix, builder

    args = Array.prototype.slice.call(arguments, 1)
    options = this._.extractOptions(args) || {}
    callback = this._.extractCallback(args)

    if (typeof callback !== 'function')
      throw new Error("One of arguments must be a function.")
    if (callback.length === 0)
      throw new Error("Callback requires an argument.")

    if (this.fieldNamePrefix !== undefined)
      prefix = this.fieldNamePrefix + '/' + name
    else
      prefix = name
    if (options.index !== undefined)
      prefix = prefix + '/' + String(options.index)

    builder = new MarkupBuilder(this.component,
      { formName: this.formName, fieldNamePrefix: prefix })
    callback.call(this.component, builder)
    builder._.elements.forEach(function(elem) {
      this._.elements.push(elem)
    }.bind(this))

    return this
  }

  labelFor(name, content, options) {
    let fieldName

    options = options || {}
    options.htmlFor = this._.elementIdFor(name)
    this.elem('label', content, options)
    return this
  }

  hiddenField(name, options) {
    return this._stringField('hidden', name, options)
  }

  textField(name, options) {
    options = options || {}
    return this._stringField(options.type || 'text', name, options)
  }

  passwordField(name, options) {
    return this._stringField('password', name, options)
  }

  _stringField(type, name, options) {
    options = options || {}
    options.type = type
    options.name = name
    this._.inputField(options)
    return this
  }

  textareaField(attrName, options) {
    let formName, vform, dasherized

    if (attrName && this.fieldNamePrefix)
      attrName = this.fieldNamePrefix + '/' + attrName
    options = options || {}
    options.name = attrName

    formName = this.formName || ''
    this.component.virtualForms.update(formName, options)

    dasherized = Inflector.dasherize(attrName.replace(/\//g, '_'))
    if (!options.id) {
      if (this.formName)
        options.id = this.formName + '-field-' + dasherized
      else
        options.id = 'field-' + dasherized
    }
    this.elem('textarea', '', options)
    return this
  }

  checkBox(attrName, options) {
    let fieldName

    options = options || {}
    options.type = 'checkbox'
    if (attrName) options.name = attrName
    if (!options.value) options.value = '1'

    if (options.name && this.fieldNamePrefix)
      fieldName = this.fieldNamePrefix + '/' + options.name
    else
      fieldName = options.name

    if (attrName.slice(-2) !== '[]') {
      this._.elements.push(
        this._.h('input', Object.assign({},
          { name: fieldName, type: 'hidden', value: '0' })))
    }
    this._.inputField(options)
    return this
  }

  radioButton(attrName, value, options) {
    options = options || {}
    options.type = 'radio'
    options.value = value
    if (attrName) options.name = attrName
    this._.inputField(options)
    return this
  }

  selectBox(name) {
    let args = Array.prototype.slice.call(arguments, 1)
    let options = this._optionsForSelect(name, args)
    let attributes = this._.generateAttributes(options)
    let formName = this.formName || ''
    let builder = new MarkupBuilder(this.component,
      { formName: this.formName, selectBoxName: name })

    this._buildOptionElements(args, builder)
    this.component.virtualForms.update(
      formName, { name: name, value: options.value })
    this._.elements.push(this._.h('select', attributes, builder._.elements))
    return this
  }

  _buildOptionElements(args, builder) {
    let callback = this._.extractCallback(args)

    if (typeof callback !== 'function')
      throw new Error("One of arguments must be a function.")
    if (callback.length === 0)
      throw new Error("Callback requires an argument.")

    callback.call(this.component, builder)
  }

  _optionsForSelect(name, args) {
    let options = this._.extractOptions(args) || {}

    if (name && this.fieldNamePrefix)
      options.name = this.fieldNamePrefix + '/' + name
    else
      options.name = name

    options.id = options.id || this._.elementIdFor(name)

    return options
  }

  btn() {
    let args, options, content, callback

    args = Array.prototype.slice.call(arguments)
    content = this._.extractContent(args)
    options = this._.extractOptions(args) || {}
    callback = this._.extractCallback(args)

    options.type = options.type || 'button'
    this.elem('button', content, options, callback)
    return this
  }

  attr(name, value) {
    return this._.setValue(this._.attr, name, value)
  }

  class(name) {
    return this._.setValue(this._.classNames, name, true)
  }

  data(name, value) {
    return this._.setValue(this._.data, name, value)
  }

  css(name, value) {
    return this._.setValue(this._.style, name, value)
  }

  on(eventName, callback) {
    if (typeof eventName === 'string')
      this._.eventCallbacks['on' + eventName] = callback
    else
      throw new Error("The first agument must be a string.")
  }

  fa(iconName, options) {
    options = options || {}
    let htmlClass = options.class || options.className
    if (htmlClass) {
      htmlClass = htmlClass + ' fa fa-' + iconName
    }
    else {
      htmlClass = 'fa fa-' + iconName
    }
    options.class = htmlClass
    this.i('', options)
    return this
  }
}

// Internal Class of Cape.MarkupBuilder
class _Internal {
  constructor(main) {
    this.main = main
    this.h = virtualDom.h
    this.elements = []
    this.classNames = {}
    this.attr = {}
    this.data = {}
    this.style = {}
    this.eventCallbacks = {}
  }

  inputField(options) {
    let attributes, dasherized, formName, vform

    options = options || {}

    this.normalizeInputFieldId(options)

    if (options.name && this.main.fieldNamePrefix)
      options.name = this.main.fieldNamePrefix + '/' + options.name

    formName = this.main.formName || ''
    this.main.component.virtualForms.update(formName, options)

    attributes = this.generateAttributes(options)
    this.elements.push(this.h('input', attributes))
    return this
  }

  normalizeInputFieldId(options) {
    if (options.id === undefined) {
      options.id = this.elementIdFor(options.name)
      if (options.type === 'radio')
        options.id = options.id + '-' + String(options.value)
    }
    if (options.id === null) delete options.id
  }

  elementIdFor(name) {
    let dasherized

    if (this.main.fieldNamePrefix)
      dasherized = Inflector.dasherize(
        this.main.fieldNamePrefix.replace(/\//g, '-') + '-' + name)
    else
      dasherized = Inflector.dasherize(name)

    if (this.main.formName)
      return this.main.formName + '-field-' + dasherized
    else
      return 'field-' + dasherized
  }

  extractContent(args) {
    if (typeof args[0] === 'string') return args[0]
  }

  extractOptions(args) {
    return this.extract('object', args)
  }

  extractCallback(args) {
    return this.extract('function', args)
  }

  extract(type, args) {
    for (let i = 0; i < args.length; i++)
      if (typeof args[i] === type) return args[i]
  }

  generateAttributes(options) {
    options = options || {}
    options = Object.assign({}, this.attr, options)
    this.attr = {}

    this.setHtmlFor(options)
    this.setClassName(options)
    this.setDataset(options)
    this.setStyle(options)

    Cape.merge(options, this.eventCallbacks)
    this.eventCallbacks = {}

    for (let key in options) {
      if (typeof options[key] === 'function') {
        options[key] = options[key].bind(this.main.component)
      }
    }
    return options
  }

  setHtmlFor(options) {
    if ('for' in options) {
      options['htmlFor'] = options['for']
      delete options['for']
    }
  }

  setClassName(options) {
    if ('class' in options) {
      options['className'] = options['class']
      delete options['class']
    }

    let classNames = []

    this.copyClassNames(classNames)
    this.classNames = []

    this.extractClassNames(classNames, options)

    if (classNames.length) {
      classNames = classNames.filter(function(e, i, self) {
        return self.indexOf(e) === i
      })
      options['className'] = classNames.join(' ')
    }
    else {
      delete options['className']
    }
  }

  copyClassNames(classNames) {
    for (let key in this.classNames)
      if (this.classNames.hasOwnProperty(key) && this.classNames[key])
        classNames.push(key)
  }

  extractClassNames(classNames, options) {
    if (typeof options['className'] === 'object') {
      for (let name in options['className']) {
        if (options['className'][name]) classNames.push(name)
      }
    }
    else if (typeof options['className'] === 'string') {
      options['className'].split(' ').forEach(e => classNames.push(e))
    }
  }

  setDataset(options) {
    if ('data' in options) {
      options['dataset'] = options['data']
      delete options['data']
    }

    let data = options.dataset || {}
    data = Object.assign({}, this.data, data)
    this.data = {}
    options.dataset = data
  }

  setStyle(options) {
    if ('visible' in options && !options['visible']) {
      options['style'] = options['style'] || {}
      options['style']['display'] = 'none'
    }

    if (typeof options.style === 'object')
      options.style = Object.assign({}, this.style, options.style)
    else
      options.style = this.style
    this.style = {}
  }

  setValue(target, name, value) {
    if (typeof name === 'object')
      Object.assign(target, name)
    else if (typeof name === 'string')
      target[name] = value

    return this.main
  }
}

let normalElementNames = [
  'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
  'blockquote', 'body', 'button', 'canvas', 'caption', 'cite', 'code',
  'colgroup', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div',
  'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer',
  'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'html',
  'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark',
  'menu', 'menuitem', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup',
  'option', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's',
  'samp', 'script', 'section', 'select', 'small', 'span', 'strong', 'style',
  'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
  'th', 'thead', 'time', 'title', 'tr', 'u', 'ul', 'var', 'video' ]

let i, tagName, attrName, eventName

for (i = normalElementNames.length; i--;) {
  tagName = normalElementNames[i]
  MarkupBuilder.prototype[tagName] = new Function("arg1", "arg2",
    "this.elem('" + tagName + "', arg1, arg2); return this")
}

let voidElementNames = [
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
  'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'
]

for (i = voidElementNames.length; i--;) {
  tagName = voidElementNames[i]
  MarkupBuilder.prototype[tagName] = new Function("options",
    "this.elem('" + tagName + "', options); return this")
}

let attrNames = [ 'checked', 'disabled' ]

for (i = attrNames.length; i--;) {
  attrName = attrNames[i]
  MarkupBuilder.prototype[attrName] = new Function("value",
    "this.attr('" + attrName + "', value); return this")
}

let eventNames = [
  'blur', 'focus', 'change', 'select', 'submit', 'reset', 'abort', 'error',
  'load', 'unload', 'click', 'dblclick', 'keyup', 'keydown', 'keypress',
  'mouseout', 'mouseover', 'mouseup', 'mousedown', 'mousemove'
]

for (i = eventNames.length; i--;) {
  eventName = eventNames[i]
  MarkupBuilder.prototype['on' + eventName] = new Function("callback",
    "this.on('" + eventName + "', callback); return this")
}

module.exports = MarkupBuilder