wbotelhos/stepy

View on GitHub
lib/jquery.stepy.js

Summary

Maintainability
D
2 days
Test Coverage
/*!
 * jQuery Stepy - A Wizard Plugin
 *
 * The MIT License
 *
 * @author:  Washington Botelho
 * @doc:     wbotelhos.com/stepy
 * @version: 1.1.0
 *
 */

;(function($) {
  'use strict';

  var methods = {
    init: function(settings) {
      return this.each(function() {
        methods.destroy.call(this);

        this.stepyOptions = $.extend({}, $.fn.stepy.defaults, settings);

        var self = $(this);

        methods._generateId.call(this);

        if (this.stepyOptions.header) {
          this.header = methods._header.call(this);
        }

        this.steps = self.find('.' + this.stepyOptions.stepClass);

        var that = this;

        this.steps.each(function(index) {
          var self = $(this);

          self.addClass(that.stepyOptions.stepClass);

          if (index === 0) {
            self.addClass('stepy-active');
          }

          if (that.stepyOptions.header) {
            methods._createHead.call(that, this, index);
          }
        });

        methods._bindNextButton.call(this);
        methods._bindBackButton.call(this);
        methods._bindFinishButton.call(this);

        if (this.stepyOptions.header) {
          methods._bindHeader.call(this);
        }

        if (this.stepyOptions.enter) {
          methods._bindEnter.call(this);
        }

        this.steps.first().find(':input:visible:enabled:first').trigger('select').trigger('focus');

        methods._setState.call(this, { settings: this.stepyOptions, index: 0, stepy: true });

        methods._stateButton.call(this, this.steps);
      });
    },

    _bindBackButton: function() {
      var button = $(this).find(this.stepyOptions.backButton);

      if (!button.length) {
        return;
      }

      button.on('click.stepy', function(e) {
        e.preventDefault();

        var index = $(this).data('index') - 1;

        if (index >= 0 && (!this.stepyOptions.back || methods._execute.call(this, this.stepyOptions.back, index, this.steps.length))) {
          methods.step.call(this, index);
        }
      }.bind(this));
    },

    _bindEnter: function() {
      var that = this;

      this.steps.find('input:not(:button, :submit)').on('keypress.stepy', function(evt) {
        var key = (evt.keyCode ? evt.keyCode : evt.which);

        if (key === 13) {
          evt.preventDefault();

          var
            step    = $(this).closest('.' + that.stepyOptions.stepClass),
            hasNext = step.next('.' + that.stepyOptions.stepClass).length;

          if (hasNext) {
            step.find(that.stepyOptions.nextButton).trigger('click');
          } else if (that.finishButton) {
            that.finishButton.trigger('click');
          }
        }
      });
    },

    _bindFinishButton: function() {
      this.finishButton = $(this).find(this.stepyOptions.finishButton);

      if (!this.finishButton.length) {
        return $.error('submit button "' + this.stepyOptions.finishButton + '" missing.');
      }

      this.finishButton.on('click.stepy', methods._onFinishButton.bind(this));
    },

    _bindHeader: function() {
      this.heads = this.header.find('li');

      this.heads.first().addClass('stepy-active');

      if (this.stepyOptions.titleClick) {
        var that = this;

        this.heads.on('click', function() {
          methods._bindHeaderHandler.call(that, this);
        });
      } else {
        this.heads.css('cursor', 'default');
      }
    },

    _bindHeaderHandler: function(head) {
      var
        current = parseInt($(this.heads).filter('.stepy-active')[0].id.match(/\d/)[0], 10),
        clicked = parseInt(head.id.match(/\d/)[0], 10);

      if (clicked > current) {
        if (this.stepyOptions.next && !methods._execute.call(this, this.stepyOptions.next, clicked)) {
          return false;
        }
      } else if (clicked < current) {
        if (this.stepyOptions.back && !methods._execute.call(this, this.stepyOptions.back, clicked)) {
          return false;
        }
      }

      if (clicked != current) {
        methods.step.call(this, clicked);
      }
    },

    _bindNextButton: function() {
      var button = $(this).find(this.stepyOptions.nextButton);

      if (!button.length) {
        return;
      }

      button.on('click.stepy', function(e) {
        e.preventDefault();

        var index = $(this).data('index') + 1;

        if (index < this.steps.length && (!this.stepyOptions.next || methods._execute.call(this, this.stepyOptions.next, index, this.steps.length))) {
          methods.step.call(this, index);
        }
      }.bind(this));
    },

    _createHead: function(step, index) {
      var
        step = $(step).attr('id', this.getAttribute('id') + '-step-' + index),
        head = methods._head.call(this, index);

      head.append(methods._title.call(this, step));

      if (this.stepyOptions.description) {
        var description = methods._description.call(this, step);

        if (description.length) {
          head.append(description);
        }
      }

      this.header.append(head);
    },

    _description: function(step) {
      var legend = step.find('.' + this.stepyOptions.legendClass);

      if (!this.stepyOptions.legend) {
        legend.hide();
      }

      if (!legend.length) {
        return null;
      }

      return $('<span />', { html: legend.html() });
    },

    _execute: function(callback, index, totalSteps) {
      if (callback === undefined) {
        return true;
      }

      var isValid = callback.call(this, index, totalSteps);

      return isValid || isValid === undefined;
    },

    _generateId: function() {
      var id = this.getAttribute('id');

      if (id === undefined || id === '') {
        $(this).attr('id', 'stepy-' + Math.random().toString().substring(2));
      }
    },

    _head: function(index) {
      return $('<li />', { id: this.getAttribute('id') + '-head-' + index });
    },

    _header: function() {
      var header = $('<ul />', { id: this.getAttribute('id') + '-header', 'class': 'stepy-header' });

      if (this.stepyOptions.titleTarget) {
        header.appendTo(this.stepyOptions.titleTarget);
      } else {
        header.insertBefore(this);
      }

      return header;
    },

    _onFinishButton: function(evt) {
      var onSubmit = undefined;

      if (this.tagName === 'FORM') {
        onSubmit = this.getAttribute('onsubmit');

        this.setAttribute('onsubmit', 'return false;');
      }

      if (!methods._execute.call(this, this.stepyOptions.finish, this.steps.length - 1)) {
        evt.preventDefault();
      } else if (this.tagName === 'FORM') {
        if (onSubmit) {
          this.setAttribute('onsubmit', onSubmit);
        } else {
          this.removeAttribute('onsubmit');
        }

        var isSubmit = this.finishButton.attr('type') === 'submit';

        if (isSubmit && (!this.stepyOptions.validate || methods.validate.call(this, this.steps.length - 1))) {
          this.submit();
        }
      }
    },

    _setState: function(data) {
      var self = $(this);

      self.data($.extend({}, self.data(), data));
    },

    _stateButton: function(steps) {
      var
        self  = $(this),
        index = self.data('index');

      if (index === 0) {
        self.find(this.stepyOptions.backButton).removeClass('stepy-active');
        self.find(this.stepyOptions.finishButton).removeClass('stepy-active');
        self.find(this.stepyOptions.nextButton).addClass('stepy-active');
      } else if (index === steps.length - 1) {
        self.find(this.stepyOptions.backButton).addClass('stepy-active');
        self.find(this.stepyOptions.finishButton).addClass('stepy-active');
        self.find(this.stepyOptions.nextButton).removeClass('stepy-active');
      } else {
        self.find(this.stepyOptions.backButton).addClass('stepy-active');
        self.find(this.stepyOptions.finishButton).removeClass('stepy-active');
        self.find(this.stepyOptions.nextButton).addClass('stepy-active');
      }
    },

    _title: function(step) {
      return $('<div />', { html: step.attr('title') || '--' });
    },

    _transition: function(max) {
      var
        from = this.steps.eq($(this).data('index')),
        to   = this.steps.eq(max);

      if (this.stepyOptions.transition === 'fade') {
        from.animate({ opacity: 0 }, this.stepyOptions.duration, function() {
          from.removeClass('stepy-active');

          to.addClass('stepy-active').animate({ opacity: 1 }, this.stepyOptions.duration);
        }.bind(this));
      } else if (this.stepyOptions.transition === 'slide') {
        var
          forward = from.index() < to.index(),
          one     = (forward ? -1 : 1) * from.outerWidth(),
          two     = (forward ? 1 : -1) * to.outerWidth(),
          three   = 0;

        from.animate({ 'margin-left': one }, this.stepyOptions.duration, function() {
          from.removeClass('stepy-active');

          to.animate({ 'margin-left': two }, 0).addClass('stepy-active').animate({ 'margin-left': three }, this.stepyOptions.duration);
        }.bind(this));
      }
    },

    destroy: function() {
      return $(this).each(function() {
        var self = $(this);

        if (self.data('stepy')) {
          var steps = self.data('stepy', false).find('.stepy-active').removeClass('stepy-active');

          self.find('.stepy-errors').remove();
        }
      });
    },

    step: function(index) {
      var
        max   = index,
        self  = $(this),
        steps = self.find('.' + this.stepyOptions.stepClass);

      if (index > steps.length) {
        index = steps.length;
      }

      if (this.stepyOptions.validate) {
        var isValid = true;

        for (var i = 0; i < index; i++) {
          isValid &= methods.validate.call(this, i);

          if (this.stepyOptions.block && !isValid) {
            max = i;
            break;
          }
        }
      }

      if (this.stepyOptions.transition) {
        methods._transition.call(this, max);
      } else {
        steps.removeClass('stepy-active').eq(max).addClass('stepy-active');
      }

      if (this.stepyOptions.header) {
        this.heads.removeClass('stepy-active').eq(max).addClass('stepy-active');
      }

      if (this.tagName === 'FORM') {
        var fields = steps.eq(max);

        if (max === index) {
          fields = fields.find(':input:enabled:visible');
        } else {
          fields = fields.find('.error').trigger('select').trigger('focus');
        }

        fields.first().trigger('select').trigger('focus');
      }

      if (this.stepyOptions.select) {
        this.stepyOptions.select.call(this, max, steps.length);
      }

      methods._setState.call(this, { index: max });
      methods._stateButton.call(this, steps);

      return self;
    },

    validate: function(index) {
      var self = $(this);

      if (!this.tagName === 'FORM') {
        return true;
      }

      var
        that     = this,
        step     = self.find('.' + this.stepyOptions.stepClass).eq(index),
        isValid  = true,
        title    = $('#' + this.getAttribute('id') + '-header').children().eq(index);

      $(step.find(':input:enabled').get().reverse()).each(function() {
        var fieldIsValid = that.stepyOptions.validate.call(that, this);

        if (fieldIsValid === undefined) {
          fieldIsValid = true;
        }

        isValid = isValid && fieldIsValid;

        if (isValid) {
          if (that.stepyOptions.errorImage) {
            title.removeClass('stepy-error');
          }
        } else {
          if (that.stepyOptions.errorImage) {
            title.addClass('stepy-error');
          }

          this.focus();
        }
      });

      return isValid;
    }
  };

  $.fn.stepy = function(method) {
    if (methods[method]) {
      return methods[method].apply(this[0], Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    } else {
      $.error('Method ' + method + ' does not exist!');
    }
  };

  $.fn.stepy.defaults = {
    back:         undefined,
    backButton:   '.stepy-back',
    block:        false,
    description:  true,
    duration:     0,
    enter:        true,
    errorImage:   false,
    finish:       undefined,
    finishButton: '.stepy-finish',
    header:       true,
    ignore:       '',
    legend:       true,
    legendClass:  'stepy-legend',
    next:         undefined,
    nextButton:   '.stepy-next',
    select:       undefined,
    stepClass:    'stepy-step',
    titleClick:   false,
    titleTarget:  undefined,
    transition:   undefined,
    validate:     undefined
  };

})(jQuery);