Source: components/forms/_form-input-range.js

'use strict';

/**
 * Range progress component.
 * @namespace rangeAction
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} _u - UCASUtilities object.
 */
(function (global, _u) {
  var rangeInputs = {}

  /**
   * Initialises any form items it finds that are of type "range" to:
   * - Allow the background before and after the thumb to be different colours.
   *   Only needed for webkit browsers as Firefox and IE have their own
   *   prefixed pseudo elements to handle this.
   * - Update any associated inputs with the current range value.
   * @function init
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} [context=document] - context to limit the selector
   * @param {String} [selector=input[type="range"]] - a valid CSS selector for the range input
   * @returns {Boolean} - true for success
   * @example
   * UCASDesignFramework.rangeAction.init()
   */
  function init (context, selector) {
    context = context || document
    selector = selector || 'input[type="range"]'

    var formItems = context.querySelectorAll(selector)

    if (formItems.length === 0) {
      _u.log.info('Failed to find any range inputs to style')
      return false
    } else {
      var progressRequired, associatedRequired

      formItems.forEach(function (formItem) {
        if (!formItem.id) {
          _u.log.info('Form item must have an ID')
          return
        }
        if (rangeInputs[formItem.id]) {
          _u.log.info('Already initialised range styles on input with ID ' + formItem.id)
          return
        }

        // Will return true if the ::-webkit-slider-runnable-track pseudo element is supported or false if not.
        // If false, browser based progress styles are supported so the JavaScript method is not needed.
        progressRequired = !!window.getComputedStyle(context.querySelector(selector, '::-webkit-slider-runnable-track')).getPropertyValue('background')
        // Will return true if the range has an associated input, false if not.
        associatedRequired = !!formItem.getAttribute('data-associated-input')

        // If neither action is required skip doing anything for this input.
        if (!progressRequired && !associatedRequired) {
          return
        }

        createRangeActions(formItem, context, progressRequired, associatedRequired)
      })
    }
    return true
  }

  /**
   * Destroy all styled range inputs.
   * @function destroyAll
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} [context=document] - context to limit the selector
   * @public
   */
  function destroyAll (context) {
    context = context || document

    for (var rangeInputId in rangeInputs) {
      var formItem = context.getElementById(rangeInputId)

      if (formItem === null) {
        delete rangeInputs[rangeInputId]
      } else {
        rangeInputs[rangeInputId].destroy(formItem)
      }
    }
  }

  /**
   * RangeProgress.
   * @class
   * @memberof! UCASDesignFramework.rangeAction
   */
  var RangeProgress = {
    init: function (formItem, progressRequired, associatedRequired) {
      if (progressRequired) {
        addRangeProgress(formItem)
      }
      if (associatedRequired) {
        addRangeAssociation(formItem)
      }
    },
    destroy: function (formItem) {
      removeRangeProgress(formItem)
      removeRangeAssociation(formItem)
      delete rangeInputs[formItem.id]
    },
    progressHandler: function (event) {
      rangeProgressHandler(event.target)
    },
    updateAssociatedHandler: function (event) {
      rangeUpdateAssociatedHandler(event.target)
    },
    associatedUpdatedHandler: function (event) {
      rangeAssociatedUpdatedHandler(event.target, event.type)
    },
    associatedInput: null
  }

  /**
   * @function addRangeProgress
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @private
   */
  function addRangeProgress (formItem) {
    // Add the progress input event to the range input. The handler function had to be setup this way
    // as we need to pass the formitem and using bind makes the function appear different (causing
    // event listener not to be removed with removeEventListener).
    formItem.addEventListener('input', rangeInputs[formItem.id].progressHandler, false)
    // Add the range_progress class to disable default range styles.
    formItem.classList.add('range_progress')
    // Set the initial state of the range input.
    rangeProgressHandler(formItem)
  }

  /**
   * @function removeRangeProgress
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @private
   */
  function removeRangeProgress (formItem) {
    // Remove the progress input event from the range input.
    formItem.removeEventListener('input', rangeInputs[formItem.id].progressHandler, false)
    // Remove the background styles and range_progress class to re-enable the default range styles.
    formItem.style.removeProperty('--range-progress')
    formItem.classList.remove('range_progress')
  }

  /**
   * @function addRangeAssociation
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @private
   */
  function addRangeAssociation (formItem) {
    var associatedInput = document.querySelector(formItem.getAttribute('data-associated-input'))

    if (associatedInput) {
      associatedInput.setAttribute('data-associated-range-id', formItem.id)
      rangeInputs[formItem.id].associatedInput = associatedInput
      formItem.addEventListener('change', rangeInputs[formItem.id].updateAssociatedHandler, false)
      formItem.addEventListener('input', rangeInputs[formItem.id].updateAssociatedHandler, false)
      formItem.addEventListener('mouseup', rangeInputs[formItem.id].updateAssociatedHandler, false)
      associatedInput.addEventListener('keyup', rangeInputs[formItem.id].associatedUpdatedHandler, false)
      associatedInput.addEventListener('blur', rangeInputs[formItem.id].associatedUpdatedHandler, false)
    }
  }

  /**
   * @function removeRangeAssociation
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @private
   */
  function removeRangeAssociation (formItem) {
    var associatedInput = rangeInputs[formItem.id].associatedInput

    if (associatedInput) {
      associatedInput.removeAttribute('data-associated-range-id')
      formItem.removeEventListener('change', rangeInputs[formItem.id].updateAssociatedHandler, false)
      formItem.removeEventListener('input', rangeInputs[formItem.id].updateAssociatedHandler, false)
      formItem.removeEventListener('mouseup', rangeInputs[formItem.id].updateAssociatedHandler, false)
      associatedInput.removeEventListener('keyup', rangeInputs[formItem.id].associatedUpdatedHandler, false)
      associatedInput.removeEventListener('blur', rangeInputs[formItem.id].associatedUpdatedHandler, false)
    }
  }

  /**
   * Handler that changes the background gradient to supply a different colour before and after.
   * the thumb.
   * @function rangeProgressHandler
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @private
   */
  function rangeProgressHandler (formItem) {
    // Get the minimum and maximum possible values and the current value.
    var max = formItem.max
    var min = formItem.min
    var value = formItem.value
    // Translate the current value into a percentage value between the min and max.
    var percent = ((value - min) / (max - min)) * 100
    formItem.style.setProperty('--range-progress', percent + '%')
  }

  /**
   * Handler that changes the associated input value.
   * @function rangeUpdateAssociatedHandler
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @private
   */
  function rangeUpdateAssociatedHandler (formItem) {
    var associatedInput = rangeInputs[formItem.id].associatedInput

    if (associatedInput) {
      associatedInput.value = formItem.value
    }
  }

  /**
   * Handler that changes the range input when the associated input is changed.
   * @function rangeAssociatedUpdatedHandler
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} associatedInput - the associated input
   * @param {String} eventType - the type of event that triggered the handler
   * @param {Node} [context=document] - context to limit the selector
   * @private
   */
  function rangeAssociatedUpdatedHandler (associatedInput, eventType, context) {
    context = context || document
    eventType = eventType || 'blur'

    var formItem = context.getElementById(associatedInput.getAttribute('data-associated-range-id'))
    var maxValue = parseInt(formItem.getAttribute('max'))
    var minValue = parseInt(formItem.getAttribute('min'))
    var newValue = parseInt(associatedInput.value)

    if (isNaN(newValue)) {
      if ((associatedInput.value.length) || ((!associatedInput.value.length) && (eventType === 'blur'))) {
        newValue = formItem.value
        associatedInput.value = newValue
      }
    } else if (newValue > maxValue) {
      newValue = maxValue
      associatedInput.value = newValue
    } else if (newValue < minValue) {
      newValue = minValue
      associatedInput.value = newValue
    }

    if (associatedInput.value.length) {
      formItem.value = newValue
      rangeProgressHandler(formItem)
    }
  }

  /**
   * Creates and registers a rangeAction.
   * @function create
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @param {Node} [context=document] - context to limit the selector
   * @param {Boolean} progressRequired - true if JavaScript styling is required
   * @param {Boolean} associatedRequired - true if the range has an associated input
   * @return {RangeProgress} - the RangeProgress object
   * @example
   * UCASDesignFramework.rangeAction.create($myFormItem)
   */
  function createRangeActions (formItem, context, progressRequired, associatedRequired) {
    progressRequired = progressRequired || !!window.getComputedStyle(formItem, '::-webkit-slider-runnable-track').getPropertyValue('background')
    associatedRequired = associatedRequired || !!formItem.getAttribute('data-associated-input')

    if (rangeInputs[formItem.id] === undefined) {
      if (formItem.id) {
        rangeInputs[formItem.id] = Object.create(RangeProgress)
        rangeInputs[formItem.id].init(formItem, progressRequired, associatedRequired)
        return rangeInputs[formItem.id]
      } else {
        _u.log.info('Form item must have an ID')
      }
    }
  }

  /**
   * @function destroy
   * @memberof! UCASDesignFramework.rangeAction
   * @param {Node} formItem - the form element
   * @example
   * UCASDesignFramework.rangeAction.destroy($myFormItem)
   */
  function destroyRangeProgress (formItem) {
    if (rangeInputs[formItem.id]) {
      rangeInputs[formItem.id].destroy(formItem)
    } else {
      _u.log.info('Form item must have an ID')
    }
  }

  // Expose public methods and properties.
  global.rangeAction = {
    addSubscriptions: true,
    init: init,
    create: createRangeActions,
    destroy: destroyRangeProgress,
    destroyAll: destroyAll
  }
})(UCASDesignFramework, UCASUtilities)