/*! * jQuery Steps v1.1.0 - 09/04/2014 * Copyright (c) 2014 Rafael Staib (http://www.jquery-steps.com) * Licensed under MIT http://www.opensource.org/licenses/MIT */ ;(function ($, undefined) { $.fn.extend({ _aria: function (name, value) { return this.attr("aria-" + name, value); }, _removeAria: function (name) { return this.removeAttr("aria-" + name); }, _enableAria: function (enable) { return (enable == null || enable) ? this.removeClass("disabled")._aria("disabled", "false") : this.addClass("disabled")._aria("disabled", "true"); }, _showAria: function (show) { return (show == null || show) ? this.show()._aria("hidden", "false") : this.hide()._aria("hidden", "true"); }, _selectAria: function (select) { return (select == null || select) ? this.addClass("current")._aria("selected", "true") : this.removeClass("current")._aria("selected", "false"); }, _id: function (id) { return (id) ? this.attr("id", id) : this.attr("id"); } }); if (!String.prototype.format) { String.prototype.format = function() { var args = (arguments.length === 1 && $.isArray(arguments[0])) ? arguments[0] : arguments; var formattedString = this; for (var i = 0; i < args.length; i++) { var pattern = new RegExp("\\{" + i + "\\}", "gm"); formattedString = formattedString.replace(pattern, args[i]); } return formattedString; }; } /** * A global unique id count. * * @static * @private * @property _uniqueId * @type Integer **/ var _uniqueId = 0; /** * The plugin prefix for cookies. * * @final * @private * @property _cookiePrefix * @type String **/ var _cookiePrefix = "jQu3ry_5teps_St@te_"; /** * Suffix for the unique tab id. * * @final * @private * @property _tabSuffix * @type String * @since 0.9.7 **/ var _tabSuffix = "-t-"; /** * Suffix for the unique tabpanel id. * * @final * @private * @property _tabpanelSuffix * @type String * @since 0.9.7 **/ var _tabpanelSuffix = "-p-"; /** * Suffix for the unique title id. * * @final * @private * @property _titleSuffix * @type String * @since 0.9.7 **/ var _titleSuffix = "-h-"; /** * An error message for an "index out of range" error. * * @final * @private * @property _indexOutOfRangeErrorMessage * @type String **/ var _indexOutOfRangeErrorMessage = "Index out of range."; /** * An error message for an "missing corresponding element" error. * * @final * @private * @property _missingCorrespondingElementErrorMessage * @type String **/ var _missingCorrespondingElementErrorMessage = "One or more corresponding step {0} are missing."; /** * Adds a step to the cache. * * @static * @private * @method addStepToCache * @param wizard {Object} A jQuery wizard object * @param step {Object} The step object to add **/ function addStepToCache(wizard, step) { getSteps(wizard).push(step); } function analyzeData(wizard, options, state) { var stepTitles = wizard.children(options.headerTag), stepContents = wizard.children(options.bodyTag); // Validate content if (stepTitles.length > stepContents.length) { throwError(_missingCorrespondingElementErrorMessage, "contents"); } else if (stepTitles.length < stepContents.length) { throwError(_missingCorrespondingElementErrorMessage, "titles"); } var startIndex = options.startIndex; state.stepCount = stepTitles.length; // Tries to load the saved state (step position) if (options.saveState && $.cookie) { var savedState = $.cookie(_cookiePrefix + getUniqueId(wizard)); // Sets the saved position to the start index if not undefined or out of range var savedIndex = parseInt(savedState, 0); if (!isNaN(savedIndex) && savedIndex < state.stepCount) { startIndex = savedIndex; } } state.currentIndex = startIndex; stepTitles.each(function (index) { var item = $(this), // item == header content = stepContents.eq(index), modeData = content.data("mode"), mode = (modeData == null) ? contentMode.html : getValidEnumValue(contentMode, (/^\s*$/.test(modeData) || isNaN(modeData)) ? modeData : parseInt(modeData, 0)), contentUrl = (mode === contentMode.html || content.data("url") === undefined) ? "" : content.data("url"), contentLoaded = (mode !== contentMode.html && content.data("loaded") === "1"), step = $.extend({}, stepModel, { title: item.html(), content: (mode === contentMode.html) ? content.html() : "", contentUrl: contentUrl, contentMode: mode, contentLoaded: contentLoaded }); addStepToCache(wizard, step); }); } /** * Triggers the onCanceled event. * * @static * @private * @method cancel * @param wizard {Object} The jQuery wizard object **/ function cancel(wizard) { wizard.triggerHandler("canceled"); } function decreaseCurrentIndexBy(state, decreaseBy) { return state.currentIndex - decreaseBy; } /** * Removes the control functionality completely and transforms the current state to the initial HTML structure. * * @static * @private * @method destroy * @param wizard {Object} A jQuery wizard object **/ function destroy(wizard, options) { var eventNamespace = getEventNamespace(wizard); // Remove virtual data objects from the wizard wizard.unbind(eventNamespace).removeData("uid").removeData("options") .removeData("state").removeData("steps").removeData("eventNamespace") .find(".actions a").unbind(eventNamespace); // Remove attributes and CSS classes from the wizard wizard.removeClass(options.clearFixCssClass + " vertical"); var contents = wizard.find(".content > *"); // Remove virtual data objects from panels and their titles contents.removeData("loaded").removeData("mode").removeData("url"); // Remove attributes, CSS classes and reset inline styles on all panels and their titles contents.removeAttr("id").removeAttr("role").removeAttr("tabindex") .removeAttr("class").removeAttr("style")._removeAria("labelledby") ._removeAria("hidden"); // Empty panels if the mode is set to 'async' or 'iframe' wizard.find(".content > [data-mode='async'],.content > [data-mode='iframe']").empty(); var wizardSubstitute = $("<{0} class=\"{1}\">{0}>".format(wizard.get(0).tagName, wizard.attr("class"))); var wizardId = wizard._id(); if (wizardId != null && wizardId !== "") { wizardSubstitute._id(wizardId); } wizardSubstitute.html(wizard.find(".content").html()); wizard.after(wizardSubstitute); wizard.remove(); return wizardSubstitute; } /** * Triggers the onFinishing and onFinished event. * * @static * @private * @method finishStep * @param wizard {Object} The jQuery wizard object * @param state {Object} The state container of the current wizard **/ function finishStep(wizard, state) { var currentStep = wizard.find(".steps li").eq(state.currentIndex); if (wizard.triggerHandler("finishing", [state.currentIndex])) { currentStep.addClass("done").removeClass("error"); wizard.triggerHandler("finished", [state.currentIndex]); } else { currentStep.addClass("error"); } } /** * Gets or creates if not exist an unique event namespace for the given wizard instance. * * @static * @private * @method getEventNamespace * @param wizard {Object} A jQuery wizard object * @return {String} Returns the unique event namespace for the given wizard */ function getEventNamespace(wizard) { var eventNamespace = wizard.data("eventNamespace"); if (eventNamespace == null) { eventNamespace = "." + getUniqueId(wizard); wizard.data("eventNamespace", eventNamespace); } return eventNamespace; } function getStepAnchor(wizard, index) { var uniqueId = getUniqueId(wizard); return wizard.find("#" + uniqueId + _tabSuffix + index); } function getStepPanel(wizard, index) { var uniqueId = getUniqueId(wizard); return wizard.find("#" + uniqueId + _tabpanelSuffix + index); } function getStepTitle(wizard, index) { var uniqueId = getUniqueId(wizard); return wizard.find("#" + uniqueId + _titleSuffix + index); } function getOptions(wizard) { return wizard.data("options"); } function getState(wizard) { return wizard.data("state"); } function getSteps(wizard) { return wizard.data("steps"); } /** * Gets a specific step object by index. * * @static * @private * @method getStep * @param index {Integer} An integer that belongs to the position of a step * @return {Object} A specific step object **/ function getStep(wizard, index) { var steps = getSteps(wizard); if (index < 0 || index >= steps.length) { throwError(_indexOutOfRangeErrorMessage); } return steps[index]; } /** * Gets or creates if not exist an unique id from the given wizard instance. * * @static * @private * @method getUniqueId * @param wizard {Object} A jQuery wizard object * @return {String} Returns the unique id for the given wizard */ function getUniqueId(wizard) { var uniqueId = wizard.data("uid"); if (uniqueId == null) { uniqueId = wizard._id(); if (uniqueId == null) { uniqueId = "steps-uid-".concat(_uniqueId); wizard._id(uniqueId); } _uniqueId++; wizard.data("uid", uniqueId); } return uniqueId; } /** * Gets a valid enum value by checking a specific enum key or value. * * @static * @private * @method getValidEnumValue * @param enumType {Object} Type of enum * @param keyOrValue {Object} Key as `String` or value as `Integer` to check for */ function getValidEnumValue(enumType, keyOrValue) { validateArgument("enumType", enumType); validateArgument("keyOrValue", keyOrValue); // Is key if (typeof keyOrValue === "string") { var value = enumType[keyOrValue]; if (value === undefined) { throwError("The enum key '{0}' does not exist.", keyOrValue); } return value; } // Is value else if (typeof keyOrValue === "number") { for (var key in enumType) { if (enumType[key] === keyOrValue) { return keyOrValue; } } throwError("Invalid enum value '{0}'.", keyOrValue); } // Type is not supported else { throwError("Invalid key or value type."); } } /** * Routes to the next step. * * @static * @private * @method goToNextStep * @param wizard {Object} The jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @return {Boolean} Indicates whether the action executed **/ function goToNextStep(wizard, options, state) { return paginationClick(wizard, options, state, increaseCurrentIndexBy(state, 1)); } /** * Routes to the previous step. * * @static * @private * @method goToPreviousStep * @param wizard {Object} The jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @return {Boolean} Indicates whether the action executed **/ function goToPreviousStep(wizard, options, state) { return paginationClick(wizard, options, state, decreaseCurrentIndexBy(state, 1)); } /** * Routes to a specific step by a given index. * * @static * @private * @method goToStep * @param wizard {Object} The jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @param index {Integer} The position (zero-based) to route to * @return {Boolean} Indicates whether the action succeeded or failed **/ function goToStep(wizard, options, state, index) { if (index < 0 || index >= state.stepCount) { throwError(_indexOutOfRangeErrorMessage); } if (options.forceMoveForward && index < state.currentIndex) { return; } var oldIndex = state.currentIndex; if (wizard.triggerHandler("stepChanging", [state.currentIndex, index])) { // Save new state state.currentIndex = index; saveCurrentStateToCookie(wizard, options, state); // Change visualisation refreshStepNavigation(wizard, options, state, oldIndex); refreshPagination(wizard, options, state); loadAsyncContent(wizard, options, state); startTransitionEffect(wizard, options, state, index, oldIndex, function() { wizard.triggerHandler("stepChanged", [index, oldIndex]); }); } else { wizard.find(".steps li").eq(oldIndex).addClass("error"); } return true; } function increaseCurrentIndexBy(state, increaseBy) { return state.currentIndex + increaseBy; } /** * Initializes the component. * * @static * @private * @method initialize * @param options {Object} The component settings **/ function initialize(options) { /*jshint -W040 */ var opts = $.extend(true, {}, defaults, options); return this.each(function () { var wizard = $(this); var state = { currentIndex: opts.startIndex, currentStep: null, stepCount: 0, transitionElement: null }; // Create data container wizard.data("options", opts); wizard.data("state", state); wizard.data("steps", []); analyzeData(wizard, opts, state); render(wizard, opts, state); registerEvents(wizard, opts); // Trigger focus if (opts.autoFocus && _uniqueId === 0) { getStepAnchor(wizard, opts.startIndex).focus(); } wizard.triggerHandler("init", [opts.startIndex]); }); } /** * Inserts a new step to a specific position. * * @static * @private * @method insertStep * @param wizard {Object} The jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @param index {Integer} The position (zero-based) to add * @param step {Object} The step object to add * @example * $("#wizard").steps().insert(0, { * title: "Title", * content: "", // optional * contentMode: "async", // optional * contentUrl: "/Content/Step/1" // optional * }); * @chainable **/ function insertStep(wizard, options, state, index, step) { if (index < 0 || index > state.stepCount) { throwError(_indexOutOfRangeErrorMessage); } // TODO: Validate step object // Change data step = $.extend({}, stepModel, step); insertStepToCache(wizard, index, step); if (state.currentIndex !== state.stepCount && state.currentIndex >= index) { state.currentIndex++; saveCurrentStateToCookie(wizard, options, state); } state.stepCount++; var contentContainer = wizard.find(".content"), header = $("<{0}>{1}{0}>".format(options.headerTag, step.title)), body = $("<{0}>{0}>".format(options.bodyTag)); if (step.contentMode == null || step.contentMode === contentMode.html) { body.html(step.content); } if (index === 0) { contentContainer.prepend(body).prepend(header); } else { getStepPanel(wizard, (index - 1)).after(body).after(header); } renderBody(wizard, state, body, index); renderTitle(wizard, options, state, header, index); refreshSteps(wizard, options, state, index); if (index === state.currentIndex) { refreshStepNavigation(wizard, options, state); } refreshPagination(wizard, options, state); return wizard; } /** * Inserts a step object to the cache at a specific position. * * @static * @private * @method insertStepToCache * @param wizard {Object} A jQuery wizard object * @param index {Integer} The position (zero-based) to add * @param step {Object} The step object to add **/ function insertStepToCache(wizard, index, step) { getSteps(wizard).splice(index, 0, step); } /** * Handles the keyup DOM event for pagination. * * @static * @private * @event keyup * @param event {Object} An event object */ function keyUpHandler(event) { var wizard = $(this), options = getOptions(wizard), state = getState(wizard); if (options.suppressPaginationOnFocus && wizard.find(":focus").is(":input")) { event.preventDefault(); return false; } var keyCodes = { left: 37, right: 39 }; if (event.keyCode === keyCodes.left) { event.preventDefault(); goToPreviousStep(wizard, options, state); } else if (event.keyCode === keyCodes.right) { event.preventDefault(); goToNextStep(wizard, options, state); } } /** * Loads and includes async content. * * @static * @private * @method loadAsyncContent * @param wizard {Object} A jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard */ function loadAsyncContent(wizard, options, state) { if (state.stepCount > 0) { var currentIndex = state.currentIndex, currentStep = getStep(wizard, currentIndex); if (!options.enableContentCache || !currentStep.contentLoaded) { switch (getValidEnumValue(contentMode, currentStep.contentMode)) { case contentMode.iframe: wizard.find(".content > .body").eq(state.currentIndex).empty() .html("") .data("loaded", "1"); break; case contentMode.async: var currentStepContent = getStepPanel(wizard, currentIndex)._aria("busy", "true") .empty().append(renderTemplate(options.loadingTemplate, { text: options.labels.loading })); $.ajax({ url: currentStep.contentUrl, cache: false }).done(function (data) { currentStepContent.empty().html(data)._aria("busy", "false").data("loaded", "1"); wizard.triggerHandler("contentLoaded", [currentIndex]); }); break; } } } } /** * Fires the action next or previous click event. * * @static * @private * @method paginationClick * @param wizard {Object} The jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @param index {Integer} The position (zero-based) to route to * @return {Boolean} Indicates whether the event fired successfully or not **/ function paginationClick(wizard, options, state, index) { var oldIndex = state.currentIndex; if (index >= 0 && index < state.stepCount && !(options.forceMoveForward && index < state.currentIndex)) { var anchor = getStepAnchor(wizard, index), parent = anchor.parent(), isDisabled = parent.hasClass("disabled"); // Enable the step to make the anchor clickable! parent._enableAria(); anchor.click(); // An error occured if (oldIndex === state.currentIndex && isDisabled) { // Disable the step again if current index has not changed; prevents click action. parent._enableAria(false); return false; } return true; } return false; } /** * Fires when a pagination click happens. * * @static * @private * @event click * @param event {Object} An event object */ function paginationClickHandler(event) { event.preventDefault(); var anchor = $(this), wizard = anchor.parent().parent().parent().parent(), options = getOptions(wizard), state = getState(wizard), href = anchor.attr("href"); switch (href.substring(href.lastIndexOf("#") + 1)) { case "cancel": cancel(wizard); break; case "finish": finishStep(wizard, state); break; case "next": goToNextStep(wizard, options, state); break; case "previous": goToPreviousStep(wizard, options, state); break; } } /** * Refreshs the visualization state for the entire pagination. * * @static * @private * @method refreshPagination * @param wizard {Object} A jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard */ function refreshPagination(wizard, options, state) { if (options.enablePagination) { var finish = wizard.find(".actions a[href$='#finish']").parent(), next = wizard.find(".actions a[href$='#next']").parent(); if (!options.forceMoveForward) { var previous = wizard.find(".actions a[href$='#previous']").parent(); previous._enableAria(state.currentIndex > 0); } if (options.enableFinishButton && options.showFinishButtonAlways) { finish._enableAria(state.stepCount > 0); next._enableAria(state.stepCount > 1 && state.stepCount > (state.currentIndex + 1)); } else { finish._showAria(options.enableFinishButton && state.stepCount === (state.currentIndex + 1)); next._showAria(state.stepCount === 0 || state.stepCount > (state.currentIndex + 1)). _enableAria(state.stepCount > (state.currentIndex + 1) || !options.enableFinishButton); } } } /** * Refreshs the visualization state for the step navigation (tabs). * * @static * @private * @method refreshStepNavigation * @param wizard {Object} A jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @param [oldIndex] {Integer} The index of the prior step */ function refreshStepNavigation(wizard, options, state, oldIndex) { var currentOrNewStepAnchor = getStepAnchor(wizard, state.currentIndex), currentInfo = $("" + options.labels.current + " "), stepTitles = wizard.find(".content > .title"); if (oldIndex != null) { var oldStepAnchor = getStepAnchor(wizard, oldIndex); oldStepAnchor.parent().addClass("done").removeClass("error")._selectAria(false); stepTitles.eq(oldIndex).removeClass("current").next(".body").removeClass("current"); currentInfo = oldStepAnchor.find(".current-info"); currentOrNewStepAnchor.focus(); } currentOrNewStepAnchor.prepend(currentInfo).parent()._selectAria().removeClass("done")._enableAria(); stepTitles.eq(state.currentIndex).addClass("current").next(".body").addClass("current"); } /** * Refreshes step buttons and their related titles beyond a certain position. * * @static * @private * @method refreshSteps * @param wizard {Object} A jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @param index {Integer} The start point for refreshing ids */ function refreshSteps(wizard, options, state, index) { var uniqueId = getUniqueId(wizard); for (var i = index; i < state.stepCount; i++) { var uniqueStepId = uniqueId + _tabSuffix + i, uniqueBodyId = uniqueId + _tabpanelSuffix + i, uniqueHeaderId = uniqueId + _titleSuffix + i, title = wizard.find(".title").eq(i)._id(uniqueHeaderId); wizard.find(".steps a").eq(i)._id(uniqueStepId) ._aria("controls", uniqueBodyId).attr("href", "#" + uniqueHeaderId) .html(renderTemplate(options.titleTemplate, { index: i + 1, title: title.html() })); wizard.find(".body").eq(i)._id(uniqueBodyId) ._aria("labelledby", uniqueHeaderId); } } function registerEvents(wizard, options) { var eventNamespace = getEventNamespace(wizard); wizard.bind("canceled" + eventNamespace, options.onCanceled); wizard.bind("contentLoaded" + eventNamespace, options.onContentLoaded); wizard.bind("finishing" + eventNamespace, options.onFinishing); wizard.bind("finished" + eventNamespace, options.onFinished); wizard.bind("init" + eventNamespace, options.onInit); wizard.bind("stepChanging" + eventNamespace, options.onStepChanging); wizard.bind("stepChanged" + eventNamespace, options.onStepChanged); if (options.enableKeyNavigation) { wizard.bind("keyup" + eventNamespace, keyUpHandler); } wizard.find(".actions a").bind("click" + eventNamespace, paginationClickHandler); } /** * Removes a specific step by an given index. * * @static * @private * @method removeStep * @param wizard {Object} A jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard * @param index {Integer} The position (zero-based) of the step to remove * @return Indecates whether the item is removed. **/ function removeStep(wizard, options, state, index) { // Index out of range and try deleting current item will return false. if (index < 0 || index >= state.stepCount || state.currentIndex === index) { return false; } // Change data removeStepFromCache(wizard, index); if (state.currentIndex > index) { state.currentIndex--; saveCurrentStateToCookie(wizard, options, state); } state.stepCount--; getStepTitle(wizard, index).remove(); getStepPanel(wizard, index).remove(); getStepAnchor(wizard, index).parent().remove(); // Set the "first" class to the new first step button if (index === 0) { wizard.find(".steps li").first().addClass("first"); } // Set the "last" class to the new last step button if (index === state.stepCount) { wizard.find(".steps li").eq(index).addClass("last"); } refreshSteps(wizard, options, state, index); refreshPagination(wizard, options, state); return true; } function removeStepFromCache(wizard, index) { getSteps(wizard).splice(index, 1); } /** * Transforms the base html structure to a more sensible html structure. * * @static * @private * @method render * @param wizard {Object} A jQuery wizard object * @param options {Object} Settings of the current wizard * @param state {Object} The state container of the current wizard **/ function render(wizard, options, state) { // Create a content wrapper and copy HTML from the intial wizard structure var wrapperTemplate = "<{0} class=\"{1}\">{2}{0}>", orientation = getValidEnumValue(stepsOrientation, options.stepsOrientation), verticalCssClass = (orientation === stepsOrientation.vertical) ? " vertical" : "", contentWrapper = $(wrapperTemplate.format(options.contentContainerTag, "content " + options.clearFixCssClass, wizard.html())), stepsWrapper = $(wrapperTemplate.format(options.stepsContainerTag, "steps " + options.clearFixCssClass, "