DemocracyOS/democracyos

View on GitHub
lib/admin/admin-permissions/add-user-input/dropdown/dropdown.js

Summary

Maintainability
B
5 hrs
Test Coverage
import keyboardEvents from 'keyboardevent-key-polyfill'
import bean from 'bean'

keyboardEvents.polyfill()

export class Dropdown {
  constructor (options = {}) {
    if (!options.container) {
      throw new Error('Dropdown needs a container element.')
    }

    this.hide = this.hide.bind(this)
    this.show = this.show.bind(this)
    this.toggle = this.toggle.bind(this)
    this.onKeyDown = this.onKeyDown.bind(this)
    this.bindShowEvents = this.bindShowEvents.bind(this)
    this.bindHideEvents = this.bindHideEvents.bind(this)
    this.onItemClick = this.onItemClick.bind(this)
    this.onItemMouseenter = this.onItemMouseenter.bind(this)

    this.options = options
    this.items = new WeakMap()
    this.showing = false
    this.focused = null
    this.el = this.render()
    this.container = options.container

    window.requestAnimationFrame(() => {
      this.init()
    })
  }

  render () {
    throw new Error('Must implement render() method.')
  }

  renderItem () {
    throw new Error('Must implement renderItem() method.')
  }

  focusItem () {
    throw new Error('Must implement focusItem() method.')
  }

  unfocusItem () {
    throw new Error('Must implement unfocusItem() method.')
  }

  init () {
    this.container.appendChild(this.el)
    this.bindHideEvents(true)
    return this
  }

  destroy () {
    this.hide()
    this.bindHideEvents(false)
    this.container.removeChild(this.el)
    return this
  }

  show () {
    if (!this.el.hasChildNodes()) return this
    if (this.showing) return this
    this.el.style.display = 'block'

    this.bindHideEvents(false)
    window.requestAnimationFrame(() => {
      this.bindShowEvents(true)
    })

    this.showing = true
    return this
  }

  bindShowEvents (bind = true) {
    let action = bind ? 'on' : 'off'
    bean[action](this.container, 'mousedown', this.hide)
    bean[action](document.documentElement, 'keydown', this.onKeyDown)
    bean[action](document.documentElement, 'mousedown', this.hide)
    bean[action](this.el, 'mousedown', '.item', this.onItemClick)
    bean[action](this.el, 'mouseenter', '*', this.onItemMouseenter)
  }

  hide () {
    if (!this.showing) return this
    this.el.style.display = 'none'

    this.bindShowEvents(false)
    window.requestAnimationFrame(() => {
      this.bindHideEvents(true)
    })

    this.showing = false
    return this
  }

  bindHideEvents (bind = true) {
    let action = bind ? 'on' : 'off'
    bean[action](this.container, 'mousedown', this.show)
  }

  toggle (...args) {
    if (this.showing) return this.hide(...args)
    return this.show(...args)
  }

  add (data) {
    if (Array.isArray(data)) {
      const fragment = document.createDocumentFragment()
      data.forEach((_data) => {
        const el = this.renderItem(_data)
        this.items.set(el, _data)
        fragment.appendChild(el)
      })
      this.el.appendChild(fragment)
    } else {
      const el = this.renderItem(data)
      this.items.set(el, data)
      this.el.appendChild(el)
    }

    return this
  }

  clear () {
    while (this.el.firstChild) this.el.removeChild(this.el.firstChild)
    return this
  }

  focus (item) {
    if (!item) return this.unfocus()
    if (this.focused && this.focused === item) return this
    this.unfocus()
    this.focusItem(item)
    this.focused = item
    return this
  }

  unfocus () {
    if (this.focused) {
      this.unfocusItem(this.focused)
      this.focused = null
    }
    return this
  }

  focusNext () {
    let item = (this.focused &&
               this.focused.nextSibling) ||
               this.el.childNodes[0]
    return this.focus(item)
  }

  focusPrev () {
    let item = (this.focused &&
               this.focused.previousSibling) ||
               this.el.childNodes[this.el.childNodes.length - 1]
    return this.focus(item)
  }

  select () {
    this.hide()
    if (!this.focused) return this

    const item = this.focused
    const data = this.items.get(item)

    this.unfocus()
    if (this.options.onSelect) {
      this.options.onSelect(data, item)
    }

    return this
  }

  onKeyDown (evt) {
    switch (evt.key) {
      case 'Escape':
        this.hide()
        break
      case 'Enter':
        this.select()
        break
      case 'ArrowUp':
        evt.preventDefault()
        evt.stopImmediatePropagation()
        this.focusPrev()
        break
      case 'ArrowDown':
        evt.preventDefault()
        evt.stopImmediatePropagation()
        this.focusNext()
        break
    }
  }

  onItemClick (evt) {
    const item = evt.currentTarget
    if (item.parentNode !== this.el) return
    this.focus(item).select()
  }

  onItemMouseenter (evt) {
    const item = evt.currentTarget
    if (item.parentNode !== this.el) return
    this.focus(item)
  }
}

export class DropdownDefaultTemplate extends Dropdown {
  render () {
    const el = document.createElement('div')
    el.classList.add('dropdown')
    el.style.display = 'none'
    return el
  }

  renderItem (data = {}) {
    const el = document.createElement('div')
    el.classList.add('item')

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        el.dataset[k] = data[k]
      }
    }

    if (data.text) el.textContent = data.text
    return el
  }

  focusItem (item) {
    return item.classList.add('active')
  }

  unfocusItem (item) {
    return item.classList.remove('active')
  }
}

export class DropdownInput extends DropdownDefaultTemplate {
  constructor (options = {}) {
    super(options)

    if (!this.options.input) {
      throw new Error('DropdownInput needs an input element.')
    }

    this.onInputKeydown = this.onInputKeydown.bind(this)

    this.input = this.options.input
  }

  init (...args) {
    super.init(...args)
    bean.on(this.input, 'keydown', this.onInputKeydown)
    return this
  }

  destroy (...args) {
    super.destroy(...args)
    bean.off(this.input, 'keydown', this.onInputKeydown)
    return this
  }

  bindShowEvents (bind = true) {
    super.bindShowEvents(bind)
    let action = bind ? 'on' : 'off'
    bean[action](this.input, 'blur', this.hide)
  }

  bindHideEvents (bind = true) {
    super.bindHideEvents(bind)
    let action = bind ? 'on' : 'off'
    bean[action](this.input, 'focus', this.show)
  }

  onInputKeydown (evt) {
    switch (evt.key) {
      case 'ArrowUp':
      case 'ArrowDown':
        if (!this.showing) this.show()
        break
    }
  }
}