/* * Fuel UX Wizard * https://github.com/ExactTarget/fuelux * * Copyright (c) 2014 ExactTarget * Licensed under the BSD New license. */ // -- BEGIN UMD WRAPPER PREFACE -- // For more information on UMD visit: // https://github.com/umdjs/umd/blob/master/jqueryPlugin.js (function (factory) { if (typeof define === 'function' && define.amd) { // if AMD loader is available, register as an anonymous module. define(['jquery'], factory); } else if (typeof exports === 'object') { // Node/CommonJS module.exports = factory(require('jquery')); } else { // OR use browser globals if AMD is not present factory(jQuery); } }(function ($) { // -- END UMD WRAPPER PREFACE -- // -- BEGIN MODULE CODE HERE -- var old = $.fn.wizard; // WIZARD CONSTRUCTOR AND PROTOTYPE var Wizard = function (element, options) { var kids; this.$element = $(element); this.options = $.extend({}, $.fn.wizard.defaults, options); this.options.disablePreviousStep = (this.$element.attr('data-restrict') === 'previous') ? true : this.options.disablePreviousStep; this.currentStep = this.options.selectedItem.step; this.numSteps = this.$element.find('.steps li').length; this.$prevBtn = this.$element.find('button.btn-prev'); this.$nextBtn = this.$element.find('button.btn-next'); // maintains backwards compatibility with < 3.8, will be removed in the future if (this.$element.children('.steps-container').length === 0) { this.$element.addClass('no-steps-container'); if (window && window.console && window.console.warn) { window.console.warn('please update your wizard markup to include ".steps-container" as seen in http://getfuelux.com/javascript.html#wizard-usage-markup'); } } kids = this.$nextBtn.children().detach(); this.nextText = $.trim(this.$nextBtn.text()); this.$nextBtn.append(kids); // handle events this.$prevBtn.on('click.fu.wizard', $.proxy(this.previous, this)); this.$nextBtn.on('click.fu.wizard', $.proxy(this.next, this)); this.$element.on('click.fu.wizard', 'li.complete', $.proxy(this.stepclicked, this)); this.selectedItem(this.options.selectedItem); if (this.options.disablePreviousStep) { this.$prevBtn.attr('disabled', true); this.$element.find('.steps').addClass('previous-disabled'); } }; Wizard.prototype = { constructor: Wizard, destroy: function () { this.$element.remove(); // any external bindings [none] // empty elements to return to original markup [none] // returns string of markup return this.$element[0].outerHTML; }, //index is 1 based //second parameter can be array of objects [{ ... }, { ... }] or you can pass n additional objects as args //object structure is as follows (all params are optional): { badge: '', label: '', pane: '' } addSteps: function (index) { var items = [].slice.call(arguments).slice(1); var $steps = this.$element.find('.steps'); var $stepContent = this.$element.find('.step-content'); var i, l, $pane, $startPane, $startStep, $step; index = (index === -1 || (index > (this.numSteps + 1))) ? this.numSteps + 1 : index; if (items[0] instanceof Array) { items = items[0]; } $startStep = $steps.find('li:nth-child(' + index + ')'); $startPane = $stepContent.find('.step-pane:nth-child(' + index + ')'); if ($startStep.length < 1) { $startStep = null; } for (i = 0, l = items.length; i < l; i++) { $step = $('
  • '); $step.append(items[i].label || '').append(''); $step.find('.badge').append(items[i].badge || index); $pane = $('
    '); $pane.append(items[i].pane || ''); if (!$startStep) { $steps.append($step); $stepContent.append($pane); } else { $startStep.before($step); $startPane.before($pane); } index++; } this.syncSteps(); this.numSteps = $steps.find('li').length; this.setState(); }, //index is 1 based, howMany is number to remove removeSteps: function (index, howMany) { var action = 'nextAll'; var i = 0; var $steps = this.$element.find('.steps'); var $stepContent = this.$element.find('.step-content'); var $start; howMany = (howMany !== undefined) ? howMany : 1; if (index > $steps.find('li').length) { $start = $steps.find('li:last'); } else { $start = $steps.find('li:nth-child(' + index + ')').prev(); if ($start.length < 1) { action = 'children'; $start = $steps; } } $start[action]().each(function () { var item = $(this); var step = item.attr('data-step'); if (i < howMany) { item.remove(); $stepContent.find('.step-pane[data-step="' + step + '"]:first').remove(); } else { return false; } i++; }); this.syncSteps(); this.numSteps = $steps.find('li').length; this.setState(); }, setState: function () { var canMovePrev = (this.currentStep > 1);//remember, steps index is 1 based... var isFirstStep = (this.currentStep === 1); var isLastStep = (this.currentStep === this.numSteps); // disable buttons based on current step if (!this.options.disablePreviousStep) { this.$prevBtn.attr('disabled', (isFirstStep === true || canMovePrev === false)); } // change button text of last step, if specified var last = this.$nextBtn.attr('data-last'); if (last) { this.lastText = last; // replace text var text = this.nextText; if (isLastStep === true) { text = this.lastText; // add status class to wizard this.$element.addClass('complete'); } else { this.$element.removeClass('complete'); } var kids = this.$nextBtn.children().detach(); this.$nextBtn.text(text).append(kids); } // reset classes for all steps var $steps = this.$element.find('.steps li'); $steps.removeClass('active').removeClass('complete'); $steps.find('span.badge').removeClass('badge-info').removeClass('badge-success'); // set class for all previous steps var prevSelector = '.steps li:lt(' + (this.currentStep - 1) + ')'; var $prevSteps = this.$element.find(prevSelector); $prevSteps.addClass('complete'); $prevSteps.find('span.badge').addClass('badge-success'); // set class for current step var currentSelector = '.steps li:eq(' + (this.currentStep - 1) + ')'; var $currentStep = this.$element.find(currentSelector); $currentStep.addClass('active'); $currentStep.find('span.badge').addClass('badge-info'); // set display of target element var $stepContent = this.$element.find('.step-content'); var target = $currentStep.attr('data-step'); $stepContent.find('.step-pane').removeClass('active'); $stepContent.find('.step-pane[data-step="' + target + '"]:first').addClass('active'); //ACE /** // reset the wizard position to the left this.$element.find('.steps').first().attr('style', 'margin-left: 0'); // check if the steps are wider than the container div var totalWidth = 0; this.$element.find('.steps > li').each(function () { totalWidth += $(this).outerWidth(); }); var containerWidth = 0; if (this.$element.find('.actions').length) { containerWidth = this.$element.width() - this.$element.find('.actions').first().outerWidth(); } else { containerWidth = this.$element.width(); } if (totalWidth > containerWidth) { // set the position so that the last step is on the right var newMargin = totalWidth - containerWidth; this.$element.find('.steps').first().attr('style', 'margin-left: -' + newMargin + 'px'); // set the position so that the active step is in a good // position if it has been moved out of view if (this.$element.find('li.active').first().position().left < 200) { newMargin += this.$element.find('li.active').first().position().left - 200; if (newMargin < 1) { this.$element.find('.steps').first().attr('style', 'margin-left: 0'); } else { this.$element.find('.steps').first().attr('style', 'margin-left: -' + newMargin + 'px'); } } } */ // only fire changed event after initializing if (typeof (this.initialized) !== 'undefined') { var e = $.Event('changed.fu.wizard'); this.$element.trigger(e, { step: this.currentStep }); } this.initialized = true; }, stepclicked: function (e) { var li = $(e.currentTarget); var index = this.$element.find('.steps li').index(li); if (index < this.currentStep && this.options.disablePreviousStep) {//enforce restrictions return; } else { var evt = $.Event('stepclicked.fu.wizard'); this.$element.trigger(evt, { step: index + 1 }); if (evt.isDefaultPrevented()) { return; } this.currentStep = (index + 1); this.setState(); } }, syncSteps: function () { var i = 1; var $steps = this.$element.find('.steps'); var $stepContent = this.$element.find('.step-content'); $steps.children().each(function () { var item = $(this); var badge = item.find('.badge'); var step = item.attr('data-step'); if (!isNaN(parseInt(badge.html(), 10))) { badge.html(i); } item.attr('data-step', i); $stepContent.find('.step-pane[data-step="' + step + '"]:last').attr('data-step', i); i++; }); }, previous: function () { if (this.options.disablePreviousStep || this.currentStep === 1) { return; } var e = $.Event('actionclicked.fu.wizard'); this.$element.trigger(e, { step: this.currentStep, direction: 'previous' }); if (e.isDefaultPrevented()) { return; }// don't increment ...what? Why? this.currentStep -= 1; this.setState(); // only set focus if focus is still on the $nextBtn (avoid stomping on a focus set programmatically in actionclicked callback) if (this.$prevBtn.is(':focus')) { var firstFormField = this.$element.find('.active').find('input, select, textarea')[0]; if (typeof firstFormField !== 'undefined') { // allow user to start typing immediately instead of having to click on the form field. $(firstFormField).focus(); } else if (this.$element.find('.active input:first').length === 0 && this.$prevBtn.is(':disabled')) { //only set focus on a button as the last resort if no form fields exist and the just clicked button is now disabled this.$nextBtn.focus(); } } }, next: function () { var e = $.Event('actionclicked.fu.wizard'); this.$element.trigger(e, { step: this.currentStep, direction: 'next' }); if (e.isDefaultPrevented()) { return; }// respect preventDefault in case dev has attached validation to step and wants to stop propagation based on it. if (this.currentStep < this.numSteps) { this.currentStep += 1; this.setState(); } else {//is last step this.$element.trigger('finished.fu.wizard'); } // only set focus if focus is still on the $nextBtn (avoid stomping on a focus set programmatically in actionclicked callback) if (this.$nextBtn.is(':focus')) { var firstFormField = this.$element.find('.active').find('input, select, textarea')[0]; if (typeof firstFormField !== 'undefined') { // allow user to start typing immediately instead of having to click on the form field. $(firstFormField).focus(); } else if (this.$element.find('.active input:first').length === 0 && this.$nextBtn.is(':disabled')) { //only set focus on a button as the last resort if no form fields exist and the just clicked button is now disabled this.$prevBtn.focus(); } } }, selectedItem: function (selectedItem) { var retVal, step; if (selectedItem) { step = selectedItem.step || -1; //allow selection of step by data-name step = Number(this.$element.find('.steps li[data-name="' + step + '"]').first().attr('data-step')) || Number(step); if (1 <= step && step <= this.numSteps) { this.currentStep = step; this.setState(); } else { step = this.$element.find('.steps li.active:first').attr('data-step'); if (!isNaN(step)) { this.currentStep = parseInt(step, 10); this.setState(); } } retVal = this; } else { retVal = { step: this.currentStep }; if (this.$element.find('.steps li.active:first[data-name]').length) { retVal.stepname = this.$element.find('.steps li.active:first').attr('data-name'); } } return retVal; } }; // WIZARD PLUGIN DEFINITION $.fn.wizard = function (option) { var args = Array.prototype.slice.call(arguments, 1); var methodReturn; var $set = this.each(function () { var $this = $(this); var data = $this.data('fu.wizard'); var options = typeof option === 'object' && option; if (!data) { $this.data('fu.wizard', (data = new Wizard(this, options))); } if (typeof option === 'string') { methodReturn = data[option].apply(data, args); } }); return (methodReturn === undefined) ? $set : methodReturn; }; $.fn.wizard.defaults = { disablePreviousStep: false, selectedItem: { step: -1 }//-1 means it will attempt to look for "active" class in order to set the step }; $.fn.wizard.Constructor = Wizard; $.fn.wizard.noConflict = function () { $.fn.wizard = old; return this; }; // DATA-API $(document).on('mouseover.fu.wizard.data-api', '[data-initialize=wizard]', function (e) { var $control = $(e.target).closest('.wizard'); if (!$control.data('fu.wizard')) { $control.wizard($control.data()); } }); // Must be domReady for AMD compatibility $(function () { $('[data-initialize=wizard]').each(function () { var $this = $(this); if ($this.data('fu.wizard')) return; $this.wizard($this.data()); }); }); // -- BEGIN UMD WRAPPER AFTERWORD -- })); // -- END UMD WRAPPER AFTERWORD --