2015-08-06 01:01:21 +02:00
|
|
|
/**
|
|
|
|
* @file Manages the multi-step navigation.
|
|
|
|
*/
|
|
|
|
jQuery(function ($) {
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
// A reference to the UserForm instance.
|
|
|
|
var userform = null;
|
2015-08-11 01:40:37 +02:00
|
|
|
|
2015-08-06 07:47:14 +02:00
|
|
|
// Settings that come from the CMS.
|
2015-08-13 01:55:39 +02:00
|
|
|
var CONSTANTS = {};
|
2015-08-10 03:40:32 +02:00
|
|
|
|
|
|
|
// Common functions that extend multiple classes.
|
|
|
|
var commonMixin = {
|
|
|
|
/**
|
|
|
|
* @func show
|
|
|
|
* @desc Show the form step. Looks after aria attributes too.
|
|
|
|
*/
|
|
|
|
show: function () {
|
|
|
|
this.$el.attr('aria-hidden', false).show();
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* @func hide
|
|
|
|
* @desc Hide the form step. Looks after aria attributes too.
|
|
|
|
*/
|
|
|
|
hide: function () {
|
|
|
|
this.$el.attr('aria-hidden', true).hide();
|
|
|
|
}
|
2015-08-06 07:47:14 +02:00
|
|
|
};
|
|
|
|
|
2012-04-26 02:14:27 +02:00
|
|
|
/**
|
2015-08-06 01:01:21 +02:00
|
|
|
* @func UserForm
|
|
|
|
* @constructor
|
2015-08-10 03:40:32 +02:00
|
|
|
* @param {object} element
|
|
|
|
* @return {object} - The UserForm instance.
|
2015-08-06 01:01:21 +02:00
|
|
|
* @desc The form
|
2012-04-26 02:14:27 +02:00
|
|
|
*/
|
2015-08-06 01:01:21 +02:00
|
|
|
function UserForm(element) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
this.$el = element instanceof jQuery ? element : $(element);
|
|
|
|
this.steps = [];
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
// Add an error container which displays a list of invalid steps on form submission.
|
|
|
|
this.errorContainer = new ErrorContainer(this.$el.children('.error-container'));
|
|
|
|
|
2015-08-26 07:19:29 +02:00
|
|
|
// Listen for events triggered by form steps.
|
2015-08-17 00:43:51 +02:00
|
|
|
this.$el.on('userform.action.prev', function (e) {
|
2015-08-06 01:01:21 +02:00
|
|
|
self.prevStep();
|
|
|
|
});
|
2015-08-17 00:43:51 +02:00
|
|
|
this.$el.on('userform.action.next', function (e) {
|
2015-08-06 01:01:21 +02:00
|
|
|
self.nextStep();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Listen for events triggered by the progress bar.
|
2015-08-06 03:40:09 +02:00
|
|
|
$('#userform-progress').on('userform.progress.changestep', function (e, stepNumber) {
|
2015-08-06 01:01:21 +02:00
|
|
|
self.jumpToStep(stepNumber - 1);
|
|
|
|
});
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
// When a field becomes valid, remove errors from the error container.
|
|
|
|
this.$el.on('userform.form.valid', function (e, fieldId) {
|
|
|
|
self.errorContainer.removeStepLink(fieldId);
|
|
|
|
});
|
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
this.$el.validate(this.validationOptions);
|
|
|
|
|
2015-08-26 07:19:29 +02:00
|
|
|
// Ensure checkbox groups are validated correctly
|
|
|
|
$('.optionset.requiredField input').each(function() {
|
|
|
|
$(this).rules('add', {
|
|
|
|
required: true
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
/*
|
|
|
|
* Default options for step validation. These get extended in main().
|
|
|
|
*/
|
|
|
|
UserForm.prototype.validationOptions = {
|
|
|
|
ignore: ':hidden',
|
2015-08-13 23:57:06 +02:00
|
|
|
errorClass: 'error',
|
2015-08-10 05:18:50 +02:00
|
|
|
errorElement: 'span',
|
|
|
|
errorPlacement: function (error, element) {
|
|
|
|
error.addClass('message');
|
|
|
|
|
2015-08-26 07:19:29 +02:00
|
|
|
if (element.is(':radio') || element.parents('.checkboxset').length > 0) {
|
2015-08-10 05:18:50 +02:00
|
|
|
error.insertAfter(element.closest('ul'));
|
2015-08-26 07:19:29 +02:00
|
|
|
} else if (element.parents('.checkbox').length > 0) {
|
|
|
|
error.insertAfter(element.next('label'));
|
2015-08-10 05:18:50 +02:00
|
|
|
} else {
|
|
|
|
error.insertAfter(element);
|
|
|
|
}
|
|
|
|
},
|
2015-08-26 07:19:29 +02:00
|
|
|
invalidHandler: function (event, validator) {
|
|
|
|
//setTimeout 0 so it runs after errorPlacement
|
|
|
|
setTimeout(function () {
|
|
|
|
validator.currentElements.filter('.error').first().focus();
|
|
|
|
}, 0);
|
|
|
|
},
|
2015-08-13 01:55:39 +02:00
|
|
|
// Callback for handling the actual submit when the form is valid.
|
|
|
|
// Submission in the jQuery.validate sence is handled at step level.
|
|
|
|
// So when the final step is submitted we have to also check all previous steps are valid.
|
|
|
|
submitHandler: function (form, e) {
|
|
|
|
var isValid = true;
|
|
|
|
|
2015-08-17 00:43:51 +02:00
|
|
|
// validate the current step
|
|
|
|
if(userform.currentStep) {
|
|
|
|
userform.currentStep.valid = $(form).valid();
|
|
|
|
}
|
2015-08-13 01:55:39 +02:00
|
|
|
|
|
|
|
// Check for invalid previous steps.
|
|
|
|
$.each(userform.steps, function (i, step) {
|
2015-08-17 00:43:51 +02:00
|
|
|
if (!step.valid && !step.conditionallyHidden()) {
|
2015-08-13 01:55:39 +02:00
|
|
|
isValid = false;
|
|
|
|
userform.errorContainer.addStepLink(step);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (isValid) {
|
|
|
|
form.submit();
|
|
|
|
} else {
|
|
|
|
userform.errorContainer.show();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// When a field becomes valid.
|
2015-08-10 05:18:50 +02:00
|
|
|
success: function (error) {
|
2015-08-26 07:19:29 +02:00
|
|
|
var errorId = $(error).attr('id'),
|
|
|
|
fieldId = errorId.substr(0, errorId.indexOf('-error')),
|
|
|
|
isCheckboxGroup = $(error).closest('.requiredField').hasClass('checkboxset');
|
|
|
|
|
|
|
|
// We need to escapse the field id if it's a checkboxfield
|
|
|
|
// because jQuery breaks when using selector that end with
|
|
|
|
// square brackets.
|
|
|
|
if (isCheckboxGroup) {
|
|
|
|
fieldId = fieldId.replace('[]', '\\\\[\\\\]');
|
|
|
|
}
|
2015-08-10 05:18:50 +02:00
|
|
|
|
|
|
|
error.remove();
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
// Pass the field's ID with the event.
|
2015-08-26 07:19:29 +02:00
|
|
|
userform.$el.trigger('userform.form.valid', [fieldId]);
|
2015-08-10 05:18:50 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
/**
|
|
|
|
* @func UserForm.addStep
|
2015-08-10 03:40:32 +02:00
|
|
|
* @param {object} step - An instance of FormStep.
|
2015-08-06 01:01:21 +02:00
|
|
|
* @desc Adds a step to the UserForm.
|
|
|
|
*/
|
|
|
|
UserForm.prototype.addStep = function (step) {
|
|
|
|
// Make sure we're dealing with a form step.
|
|
|
|
if (!step instanceof FormStep) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
step.id = this.steps.length;
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
this.steps.push(step);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func UserForm.setCurrentStep
|
2015-08-10 03:40:32 +02:00
|
|
|
* @param {object} step - An instance of FormStep.
|
2015-08-06 01:01:21 +02:00
|
|
|
* @desc Sets the step the user is currently on.
|
|
|
|
*/
|
|
|
|
UserForm.prototype.setCurrentStep = function (step) {
|
|
|
|
// Make sure we're dealing with a form step.
|
2015-08-13 01:31:37 +02:00
|
|
|
if (!(step instanceof FormStep)) {
|
2015-08-06 01:01:21 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.currentStep = step;
|
|
|
|
this.currentStep.show();
|
2015-08-06 07:47:14 +02:00
|
|
|
|
|
|
|
// Record the user has viewed the step.
|
|
|
|
step.viewed = true;
|
|
|
|
step.$el.addClass('viewed');
|
2015-08-06 01:01:21 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func UserForm.jumpToStep
|
2015-08-10 03:40:32 +02:00
|
|
|
* @param {number} stepNumber
|
2015-08-17 00:43:51 +02:00
|
|
|
* @param {boolean} [direction] - Defaults to forward (true).
|
2015-08-06 01:01:21 +02:00
|
|
|
* @desc Jumps to a specific form step.
|
|
|
|
*/
|
2015-08-17 00:43:51 +02:00
|
|
|
UserForm.prototype.jumpToStep = function (stepNumber, direction) {
|
2015-08-13 01:55:39 +02:00
|
|
|
var targetStep = this.steps[stepNumber],
|
2015-08-17 00:43:51 +02:00
|
|
|
isValid = false,
|
|
|
|
forward = direction === void 0 ? true : direction;
|
2015-08-06 01:01:21 +02:00
|
|
|
|
|
|
|
// Make sure the target step exists.
|
|
|
|
if (targetStep === void 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-08-17 00:43:51 +02:00
|
|
|
// Make sure the step we're trying to set as current is not
|
|
|
|
// hidden by custom display rules. If it is then jump to the next step.
|
|
|
|
if (targetStep.conditionallyHidden()) {
|
|
|
|
if (forward) {
|
|
|
|
this.jumpToStep(stepNumber + 1);
|
|
|
|
} else {
|
|
|
|
this.jumpToStep(stepNumber - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
// Validate the form.
|
|
|
|
// This well effectivly validate the current step and not the entire form.
|
|
|
|
// This is because hidden fields are excluded from validation, and all fields
|
|
|
|
// on all other steps, are currently hidden.
|
|
|
|
isValid = this.$el.valid();
|
|
|
|
|
|
|
|
// Set the 'valid' property on the current step.
|
2015-08-17 00:43:51 +02:00
|
|
|
this.currentStep.valid = isValid;
|
2015-08-13 01:55:39 +02:00
|
|
|
|
|
|
|
// Users can navigate to step's they've already viewed even if the current step is invalid.
|
|
|
|
if (isValid === false && targetStep.viewed === false) {
|
2015-08-06 07:47:14 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
this.currentStep.hide();
|
|
|
|
this.setCurrentStep(targetStep);
|
2015-08-06 03:40:09 +02:00
|
|
|
|
2015-08-17 00:43:51 +02:00
|
|
|
this.$el.trigger('userform.form.changestep', [targetStep.id]);
|
2015-08-06 01:01:21 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func UserForm.nextStep
|
|
|
|
* @desc Advances the form to the next step.
|
|
|
|
*/
|
|
|
|
UserForm.prototype.nextStep = function () {
|
2015-08-17 00:43:51 +02:00
|
|
|
this.jumpToStep(this.steps.indexOf(this.currentStep) + 1, true);
|
2015-08-06 01:01:21 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func UserForm.prevStep
|
|
|
|
* @desc Goes back one step (not bound to browser history).
|
|
|
|
*/
|
|
|
|
UserForm.prototype.prevStep = function () {
|
2015-08-17 00:43:51 +02:00
|
|
|
this.jumpToStep(this.steps.indexOf(this.currentStep) - 1, false);
|
2015-08-06 01:01:21 +02:00
|
|
|
};
|
|
|
|
|
2015-08-10 03:40:32 +02:00
|
|
|
/**
|
|
|
|
* @func ErrorContainer
|
|
|
|
* @constructor
|
|
|
|
* @param {object} element - The error container element.
|
|
|
|
* @return {object} - The ErrorContainer instance.
|
|
|
|
* @desc Creates an error container. Used to display step error messages at the top.
|
|
|
|
*/
|
|
|
|
function ErrorContainer(element) {
|
|
|
|
this.$el = element instanceof jQuery ? element : $(element);
|
|
|
|
|
|
|
|
// Set the error container's heading.
|
|
|
|
this.$el.find('h4').text(ss.i18n._t('UserForms.ERROR_CONTAINER_HEADER', 'Please correct the following errors and try again:'));
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
/**
|
|
|
|
* @func hasErrors
|
|
|
|
* @return boolean
|
|
|
|
* @desc Checks if the error container has any error messages.
|
|
|
|
*/
|
|
|
|
ErrorContainer.prototype.hasErrors = function () {
|
|
|
|
return this.$el.find('.error-list').children().length > 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func removeErrorMessage
|
|
|
|
* @desc Removes an error message from the error container.
|
|
|
|
*/
|
|
|
|
ErrorContainer.prototype.removeErrorMessage = function (fieldId) {
|
|
|
|
this.$el.find('#' + fieldId + '-top-error').remove();
|
|
|
|
|
|
|
|
// If there are no more error then hide the container.
|
|
|
|
if (!this.hasErrors()) {
|
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
/**
|
|
|
|
* @func addStepLink
|
|
|
|
* @param {object} step - FormStep instance.
|
|
|
|
* @desc Adds a link to a form step as an error message.
|
|
|
|
*/
|
|
|
|
ErrorContainer.prototype.addStepLink = function (step) {
|
|
|
|
var self = this,
|
|
|
|
itemID = step.$el.attr('id') + '-error-link',
|
|
|
|
$itemElement = this.$el.find('#' + itemID),
|
|
|
|
stepID = step.$el.attr('id'),
|
|
|
|
stepTitle = step.$el.data('title');
|
|
|
|
|
|
|
|
// If the item already exists we don't need to do anything.
|
|
|
|
if ($itemElement.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$itemElement = $('<li id="' + itemID + '"><a href="#' + stepID + '">' + stepTitle + '</a></li>');
|
|
|
|
|
|
|
|
$itemElement.on('click', function (e) {
|
|
|
|
e.preventDefault();
|
|
|
|
userform.jumpToStep(step.id);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.$el.find('.error-list').append($itemElement);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func removeStepLink
|
|
|
|
* @param {object} step - FormStep instance.
|
|
|
|
* @desc Removes a step link from the error container.
|
|
|
|
*/
|
|
|
|
ErrorContainer.prototype.removeStepLink = function (fieldId) {
|
|
|
|
var stepID = $('#' + fieldId).closest('.form-step').attr('id');
|
|
|
|
|
|
|
|
this.$el.find('#' + stepID + '-error-link').remove();
|
|
|
|
|
|
|
|
// Hide the error container if we've just removed the last error.
|
|
|
|
if (this.$el.find('.error-list').is(':empty')) {
|
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-08-10 03:40:32 +02:00
|
|
|
/**
|
|
|
|
* @func ErrorContainer.updateErrorMessage
|
2015-08-10 05:18:50 +02:00
|
|
|
* @param {object} $input - The jQuery input object which contains the field to validate.
|
2015-08-10 03:40:32 +02:00
|
|
|
* @param {object} message - The error message to display (html escaped).
|
|
|
|
* @desc Update an error message (displayed at the top of the form).
|
|
|
|
*/
|
2015-08-10 05:18:50 +02:00
|
|
|
ErrorContainer.prototype.updateErrorMessage = function ($input, message) {
|
|
|
|
var inputID = $input.attr('id'),
|
2015-08-10 03:40:32 +02:00
|
|
|
anchor = '#' + inputID,
|
|
|
|
elementID = inputID + '-top-error',
|
|
|
|
messageElement = $('#' + elementID),
|
2015-08-10 05:18:50 +02:00
|
|
|
describedBy = $input.attr('aria-describedby');
|
2015-08-10 03:40:32 +02:00
|
|
|
|
|
|
|
// The 'message' param will be an empty string if the field is valid.
|
|
|
|
if (!message) {
|
|
|
|
// Style issues as fixed if they already exist
|
|
|
|
messageElement.addClass('fixed');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
messageElement.removeClass('fixed');
|
|
|
|
|
|
|
|
this.show();
|
|
|
|
|
|
|
|
if (messageElement.length === 1) {
|
|
|
|
// Update the existing error message.
|
|
|
|
messageElement.show().find('a').html(message);
|
|
|
|
} else {
|
|
|
|
// Generate better link to field
|
2015-08-10 05:18:50 +02:00
|
|
|
$input.closest('.field[id]').each(function(){
|
2015-08-10 03:40:32 +02:00
|
|
|
anchor = '#' + $(this).attr('id');
|
|
|
|
});
|
2015-08-10 05:18:50 +02:00
|
|
|
|
2015-08-10 03:40:32 +02:00
|
|
|
// Add a new error message
|
|
|
|
messageElement = $('<li><a></a></li>');
|
|
|
|
messageElement
|
|
|
|
.attr('id', elementID)
|
|
|
|
.find('a')
|
|
|
|
.attr('href', location.pathname + location.search + anchor)
|
|
|
|
.html(message);
|
|
|
|
|
|
|
|
this.$el.find('ul').append(messageElement);
|
2015-08-10 05:18:50 +02:00
|
|
|
|
2015-08-10 03:40:32 +02:00
|
|
|
// link back to original input via aria
|
|
|
|
// Respect existing non-error aria-describedby
|
2015-08-10 05:18:50 +02:00
|
|
|
if (!describedBy) {
|
2015-08-10 03:40:32 +02:00
|
|
|
describedBy = elementID;
|
2015-08-10 05:18:50 +02:00
|
|
|
} else if (!describedBy.match(new RegExp('\\b' + elementID + '\\b'))) {
|
2015-08-10 03:40:32 +02:00
|
|
|
// Add to end of list if not already present
|
|
|
|
describedBy += " " + elementID;
|
|
|
|
}
|
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
$input.attr('aria-describedby', describedBy);
|
2015-08-10 03:40:32 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
/**
|
|
|
|
* @func FormStep
|
|
|
|
* @constructor
|
2015-08-10 03:40:32 +02:00
|
|
|
* @param {object} element
|
|
|
|
* @return {object} - The FormStep instance.
|
2015-08-06 01:01:21 +02:00
|
|
|
* @desc Creates a form step.
|
|
|
|
*/
|
|
|
|
function FormStep(element) {
|
2015-08-06 03:40:09 +02:00
|
|
|
var self = this;
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
this.$el = element instanceof jQuery ? element : $(element);
|
2015-08-17 00:43:51 +02:00
|
|
|
|
|
|
|
// Find button for this step
|
|
|
|
this.$elButton = $(".step-button-wrapper[data-for='" + this.$el.prop('id') + "']");
|
|
|
|
|
2015-08-06 07:47:14 +02:00
|
|
|
// Has the step been viewed by the user?
|
|
|
|
this.viewed = false;
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
// Is the form step valid?
|
|
|
|
// This value is used on form submission, which fails, if any of the steps are invalid.
|
|
|
|
this.valid = false;
|
|
|
|
|
|
|
|
// The internal id of the step. Used for getting the step from the UserForm.steps array.
|
|
|
|
this.id = null;
|
|
|
|
|
2015-08-06 07:47:14 +02:00
|
|
|
this.hide();
|
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
|
|
|
|
this.errorContainer = new ErrorContainer(this.$el.find('.error-container'));
|
2015-08-06 07:47:14 +02:00
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
// Listen for errors on the UserForm.
|
2015-08-13 01:55:39 +02:00
|
|
|
userform.$el.on('userform.form.error', function (e, validator) {
|
2015-08-10 05:18:50 +02:00
|
|
|
// The step only cares about errors if it's currently visible.
|
|
|
|
if (!self.$el.is(':visible')) {
|
|
|
|
return;
|
|
|
|
}
|
2015-08-06 07:47:14 +02:00
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
// Add or update each error in the list.
|
|
|
|
$.each(validator.errorList, function (i, error) {
|
|
|
|
self.errorContainer.updateErrorMessage($(error.element), error.message);
|
|
|
|
});
|
|
|
|
});
|
2015-08-06 07:47:14 +02:00
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
// Listen for fields becoming valid
|
2015-08-13 01:55:39 +02:00
|
|
|
userform.$el.on('userform.form.valid', function (e, fieldId) {
|
2015-08-10 05:18:50 +02:00
|
|
|
self.errorContainer.removeErrorMessage(fieldId);
|
|
|
|
});
|
2015-08-06 07:47:14 +02:00
|
|
|
}
|
2015-08-10 05:18:50 +02:00
|
|
|
|
2015-08-17 00:43:51 +02:00
|
|
|
// Ensure that page visibilty updates the step navigation
|
|
|
|
this
|
|
|
|
.$elButton
|
|
|
|
.on('userform.field.hide userform.field.show', function(){
|
|
|
|
userform.$el.trigger('userform.form.conditionalstep');
|
|
|
|
});
|
|
|
|
|
2015-08-10 05:18:50 +02:00
|
|
|
return this;
|
|
|
|
}
|
2015-08-17 00:43:51 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine if this step is conditionally disabled
|
|
|
|
*
|
|
|
|
* @returns {Boolean}
|
|
|
|
*/
|
|
|
|
FormStep.prototype.conditionallyHidden = function(){
|
|
|
|
// Because the element itself could be visible but 0 height, so check visibility of button
|
|
|
|
return ! this
|
|
|
|
.$elButton
|
|
|
|
.find('button')
|
|
|
|
.is(':visible');
|
|
|
|
};
|
2015-08-06 07:47:14 +02:00
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
/**
|
|
|
|
* @func ProgressBar
|
|
|
|
* @constructor
|
2015-08-10 03:40:32 +02:00
|
|
|
* @param {object} element
|
|
|
|
* @return {object} - The Progress bar instance.
|
2015-08-06 01:01:21 +02:00
|
|
|
* @desc Creates a progress bar.
|
|
|
|
*/
|
|
|
|
function ProgressBar(element) {
|
2015-08-06 03:40:09 +02:00
|
|
|
var self = this;
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
this.$el = element instanceof jQuery ? element : $(element);
|
2015-08-06 03:40:09 +02:00
|
|
|
this.$buttons = this.$el.find('.step-button-jump');
|
2015-08-13 01:13:13 +02:00
|
|
|
this.$jsAlign = this.$el.find('.js-align');
|
2015-08-06 01:01:21 +02:00
|
|
|
|
2015-08-06 03:40:09 +02:00
|
|
|
// Update the progress bar when 'step' buttons are clicked.
|
|
|
|
this.$buttons.each(function (i, stepButton) {
|
2015-08-06 01:01:21 +02:00
|
|
|
$(stepButton).on('click', function (e) {
|
|
|
|
e.preventDefault();
|
2015-08-06 07:47:14 +02:00
|
|
|
self.$el.trigger('userform.progress.changestep', [parseInt($(this).text(), 10)]);
|
2015-08-06 01:01:21 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2015-08-06 03:40:09 +02:00
|
|
|
// Update the progress bar when 'prev' and 'next' buttons are clicked.
|
2015-08-17 00:43:51 +02:00
|
|
|
userform.$el.on('userform.form.changestep', function (e, stepID) {
|
|
|
|
self.update(stepID);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Listen for steps being conditionally shown / hidden by display rules.
|
|
|
|
// We need to update step related UI like the number of step buttons
|
|
|
|
// and any text that shows the total number of steps.
|
|
|
|
userform.$el.on('userform.form.conditionalstep', function () {
|
|
|
|
// Update the step numbers on the buttons.
|
|
|
|
var $visibleButtons = self.$buttons.filter(':visible');
|
|
|
|
|
|
|
|
$visibleButtons.each(function (i, button) {
|
|
|
|
$(button).text(i + 1);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Update the actual progress bar.
|
|
|
|
self.$el.find('.progress-bar').attr('aria-valuemax', $visibleButtons.length);
|
|
|
|
|
|
|
|
// Update any text that uses the total number of steps.
|
|
|
|
self.$el.find('.total-step-number').text($visibleButtons.length);
|
2015-08-06 03:40:09 +02:00
|
|
|
});
|
|
|
|
|
2015-08-10 03:28:06 +02:00
|
|
|
// Spaces out the steps below progress bar evenly
|
2015-08-13 01:13:13 +02:00
|
|
|
this.$jsAlign.each(function (index, button) {
|
2015-08-10 03:28:06 +02:00
|
|
|
var $button = $(button),
|
2015-08-13 01:13:13 +02:00
|
|
|
leftPercent = (100 / (self.$jsAlign.length - 1) * index + '%'),
|
2015-08-10 03:28:06 +02:00
|
|
|
buttonOffset = -1 * ($button.innerWidth() / 2);
|
|
|
|
|
|
|
|
$button.css({left: leftPercent, marginLeft: buttonOffset});
|
2015-08-17 01:06:56 +02:00
|
|
|
|
2015-08-13 02:39:04 +02:00
|
|
|
// First and last buttons are kept within userform-progress container
|
|
|
|
if (index === self.$jsAlign.length - 1) {
|
|
|
|
$button.css({marginLeft: buttonOffset * 2});
|
|
|
|
} else if (index === 0) {
|
|
|
|
$button.css({marginLeft: 0});
|
|
|
|
}
|
2015-08-10 03:28:06 +02:00
|
|
|
});
|
|
|
|
|
2015-08-17 00:43:51 +02:00
|
|
|
this.update(0);
|
2015-08-06 04:19:05 +02:00
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2015-08-06 03:40:09 +02:00
|
|
|
/**
|
|
|
|
* @func ProgressBar.update
|
2015-08-17 00:43:51 +02:00
|
|
|
* @param {number} stepID - Zero based index of the new step.
|
2015-08-06 03:40:09 +02:00
|
|
|
* @desc Update the progress element to show a new step.
|
|
|
|
*/
|
2015-08-17 00:43:51 +02:00
|
|
|
ProgressBar.prototype.update = function (stepID) {
|
|
|
|
var $newStepElement = $($('.form-step')[stepID]),
|
2015-08-19 05:29:14 +02:00
|
|
|
stepNumber = 0,
|
|
|
|
barWidth = stepID / (this.$buttons.length - 1) * 100;
|
2015-08-17 00:43:51 +02:00
|
|
|
|
|
|
|
// Set the current step number.
|
|
|
|
this.$buttons.each(function (i, button) {
|
|
|
|
if (i > stepID) {
|
|
|
|
return false; // break the loop
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($(button).is(':visible')) {
|
|
|
|
stepNumber += 1;
|
|
|
|
}
|
|
|
|
});
|
2015-08-10 03:28:06 +02:00
|
|
|
|
2015-08-06 03:40:09 +02:00
|
|
|
// Update elements that contain the current step number.
|
|
|
|
this.$el.find('.current-step-number').each(function (i, element) {
|
2015-08-17 00:43:51 +02:00
|
|
|
$(element).text(stepNumber);
|
2015-08-06 03:40:09 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
// Update aria attributes.
|
|
|
|
this.$el.find('[aria-valuenow]').each(function (i, element) {
|
2015-08-17 00:43:51 +02:00
|
|
|
$(element).attr('aria-valuenow', stepNumber);
|
2015-08-06 03:40:09 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
// Update the CSS classes on step buttons.
|
|
|
|
this.$buttons.each(function (i, element) {
|
2015-08-06 07:47:14 +02:00
|
|
|
var $element = $(element),
|
|
|
|
$item = $element.parent();
|
|
|
|
|
2015-08-17 00:43:51 +02:00
|
|
|
if (parseInt($element.text(), 10) === stepNumber && $element.is(':visible')) {
|
2015-08-06 07:47:14 +02:00
|
|
|
$item.addClass('current viewed');
|
|
|
|
$element.removeAttr('disabled');
|
2015-08-06 03:40:09 +02:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$item.removeClass('current');
|
|
|
|
});
|
|
|
|
|
2015-08-10 03:28:06 +02:00
|
|
|
// Update the progress bar's title with the new step's title.
|
2015-08-19 05:29:14 +02:00
|
|
|
this.$el.siblings('.progress-title').text($newStepElement.data('title'));
|
2015-08-10 03:28:06 +02:00
|
|
|
|
2015-08-06 03:40:09 +02:00
|
|
|
// Update the width of the progress bar.
|
2015-08-19 05:29:14 +02:00
|
|
|
barWidth = barWidth ? barWidth + '%' : '';
|
|
|
|
this.$el.find('.progress-bar').width(barWidth);
|
2015-08-17 00:43:51 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func FormActions
|
|
|
|
* @constructor
|
|
|
|
* @param {object} element
|
|
|
|
* @desc Creates the navigation and actions (Prev, Next, Submit buttons).
|
|
|
|
*/
|
|
|
|
function FormActions (element) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
this.$el = element instanceof jQuery ? element : $(element);
|
|
|
|
|
2015-08-21 01:41:42 +02:00
|
|
|
this.$prevButton = this.$el.find('.step-button-prev');
|
|
|
|
this.$nextButton = this.$el.find('.step-button-next');
|
|
|
|
|
|
|
|
// Show the buttons.
|
|
|
|
this.$prevButton.parent().attr('aria-hidden', false).show();
|
|
|
|
this.$nextButton.parent().attr('aria-hidden', false).show();
|
|
|
|
|
2015-08-17 00:43:51 +02:00
|
|
|
// Bind the step navigation event listeners.
|
2015-08-21 01:41:42 +02:00
|
|
|
this.$prevButton.on('click', function (e) {
|
2015-08-17 00:43:51 +02:00
|
|
|
e.preventDefault();
|
|
|
|
self.$el.trigger('userform.action.prev');
|
|
|
|
});
|
2015-08-21 01:41:42 +02:00
|
|
|
this.$nextButton.on('click', function (e) {
|
2015-08-17 00:43:51 +02:00
|
|
|
e.preventDefault();
|
|
|
|
self.$el.trigger('userform.action.next');
|
|
|
|
});
|
|
|
|
|
|
|
|
// Listen for changes to the current form step, or conditional pages,
|
|
|
|
// so we can show hide buttons appropriatly.
|
|
|
|
userform.$el.on('userform.form.changestep userform.form.conditionalstep', function () {
|
|
|
|
self.update();
|
|
|
|
});
|
|
|
|
|
|
|
|
this.update();
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @func FormAcrions.update
|
|
|
|
* @param {number} stepID - Zero based ID of the current step.
|
|
|
|
* @desc Updates the form actions element to reflect the current state of the page.
|
|
|
|
*/
|
|
|
|
FormActions.prototype.update = function () {
|
|
|
|
var numberOfSteps = userform.steps.length,
|
|
|
|
stepID = userform.currentStep.id,
|
|
|
|
i, lastStep;
|
|
|
|
|
|
|
|
// Update the "Prev" button.
|
|
|
|
this.$el.find('.step-button-prev')[stepID === 0 ? 'hide' : 'show']();
|
|
|
|
|
|
|
|
// Find last step, skipping hidden ones
|
|
|
|
for(i = numberOfSteps - 1; i >= 0; i--) {
|
|
|
|
lastStep = userform.steps[i];
|
|
|
|
|
|
|
|
// Skip if step is hidden
|
|
|
|
if(lastStep.conditionallyHidden()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the "Next" button.
|
|
|
|
this.$el.find('.step-button-next')[stepID >= i ? 'hide' : 'show']();
|
|
|
|
|
|
|
|
// Update the "Actions".
|
|
|
|
this.$el.find('.Actions')[stepID >= i ? 'show' : 'hide']();
|
|
|
|
|
|
|
|
// Stop processing last step
|
|
|
|
break;
|
|
|
|
}
|
2015-08-06 03:40:09 +02:00
|
|
|
};
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
/**
|
|
|
|
* @func main
|
|
|
|
* @desc Bootstraps the front-end.
|
|
|
|
*/
|
|
|
|
function main() {
|
2015-08-13 01:55:39 +02:00
|
|
|
var progressBar = null,
|
2015-08-17 00:43:51 +02:00
|
|
|
formActions = null,
|
2015-08-13 01:55:39 +02:00
|
|
|
$userform = $('.userform');
|
2015-08-06 01:01:21 +02:00
|
|
|
|
2015-08-21 01:41:42 +02:00
|
|
|
// If there's no userform, do nothing.
|
|
|
|
if ($userform.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
CONSTANTS.ENABLE_LIVE_VALIDATION = $userform.data('livevalidation') !== void 0;
|
|
|
|
CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP = $userform.data('toperrors') !== void 0;
|
|
|
|
CONSTANTS.HIDE_FIELD_LABELS = $userform.data('hidefieldlabels') !== void 0;
|
2015-08-10 03:40:32 +02:00
|
|
|
|
2015-08-06 07:47:14 +02:00
|
|
|
// Extend the default validation options with conditional options
|
|
|
|
// that are set by the user in the CMS.
|
2015-08-13 23:57:06 +02:00
|
|
|
if (CONSTANTS.ENABLE_LIVE_VALIDATION === false) {
|
2015-08-10 05:18:50 +02:00
|
|
|
$.extend(UserForm.prototype.validationOptions, {
|
2015-08-13 23:57:06 +02:00
|
|
|
onfocusout: false
|
2015-08-06 07:47:14 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
|
2015-08-10 05:18:50 +02:00
|
|
|
$.extend(UserForm.prototype.validationOptions, {
|
|
|
|
// Callback for custom code when an invalid form / step is submitted.
|
2015-08-06 07:47:14 +02:00
|
|
|
invalidHandler: function (event, validator) {
|
2015-08-11 01:40:37 +02:00
|
|
|
$userform.trigger('userform.form.error', [validator]);
|
2015-08-06 07:47:14 +02:00
|
|
|
},
|
|
|
|
onfocusout: false
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-08-10 03:28:06 +02:00
|
|
|
// Display all the things that are hidden when JavaScript is disabled.
|
|
|
|
$('.userform-progress, .step-navigation').attr('aria-hidden', false).show();
|
|
|
|
|
2015-08-13 01:55:39 +02:00
|
|
|
// Extend classes with common functionality.
|
|
|
|
$.extend(FormStep.prototype, commonMixin);
|
|
|
|
$.extend(ErrorContainer.prototype, commonMixin);
|
|
|
|
|
2015-08-11 01:40:37 +02:00
|
|
|
userform = new UserForm($userform);
|
2015-08-10 05:18:50 +02:00
|
|
|
|
2015-08-10 03:40:32 +02:00
|
|
|
// Conditionally hide field labels and use HTML5 placeholder instead.
|
|
|
|
if (CONSTANTS.HIDE_FIELD_LABELS) {
|
2015-08-11 01:40:37 +02:00
|
|
|
$userform.find('label.left').each(function () {
|
2015-08-10 03:40:32 +02:00
|
|
|
var $label = $(this);
|
|
|
|
|
|
|
|
$('[name="' + $label.attr('for') + '"]').attr('placeholder', $label.text());
|
|
|
|
$label.remove();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
// Initialise the form steps.
|
|
|
|
userform.$el.find('.form-step').each(function (i, element) {
|
|
|
|
var step = new FormStep(element);
|
|
|
|
|
|
|
|
userform.addStep(step);
|
|
|
|
});
|
|
|
|
|
|
|
|
userform.setCurrentStep(userform.steps[0]);
|
2015-08-17 00:43:51 +02:00
|
|
|
|
|
|
|
// Initialise actions and progressbar
|
|
|
|
progressBar = new ProgressBar($('#userform-progress'));
|
|
|
|
formActions = new FormActions($('#step-navigation'));
|
2015-08-06 01:01:21 +02:00
|
|
|
|
2015-08-10 03:40:32 +02:00
|
|
|
// Enable jQuery UI datepickers
|
|
|
|
$(document).on('click', 'input.text[data-showcalendar]', function() {
|
|
|
|
var $element = $(this);
|
|
|
|
|
|
|
|
$element.ssDatepicker();
|
|
|
|
|
|
|
|
if($element.data('datepicker')) {
|
|
|
|
$element.datepicker('show');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2015-08-06 01:01:21 +02:00
|
|
|
// Make sure the form doesn't expire on the user. Pings every 3 mins.
|
|
|
|
setInterval(function () {
|
|
|
|
$.ajax({ url: 'UserDefinedForm_Controller/ping' });
|
|
|
|
}, 180 * 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
main();
|
2012-04-26 02:14:27 +02:00
|
|
|
});
|