Source: components/forms/form-items/form-item-range_slider/_form-item-range_slider.js

/* global noUiSlider */

'use strict';

/**
 * Range slider component.
 * @namespace rangeSlider
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} _u - UCASUtilities object.
 * @param {object} noUiSlider - noUiSlider.
 */
(function (global, _u, noUiSlider) {
  var sliders = []

  /**
   * Initialise plugin.
   * @function init
   * @memberof! UCASDesignFramework.rangeSlider
   * @public
   * @param {Node} [context=document] - DOM element containing the range slider
   */
  function init (context) {
    context = context || document
    var rangeSliders = context.querySelectorAll('.form-item--range-slider:not([data-initialised="true"])')

    _u.forEach(rangeSliders, function (i, el) {
      create(el)
    })
  }

  /**
   * Create slider.
   * @param {Node} el - a form item to be turned into a range slider
   */
  function create (el) {
    // Check it's not already initialsed.
    if (el.getAttribute('data-initialised') === 'true') {
      _u.log.warn('noUiSlider already initialised for ' + el.id)
      return
    }

    // Get attributes from element.
    el.options = JSON.parse(el.getAttribute('data-range-slider-options'))
    if (el.options === null || typeof el.options !== 'object') {
      _u.log.error('data-range-slider-options is not a JSON object for ' + el.id)
      return
    }

    // Create slider.
    var slider = document.createElement('div')
    slider.classList.add('range-slider')

    // Add to DOM.
    el.appendChild(slider)

    // Check we have inputs for this form item and set the options accordingly.
    // This is to respect the initial value set in the HTML when a form is reloaded.
    var inputs = el.querySelector('.range-slider-inputs')
    if (inputs && inputs.childElementCount === 2) {
      var start = inputs.children[0].value
      var end = ''

      // If string includes the + as added to the value when "addPlus": true, then we need to remove it again
      // during slider creation otherwise it'll be NaN.
      if (inputs.children[1].value.includes('+')) {
        end = inputs.children[1].value.substring(0, inputs.length - 1)
      } else {
        end = inputs.children[1].value
      }
      if (start && end) {
        el.options.start = [start, end]
      }
    }

    // Initialise noUiSlider.
    try {
      noUiSlider.create(slider, el.options)
    } catch (e) {
      _u.log.error('Failed to initialise noUiSlider for ' + el.id + ': ' + e)
      // Clean up the div we added.
      el.removeChild(slider)
      // Give up and go home.
      return
    }

    el.setAttribute('data-initialised', 'true')
    sliders[el.id] = slider
    // Add a reference to the slider to the main form-item container so we can refer to it later.
    // The destroy() function removes this as otherwise we'd be adding a memory leak.
    el.slider = slider

    // Check we have inputs and, if we do, change the tabindex of the handles so we can't tab to them
    // and make them hidden to screen readers.
    // See @todo DF-968.
    if (inputs) {
      _u.forEach(slider.querySelectorAll('.noUi-handle'), function (i, el) {
        el.setAttribute('tabindex', '-1')
        el.setAttribute('aria-hidden', 'true')
      })
    }

    // Add event handlers.
    addEventHandlers(el)
  }

  /**
   * Create slider from ID.
   * @function create
   * @memberof! UCASDesignFramework.rangeSlider
   * @param {string} id - the id of the form item to be turned into a range slider
   */
  function createFromId (id) {
    var el = document.getElementById(id)
    el ? create(el) : _u.log.error('No element with id ' + id)
  }

  /**
   * Add event handlers to both the noUiSlider and the original input elements.
   * @param {Node} el - the original form item
   */
  function addEventHandlers (el) {
    var firstInput = el.querySelector('input[class$="-first"]')
    var secondInput = el.querySelector('input[class$="-second"]')

    // Only add the event handlers if the inputs exist.
    if (firstInput && secondInput) {
      el.slider.noUiSlider.on('update', function (values, handle) {
        var value = Math.round(values[handle])
        if (handle) {
          if (el.options.addPlus === true && value === el.options.range.max) {
            secondInput.value = el.options.range.max + '+'
          } else {
            secondInput.value = value
          }
        } else {
          firstInput.value = value
        }
      })

      // Make a note of which input is which so the update handler knows
      // how to update noUiSlider.
      firstInput.sliderInput = 0
      secondInput.sliderInput = 1
      // Add the event listener to the main form-item container so it can handle everything.
      el.addEventListener('change', inputUpdateHandler)
    }
  }

  /**
   * Input update handler.
   * @param {Event} ev - the DOM event
   */
  function inputUpdateHandler (ev) {
    var input = ev.target

    // Basic validation checks.
    // noUiSlider handles the rest.
    if (this.options && this.options.range) {
      var min = this.options.range.min
      var max = this.options.range.max
      // For the first slider, if the value is not an integer, then set it to the minimum.
      if (input.sliderInput === 0) {
        input.value = !isNaN(Math.round(input.value)) && input.value >= min ? input.value : min
      }
      // For the second slider, if the value is not an integer, then set it to the maximum.
      if (input.sliderInput === 1) {
        input.value = !isNaN(Math.round(input.value)) & input.value >= min && input.value <= max ? input.value : max
      }
    }

    // this refers to the main form-item container that the event listener was attached to.
    this.slider.noUiSlider.set(input.sliderInput ? [null, input.value] : [input.value, null])
  }

  /**
   * Destroy slider.
   * @param {Node} el - the range slider node
   */
  function destroy (el) {
    el.slider.noUiSlider.destroy()
    el.removeAttribute('data-initialised')
    el.removeEventListener('change', inputUpdateHandler)
    el.removeChild(el.slider)
    delete el.slider
    delete el.options
    delete sliders[el.id]
  }

  /**
   * Destroy slider from ID.
   * @function destroy
   * @memberof! UCASDesignFramework.rangeSlider
   * @public
   * @param {string} id - the id of the form item
   */
  function destroyFromId (id) {
    var el = document.getElementById(id)
    el ? destroy(el) : _u.log.error('No element with id ' + id)
  }

  // Expose public methods and properties.
  global.rangeSlider = {
    init: init,
    initOnLoad: true,
    create: createFromId,
    destroy: destroyFromId,
    /**
     * @function sliders
     * @memberof! UCASDesignFramework.rangeSlider
     * @return {string[]} - list of slider ids
     */
    get sliders () {
      return Object.keys(sliders)
    }
  }
})(UCASDesignFramework, UCASUtilities, noUiSlider)