modal.js 3.99 KB
import { NAME_MODAL } from '../../constants/components'
import { EVENT_NAME_SHOW, EVENT_OPTIONS_PASSIVE } from '../../constants/events'
import { CODE_ENTER, CODE_SPACE } from '../../constants/key-codes'
import { getAttr, hasAttr, isDisabled, matches, select, setAttr } from '../../utils/dom'
import { getRootActionEventName, eventOn, eventOff } from '../../utils/events'
import { isString } from '../../utils/inspect'
import { keys } from '../../utils/object'
import { getEventRoot } from '../../utils/get-event-root'
import { getInstanceFromDirective } from '../../utils/get-instance-from-directive'

// Emitted show event for modal
const ROOT_ACTION_EVENT_NAME_SHOW = getRootActionEventName(NAME_MODAL, EVENT_NAME_SHOW)

// Prop name we use to store info on root element
const PROPERTY = '__bv_modal_directive__'

const getTarget = ({ modifiers = {}, arg, value }) => {
  // Try value, then arg, otherwise pick last modifier
  return isString(value) ? value : isString(arg) ? arg : keys(modifiers).reverse()[0]
}

const getTriggerElement = el => {
  // If root element is a dropdown-item or nav-item, we
  // need to target the inner link or button instead
  return el && matches(el, '.dropdown-menu > li, li.nav-item') ? select('a, button', el) || el : el
}

const setRole = trigger => {
  // Ensure accessibility on non button elements
  if (trigger && trigger.tagName !== 'BUTTON') {
    // Only set a role if the trigger element doesn't have one
    if (!hasAttr(trigger, 'role')) {
      setAttr(trigger, 'role', 'button')
    }
    // Add a tabindex is not a button or link, and tabindex is not provided
    if (trigger.tagName !== 'A' && !hasAttr(trigger, 'tabindex')) {
      setAttr(trigger, 'tabindex', '0')
    }
  }
}

const bind = (el, binding, vnode) => {
  const target = getTarget(binding)
  const trigger = getTriggerElement(el)
  if (target && trigger) {
    const handler = event => {
      // `currentTarget` is the element with the listener on it
      const currentTarget = event.currentTarget
      if (!isDisabled(currentTarget)) {
        const type = event.type
        const key = event.keyCode
        // Open modal only if trigger is not disabled
        if (
          type === 'click' ||
          (type === 'keydown' && (key === CODE_ENTER || key === CODE_SPACE))
        ) {
          getEventRoot(getInstanceFromDirective(vnode, binding)).$emit(
            ROOT_ACTION_EVENT_NAME_SHOW,
            target,
            currentTarget
          )
        }
      }
    }
    el[PROPERTY] = { handler, target, trigger }
    // If element is not a button, we add `role="button"` for accessibility
    setRole(trigger)
    // Listen for click events
    eventOn(trigger, 'click', handler, EVENT_OPTIONS_PASSIVE)
    if (trigger.tagName !== 'BUTTON' && getAttr(trigger, 'role') === 'button') {
      // If trigger isn't a button but has role button,
      // we also listen for `keydown.space` && `keydown.enter`
      eventOn(trigger, 'keydown', handler, EVENT_OPTIONS_PASSIVE)
    }
  }
}

const unbind = el => {
  const oldProp = el[PROPERTY] || {}
  const trigger = oldProp.trigger
  const handler = oldProp.handler
  if (trigger && handler) {
    eventOff(trigger, 'click', handler, EVENT_OPTIONS_PASSIVE)
    eventOff(trigger, 'keydown', handler, EVENT_OPTIONS_PASSIVE)
    eventOff(el, 'click', handler, EVENT_OPTIONS_PASSIVE)
    eventOff(el, 'keydown', handler, EVENT_OPTIONS_PASSIVE)
  }
  delete el[PROPERTY]
}

const componentUpdated = (el, binding, vnode) => {
  const oldProp = el[PROPERTY] || {}
  const target = getTarget(binding)
  const trigger = getTriggerElement(el)
  if (target !== oldProp.target || trigger !== oldProp.trigger) {
    // We bind and rebind if the target or trigger changes
    unbind(el, binding, vnode)
    bind(el, binding, vnode)
  }
  // If trigger element is not a button, ensure `role="button"`
  // is still set for accessibility
  setRole(trigger)
}

const updated = () => {}

/*
 * Export our directive
 */
export const VBModal = {
  inserted: componentUpdated,
  updated,
  componentUpdated,
  unbind
}