Source: components/widgets/accordion/_accordion.js

'use strict';

/**
 * Accordion setup and control scripts
 * @namespace accordion
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object
 * @param {object} _u - UCASUtilities object
 */
(function (global, _u) {
  // Create namespace for public methods
  global.accordion = {}
  var attributePrefix = 'data-'

  /**
   * Initialise the Accordions
   * @function init
   * @memberof! UCASDesignFramework.accordion
   * @public
   * @param {Node} context - DOM element we're limiting selection to
   * @todo DF-1123 Make the context optional
   */
  function init (context) {
    context = context || document

    var targets = context.querySelectorAll('[accordion-trigger]')
    if (targets.length > 0) {
      attributePrefix = ''
    } else {
      targets = document.querySelectorAll('[' + attributePrefix + 'accordion-trigger]')
    }

    // Support legacy markup by updating it.
    targets = Array.prototype.slice.call(targets)
    targets = targets.concat(updateLegacyMarkup(document.querySelectorAll('.accordion__toggle:not([' + attributePrefix + 'accordion-trigger])')))

    // Add event listeners for all toggles.
    _u.forEach(targets, function (i, el) {
      el.addEventListener('click', toggleAccordion)
      el.addEventListener('keypress', function (ev) {
        toggleAccordion(ev)
      })
    })

    // Set up the controls for any accordions that reqest them.
    createControls(context)
    var accordionControls = context.querySelectorAll('[' + attributePrefix + 'accordion-controler]')
    _u.forEach(accordionControls, function (i, el) {
      if (el.attributes[attributePrefix + 'accordion-controler'].value === 'expand') {
        el.addEventListener('click', expandAll)
        el.addEventListener('keypress', function (ev) {
          expandAll(ev)
        })
      } else if (el.attributes[attributePrefix + 'accordion-controler'].value === 'collapse') {
        el.addEventListener('click', collapseAll)
        el.addEventListener('keypress', function (ev) {
          collapseAll(ev)
        })
      }
    })
  }

  /**
   * Support legacy markup by updating it.
   * @param {NodeList} legacyTargets - nodelist of nodes with legacy markup
   * @return {Array} - array of updated nodes
   */
  function updateLegacyMarkup (legacyTargets) {
    if (legacyTargets.length === 0) {
      return []
    }

    _u.forEach(legacyTargets, function (i, el) {
      el.setAttribute('aria-expanded', 'false')
      var inner = el.parentElement.querySelector('.accordion__inner')
      inner.setAttribute(attributePrefix + 'accordion-state', 'collapsed')
      inner.setAttribute('aria-hidden', 'true')
    })

    return Array.prototype.slice.call(legacyTargets)
  }

  function createControls (context) {
    // Create expand/collapse control
    var accordionParents = context.querySelectorAll('[' + attributePrefix + 'accordion-controls="true"]')
    if (accordionParents.length === 0) {
      return
    }
    var controlMarkup = '<button ' + attributePrefix + 'accordion-controler="expand" tabindex="0" class="button button--secondary">expand all</button><button ' + attributePrefix + 'accordion-controler="collapse" tabindex="0" class="button button--secondary">collapse all</button>'
    _u.forEach(accordionParents, function (i, el) {
      if (el.querySelector('.accordion-controller-wrapper') === null) {
        var expandCollapseControl = context.createElement('div')
        expandCollapseControl.classList.add('accordion-controller-wrapper')
        expandCollapseControl.innerHTML = controlMarkup
        el.insertBefore(expandCollapseControl, el.firstChild)
      }
    })
  }

  /**
   * Handles accordion behaviour and collapses/expands children and/or siblings
   * @param {Event} ev - DOM event
   */
  function toggleAccordion (ev) {
    var target
    if (clickOrKeypress(ev) === 'click') { // Is this a click?
      target = this
    } else if (clickOrKeypress(ev) === 'enter' || clickOrKeypress(ev) === 'space') { // Is this the enter or space key?
      ev.preventDefault()
      target = ev.target
    } else { // Is this any other key?
      return
    }
    var targetAccordion = target.parentElement.querySelector('.accordion__inner')

    // To support legacy markup, we also have to cope with accordion-state not being set:
    // !targetAccordion.attributes[attributePrefix + 'accordion-state']
    if (!targetAccordion.attributes[attributePrefix + 'accordion-state'] || targetAccordion.attributes[attributePrefix + 'accordion-state'].value === 'collapsed') {
      if (getAccordionMode(target) === 'toggle') {
        collapseExpandedSiblings(targetAccordion)
      }
      expandAccordion(targetAccordion)
      target.setAttribute('aria-expanded', 'true')
    } else {
      collapseExpandedChildren(targetAccordion)
      collapseAccordion(targetAccordion)
      target.setAttribute('aria-expanded', 'false')
    }
  }

  function clickOrKeypress (ev) {
    switch (ev.which) {
      case 1:
        return 'click'
      case 13:
        return 'enter'
      case 32:
        return 'space'
      default:
        return false
    }
  }

  /**
   * Collapse all expanded children of passed in element
   * @param {DOMelement} el // Parent element
   */
  function collapseExpandedChildren (el) {
    var elementsTocollapse = el.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
    var accordionToggles = el.querySelectorAll('[aria-expanded="true"]')
    _u.forEach(accordionToggles, function (i, el) {
      el.setAttribute('aria-expanded', 'false')
    })
    _u.forEach(elementsTocollapse, function (i, el) {
      collapseAccordion(el)
    })
  }

  function collapseAll (ev) {
    var target
    if (clickOrKeypress(ev) === 'click') { // Is this a click?
      target = this
    } else if (clickOrKeypress(ev) === 'enter' || clickOrKeypress(ev) === 'space') { // Is this the enter or space key?
      target = ev.target
    } else { // Is this any other key?
      return
    }

    ev.preventDefault()

    var parent = target.parentElement.parentElement
    var elementsToCollapse = parent.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
    var expandedToggles = parent.querySelectorAll('[aria-expanded="true"]')
    _u.forEach(expandedToggles, function (i, el) {
      el.setAttribute('aria-expanded', 'false')
    })
    _u.forEach(elementsToCollapse, function (i, el) {
      collapseAccordion(el)
    })
  }

  function expandAll (ev) {
    var target
    if (clickOrKeypress(ev) === 'click') { // Is this a click?
      target = this
    } else if (clickOrKeypress(ev) === 'enter' || clickOrKeypress(ev) === 'space') { // Is this the enter or space key?
      target = ev.target
    } else { // Is this any other key?
      return
    }

    ev.preventDefault()

    var parent = target.parentElement.parentElement
    var elementsToExpand = parent.querySelectorAll('[' + attributePrefix + 'accordion-state="collapsed"]')
    var expandedToggles = parent.querySelectorAll('[aria-expanded="false"]')
    _u.forEach(expandedToggles, function (i, el) {
      el.setAttribute('aria-expanded', 'true')
    })
    _u.forEach(elementsToExpand, function (i, el) {
      expandAccordion(el)
    })
  }

  /**
   * Collapse all expanded siblings of passed in element
   * @param {DOMelement} el // Parent element
   */
  function collapseExpandedSiblings (el) {
    var accordionParent = el.parentElement.parentElement
    var accordionToggles = accordionParent.querySelectorAll('[aria-expanded="true"]')
    _u.forEach(accordionToggles, function (i, el) {
      el.setAttribute('aria-expanded', 'false')
    })
    var expandedTargets = accordionParent.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
    _u.forEach(expandedTargets, function (i, el) {
      collapseAccordion(el)
    })
  }

  /**
   * Gets data attribute from parent element
   * Returns value of accordion-mode attribute of the parent accordion or false if not present
   * @param {DOMelement} el // Target element
   * @return {string|false} - accordion mode or false
   */
  function getAccordionMode (el) {
    if (el.parentElement.parentElement.attributes[attributePrefix + 'accordion-mode']) {
      return el.parentElement.parentElement.attributes[attributePrefix + 'accordion-mode'].value
    } else {
      return false
    }
  }

  function expandAccordion (target) {
    target.setAttribute(attributePrefix + 'accordion-state', 'expanded')
    target.setAttribute('aria-hidden', 'false')
    target.previousElementSibling.setAttribute('aria-expanded', true)

    // Make sure we remove the hiding attribute if the accordion is quickly re-opened.
    target.removeAttribute('data-accordion-hiding')
    // Setting a data attribute so we can control when the element is set to height:auto
    // which we want to do to make sure all content is visible.
    target.setAttribute('data-accordion-expanding', '')
    // This timeout is not strictly necessary but allows for CSS animations to take place beforehand.
    target.timer = window.setTimeout(function () {
      target.removeAttribute('data-accordion-expanding')
    }, 400)

    // @event designFrameworkAccordionExpandAccordion
    var event = document.createEvent('Event')
    event.initEvent('designFrameworkAccordionExpandAccordion', true, true)
    target.dispatchEvent(event)
  }

  function collapseAccordion (target) {
    target.setAttribute(attributePrefix + 'accordion-state', 'collapsed')
    target.setAttribute('aria-hidden', 'true')
    target.previousElementSibling.setAttribute('aria-expanded', false)
    var expandedChildren = target.parentElement.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
    _u.forEach(expandedChildren, function (i, el) {
      el.setAttribute(attributePrefix + 'accordion-state', 'collapsed')
      el.setAttribute('aria-hidden', 'true')
      el.previousElementSibling.setAttribute('aria-expanded', false)
    })

    // Make sure we remove the expanding attribute if the accordion is quickly re-closed.
    target.removeAttribute('data-accordion-expanding')
    // Setting a data attribute so we can control when the element is set to display:none
    // which we want to do to avoid it being tabable when hidden.
    target.setAttribute('data-accordion-hiding', '')
    target.setAttribute('aria-hidden', 'true')
    // This timeout is not strictly necessary but allows for CSS animations to take place beforehand.
    target.timer = window.setTimeout(function () {
      target.removeAttribute('data-accordion-hiding')
    }, 400)

    // @event designFrameworkAccordionCollapseAccordion
    var event = document.createEvent('Event')
    event.initEvent('designFrameworkAccordionCollapseAccordion', true, true)
    target.dispatchEvent(event)
  }

  /**
   * Assign public methods.
   */
  global.accordion = {
    addSubscriptions: true,
    init: init
  }
})(UCASDesignFramework, UCASUtilities)