Source: components/forms/_multistep.js

/* global jQuery */

'use strict';

/**
 * Adds multistep functionality to forms using jQuery.
 * @namespace multistep
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} $ - jQuery.
 */
(function (global, $) {
  global.multistep = {}

  // @private
  var multistepForm = {}
  // @private
  var submit = {}
  // @private
  var panels = []
  // @private
  var progress = {}
  // @private
  var settings = {}
  // @private
  var defaultSettings = {
    panel: '.form-multistep--panel',
    progress: false,
    nextButtonEnabled: false,
    prevButtonEnabled: true,
    enableStepNav: false
  }

  /**
   * Initialise the multistep form.
   * @function init
   * @memberof! UCASDesignFramework.multistep
   * @public
   * @param {object} context - DOM element containing the multistep form
   */
  function init (context) {
    // Set the form.
    // Make sure there's only one. We may add support for multiple forms one day, you never know.
    multistepForm = $('[data-multistep-settings]:first-of-type')
    // Read in the settings.
    var options = multistepForm.data('multistep-settings')
    // Extend the options passed via the initialisation call with some sensible defaults.
    settings = $.extend({}, defaultSettings, options)

    // Set the panels.
    panels = $(multistepForm).find(settings.panel)
    // Insert the progress bar if it's enabled.
    if (settings.progress) {
      initProgressBar()
    }
    // Set up the initial panel.
    panels.each(initPanel)
    // Disable the submit button by default.
    // It is re-enabled on the last panel.
    submit = multistepForm.find('[type="submit"]')
    disableSubmit()
    // Update the height on window resize.
    $(window).on('resize', function () {
      updateHeight()
    })
    // Resize the container to fit the content.
    updateHeight()
  }

  /**
   * Initialise the progress bar.
   * @private
   */
  function initProgressBar () {
    // Insert the progress bar.
    progress = $('<ul>').addClass('progress-indicator js-progress-indicator--disabled').insertBefore(multistepForm)
    for (var i = 0; i < panels.length; i++) {
      var progressItem = $('<li>').html('Step' + i).appendTo(progress)
      if (i === 0) {
        progressItem.addClass('active')
      }
    }
    // Attach click handlers to the progress bar.
    enableProgressBar()
  }

  /**
   * Enable the progress bar.
   * @private
   */
  function enableProgressBar () {
    if (settings.enableStepNav) {
      $('li', progress).each(function (i) {
        $(this).on('click', function () {
          goto(i)
        })
      })
      progress.removeClass('js-progress-indicator--disabled')
    }
  }

  /**
   * Disable the progress bar.
   * @private
   */
  function disableProgressBar () {
    $('li', progress).each(function (i) {
      $(this).off('click')
    })
    progress.addClass('js-progress-indicator--disabled')
  }

  /**
   * Enable the submit button.
   * @function enableSubmit
   * @memberof! UCASDesignFramework.multistep
   * @public
   */
  function enableSubmit () {
    submit.off('click.submit')
  }

  /**
   * Disable the submit button.
   * @function disableSubmit
   * @memberof! UCASDesignFramework.multistep
   * @public
   */
  function disableSubmit () {
    submit.off('.submit')
    submit.on('click.submit', function (e) {
      e.preventDefault()
    })
  }

  /**
   * Initialise a panel.
   * @private
   * @param {number} index - the index of the panel in the panels array.
   * @param {object} panel - the panel node
   */
  function initPanel (index, panel) {
    var formItemActions = $(panel).find('.form-item__multistep-actions')
    // Check to see if form multistep actions appears on this panel.
    // If not, create it.
    if (formItemActions.length === 0) {
      formItemActions = '<div class="form-item form-item__actions form-item__multistep-actions">' +
                            '<div class="buttons">' +
                                '<div class="buttons__group"></div>' +
                                '<div class="buttons__group"></div>' +
                            '</div>' +
                        '</div>'
      $(panel).append($(formItemActions))
    }
    // Add a next button.
    if (index < panels.length - 1) {
      var nextButton = $('<button>').html('Next').addClass('button next button--primary').attr('type', 'button').prependTo($(panel).find('.buttons__group:last-child'))
      nextButton.on('click', function (e) {
        e.preventDefault()
      })
    }
    // Add a previous button.
    if (index > 0) {
      var prevButton = $('<button>').html('Previous').addClass('button previous').attr('type', 'button').prependTo($(panel).find('.buttons__group:first-child'))
      prevButton.on('click', function (e) {
        e.preventDefault()
      })
    }
    // Setup the initial panel with events.
    if (index === 0) {
      $(panel).addClass('current')
      enablePanel($(panel))
    }
    // Set the tabindex so the panel can receive focus.
    $(panel).attr('tabindex', -1)
  }

  /**
   * Disable a panel by switching off its click handlers.
   * @private
   * @param {object} [panel=The current panel] - the panel node
   * @fires multistepPanelDisabled
   */
  function disablePanel (panel) {
    panel = panel || getCurrent()
    // Switch off click handlers for the panel.
    disableNavigation(panel)
    // Set the correct class on the panel we're leaving.
    panel.removeClass('current').removeClass('first')
    // @event multistepPanelDisabled
    multistepForm.trigger('multistepPanelDisabled', [panel])
  }

  /**
   * Disable navigation.
   * @function disableNavigation
   * @memberof! UCASDesignFramework.multistep
   * @public
   * @param {object} [panel=The current panel] - the panel node
   * @fires multistepNavigationDisabled
   */
  function disableNavigation (panel) {
    panel = panel || getCurrent()
    // Switch off click handlers for the panel.
    panel.find('.form-item__actions button.next').off('click.next')
    panel.find('.form-item__actions button.previous').off('click.prev')
    // Switch off click handlers for the progress bar.
    if (settings.progress) {
      disableProgressBar()
    }
    // @event multistepNavigationDisabled
    multistepForm.trigger('multistepNavigationDisabled', [panel])
  }

  /**
   * Enable a panel by adding click handlers.
   * @private
   * @param {object} [panel=The current panel] - the panel node
   * @fires multistepPanelEnabled
   */
  function enablePanel (panel) {
    panel = panel || getCurrent()
    // Add click handlers for the panel.
    enableNavigation(panel)
    // Set the correct class on the panel.
    panel.removeClass('next').removeClass('prev').addClass('current')
    // Give the panel focus to help keyboard navigation.
    panel.focus()
    // @event multistepPanelEnabled
    multistepForm.trigger('multistepPanelEnabled', [panel])
  }

  /**
   * Enable navigation.
   * @function enableNavigation
   * @memberof! UCASDesignFramework.multistep
   * @public
   * @param {object} [panel=The current panel] - the panel node
   * @fires multistepNavigationEnabled
   * @fires multistepNextTriggered
   * @fires multistepPrevTriggered
   */
  function enableNavigation (panel) {
    panel = panel || getCurrent()
    var index = getIndex(panel)
    // Add click handlers for the panel.
    panel.find('.form-item__actions button.next').on('click.next', function () {
      if (settings.nextButtonEnabled) {
        next()
      }
      // @event multistepNextTriggered
      multistepForm.trigger('multistepNextTriggered', { 'panel': panel, 'currentIndex': index })
    })
    panel.find('.form-item__actions button.previous').on('click.prev', function () {
      if (settings.prevButtonEnabled) {
        prev()
      }
      // @event multistepPrevTriggered
      multistepForm.trigger('multistepPrevTriggered', { 'panel': panel, 'currentIndex': index })
    })
    // Switch on click handlers for the progress bar.
    if (settings.progress) {
      enableProgressBar()
    }
    // @event multistepNavigationEnabled
    multistepForm.trigger('multistepNavigationEnabled', [panel])
  }

  /**
   * Calculate the container height.
   * @private
   * @param {object} panel - the panel node
   * @returns {number} - the height in pixels
   */
  function calculateContainerHeight (panel) {
    var height = panel.outerHeight(true)
    if (settings.progress && multistepForm.find('.progress-indicator').length > 0) {
      height += multistepForm.find('.progress-indicator').outerHeight(true)
    }
    return height
  }

  /**
   * Update the container height.
   * @function updateHeight
   * @memberof! UCASDesignFramework.multistep
   * @param {object} [panel=The current panel] - the panel node
   * @public
   */
  function updateHeight (panel) {
    panel = panel || getCurrent()
    multistepForm.height(calculateContainerHeight(panel))
  }

  /**
   * Get the current panel.
   * @private
   * @returns {object} The current panel object.
   */
  function getCurrent () {
    return multistepForm.find('.current')
  }

  /**
   * Get a panel index.
   * @function getIndex
   * @memberof! UCASDesignFramework.multistep
   * @public
   * @param {object} [panel=A panel] - a panel node
   * @returns {object} The current panel object.
   */
  function getIndex (panel) {
    panel = panel || getCurrent()
    return panels.index(panel)
  }

  /**
   * Callback for successful navigation to another panel.
   * @callback onSuccessCallback
   * @memberof! UCASDesignFramework.multistep
   * @param {int} index - the index of the panel being navigated to.
   */

  /**
   * Callback for failed navigation to another panel.
   * @callback onFailureCallback
   * @memberof! UCASDesignFramework.multistep
   * @param {string} message - failure message.
   */

  /**
   * Go to the next panel.
   * @function next
   * @memberof! UCASDesignFramework.multistep
   * @public
   * @param {onSuccessCallback} onSuccess - success callback
   * @param {onFailureCallback} onFailure - failure callback
   */
  function next (onSuccess, onFailure) {
    var index = getIndex() + 1
    goto(index, onSuccess, onFailure)
  }

  /**
   * Go to the previous panel.
   * @function prev
   * @memberof! UCASDesignFramework.multistep
   * @public
   * @param {onSuccessCallback} onSuccess - success callback
   * @param {onFailureCallback} onFailure - failure callback
   */
  function prev (onSuccess, onFailure) {
    var index = getIndex() - 1
    goto(index, onSuccess, onFailure)
  }

  /**
   * Go to a panel.
   * @function goto
   * @memberof! UCASDesignFramework.multistep
   * @public
   * @param {number} index - the index of the panel in the panels array.
   * @param {onSuccessCallback} onSuccess - success callback
   * @param {onFailureCallback} onFailure - failure callback
   * @fires multistepPanelLoaded
   */
  function goto (index, onSuccess, onFailure) {
    // Check that we have a valid index.
    if (index < 0 || index >= panels.length) {
      if (typeof onFailure === 'function') {
        onFailure('Attempted to go to non-existant panel (' + index + ').')
      }
      return
    } else {
      var current = getCurrent()
      var goto = panels.eq(index)

      // Switch off any click handlers for the panel we're leaving.
      disablePanel(current)
      // Switch on the click handlers for the panel we're going to.
      enablePanel(goto)
      // Enable the submit button if we're on the last panel.
      index === panels.length - 1 ? enableSubmit() : disableSubmit()

      // Resize the container to fit the content.
      updateHeight(goto)

      // @todo This could be used elsewhere.
      // @link http://stackoverflow.com/a/22480938
      var isScrolledIntoView = function (el) {
        var elemTop = el.getBoundingClientRect().top
        var isVisible = (elemTop >= $(window).scrollTop()) && (elemTop <= window.innerHeight)
        return isVisible
      }

      // Make sure we are at least at the top of the form.
      if (!isScrolledIntoView(multistepForm.parent()[0])) {
        $(window).scrollTop(multistepForm.parent().offset().top)
      }

      // Update the progress bar.
      if (settings.progress) {
        progress.find('li').removeClass('active')
        progress.find('li:lt(' + (index + 1) + ')').addClass('active')
      }
      // @event multistepPanelLoaded
      multistepForm.trigger('multistepPanelLoaded', [goto])
      if (typeof onSuccess === 'function') {
        onSuccess(index)
      }
    }
  }

  /**
   * Assign public methods.
   */
  global.multistep = {
    init: init,
    next: next,
    prev: prev,
    goto: goto,
    updateHeight: updateHeight,
    disableNavigation: disableNavigation,
    enableNavigation: enableNavigation,
    disableSubmit: disableSubmit,
    enableSubmit: enableSubmit,
    getIndex: getIndex
  }
})(UCASDesignFramework, jQuery)