@userfrosting/theme-adminlte
Version:
Admin LTE Theme for UserFrosting 5
436 lines (396 loc) • 16.6 kB
JavaScript
/**
* uf-form plugin. Handles validation and submission for basic UserFrosting forms.
*
* This plugin uses the jQueryvalidation plugin (https://jqueryvalidation.org/) to perform instant, client-side form validation.
* UserFrosting forms must be wrapped in a <form> element, and contain a <button type=submit> element for submission.
*
* Forms are then set to submit via AJAX when the submit button is clicked.
*
* === USAGE ===
*
* uf-form can be initialized on a form element as follows:
*
* $('#myForm').ufForm(options);
*
* `options` is an object containing any of the following parameters:
* @param {JSON} validator An object containing two keys, "rules" and "messages", which specify the jQueryvalidation rules to use.
* @param {Object} msgTarget a jQuery selector specifying the element where any error messages should be inserted. Defaults to looking for a container with class .js-form-alerts inside this form.
* @param {Callback} beforeSubmitCallback a callback function to execute immediately after form validation, before the form is submitted.
* @param {bool} binaryCheckboxes specify whether to submit checkboxes as binary values 0 and 1, instead of omitting unchecked checkboxes from submission.
*
* == EVENTS ==
*
* ufForm triggers the following events:
*
* `submitSuccess.ufForm`: triggered when the form is successfully submitted, after re-enabling the submit button.
* `submitError.ufForm`: triggered when the form submission (not validation) fails, after re-enabling the submit button
* and displaying any error messages.
*
* UserFrosting https://www.userfrosting.com
* @author Alexander Weissman <https://alexanderweissman.com>
*
* @todo Implement proper fallback for when `set` function isn't supported by FormData.
*/
;
(function ($, window, document, undefined) {
'use strict';
// Define plugin name and defaults.
var pluginName = 'ufForm',
defaults = {
DEBUG: false,
site: site, // global site variables
submittingText: "<i class='fa-solid fa-spinner fa-spin'></i>",
beforeSubmitCallback: null,
binaryCheckboxes: true, // submit checked/unchecked checkboxes as 0/1 values
keyupDelay: 0,
showAlertOnSuccess: false,
// These are options that will be passed to jQuery Validate
// See https://jqueryvalidation.org/validate/#-validate()
validator: {
rules: {},
messages: {}
},
// Deprecated
validators: {
rules: {},
messages: {}
}
};
// Constructor
function Plugin(element, options) {
this.element = element[0];
this.$element = $(this.element);
var lateDefaults = {
submitButton: this.$element.find('button[type=submit]'),
msgTarget: this.$element.find('.js-form-alerts:first'),
// These are options that will be passed to jQuery Validate
// See https://jqueryvalidation.org/validate/#-validate()
validator: {
submitHandler: $.proxy(this.defaultSubmitHandler, this),
onkeyup: $.proxy(this._onKeyUp, this)
},
ajax: {
// Override jQuery's strict JSON parsing
converters: {
'text json': $.proxy(this._defaultResponseParser, this)
},
// Response type
dataType: this._debugAjax ? 'html' : 'json',
// Disable the submit button before sending the request
beforeSend: $.proxy(this.disableSubmit, this),
// enable the submit button once the request completes
complete: $.proxy(this.enableSubmit, this)
}
};
// Legacy options
if ((typeof options !== 'undefined') && 'validators' in options) {
options.validator = options.validators;
}
this.settings = $.extend(true, {}, defaults, lateDefaults, options);
this._defaults = $.extend(true, {}, defaults, lateDefaults);
this._name = pluginName;
this._debugAjax = (typeof site !== 'undefined') && site.debug.ajax;
this.submitButtonText = this.settings.submitButton ? this.settings.submitButton.html() : '';
// Detect changes to element attributes
this.$element.attrchange({
callback: function (event) {
this.element = event.target;
}.bind(this)
});
// Setup validator
this.validator = this.$element.validate(this.settings.validator);
return this;
}
// Functions
/**
* Set "loading" text for submit button, if it exists, and disable button
*/
Plugin.prototype.disableSubmit = function () {
var submitButton = this.settings.submitButton;
// Do nothing, if the button is already disabled
if (submitButton.prop('disabled')) {
return this;
}
if (!submitButton) {
console.error('Submit button not found.');
return this;
}
this.submitButtonText = submitButton.html();
submitButton.prop('disabled', true);
submitButton.html(this.settings.submittingText);
return this;
};
/**
* Restore button text and re-enable submit button
*
* @return this
*/
Plugin.prototype.enableSubmit = function () {
var submitButton = this.settings.submitButton;
// Do nothing, if the button is already enabled
if (!submitButton.prop('disabled')) {
return this;
}
if (!submitButton) {
console.error('Submit button not found.');
return this;
}
submitButton.prop('disabled', false);
submitButton.html(this.submitButtonText);
return this;
};
/**
* Handles the form submission after successful client-side validation.
*
* @param {Element} form
* @param {Event} event
*/
Plugin.prototype.defaultSubmitHandler = function (form, event) {
// Execute any "before submit" callback
if (this.settings.beforeSubmitCallback) {
this.settings.beforeSubmitCallback();
}
// Get basic request parameters.
var ajaxParams = this.prepareRequestData(form, this.settings.ajax);
// Submit the form
this.submitForm(ajaxParams);
};
/**
* Submit the form via AJAX
* @param {object} reqParams
* @returns {Deferred}
*/
Plugin.prototype.submitForm = function (reqParams) {
return $.ajax(reqParams).then(
// Submission successful
$.proxy(this._defaultSuccess, this),
// Submission failed
$.proxy(this._defaultError, this)
);
}
/**
* Default handler for AJAX request success.
*
* @param {object} data
* @param {string} textStatus
* @param {jqXHR} jqXHR
* @return {jqXHR}
*/
Plugin.prototype._defaultSuccess = function (data, textStatus, jqXHR) {
this.$element.trigger('submitSuccess.ufForm', [data, textStatus, jqXHR]);
if (this.settings.showAlertOnSuccess) {
// showing UF alerts on success also.
// This will show the Alerts from the Alert stream when the page is not reloaded after the ufForm submit succeeds,
if (!this.settings.msgTarget.data('ufAlerts')) {
this.settings.msgTarget.ufAlerts();
} else {
this.settings.msgTarget.ufAlerts('clear');
}
this.settings.msgTarget.ufAlerts('fetch').ufAlerts('render');
}
return jqXHR;
};
/**
* Default handler for AJAX request fail/error.
*
* @param {jqXHR} jqXHR
* @param {string} textStatus
* @param {string} errorThrown
* @return {jqXHR}
*/
Plugin.prototype._defaultError = function (jqXHR, textStatus, errorThrown) {
// Error messages
if (!this.settings.msgTarget.data('ufAlerts')) {
this.settings.msgTarget.ufAlerts();
}
if (this._debugAjax && jqXHR.responseText) {
this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]);
this.settings.msgTarget.ufAlerts('push', 'danger', jqXHR.responseJSON.description).ufAlerts('render');
// TODO : Below only works if the response is HTML, above if JSON. Might be worth testing for both
// document.write(jqXHR.responseText);
// document.close();
} else {
if (this.settings.DEBUG) {
console.error(jqXHR.status + ': ' + jqXHR.responseText);
}
// Display errors on failure
// TODO: ufAlerts widget should have a 'destroy' method
if (!this.settings.msgTarget.data('ufAlerts')) {
this.settings.msgTarget.ufAlerts();
} else {
this.settings.msgTarget.ufAlerts('clear');
}
this.settings.msgTarget.ufAlerts('fetch').ufAlerts('render');
this.settings.msgTarget.on('render.ufAlerts', $.proxy(function () {
this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]);
}, this));
}
return jqXHR;
};
/**
* Default parser for text/json ajax response from server.
*
* @param {string} result
* @return {object}
*/
Plugin.prototype._defaultResponseParser = function (result) {
try {
// First try to use native browser parsing
if (typeof JSON === 'object' && typeof JSON.parse === 'function') {
return JSON.parse(result);
} else {
return $.parseJSON(result);
}
} catch (e) {
// statements to handle any exceptions
console.warn('Could not parse expected JSON response.');
return {};
}
};
/**
* Prepares the ajax request parameters, serializing the form according to its enctype,
* and then using the serialized data as the ajax `data` parameter.
*
* @param {Element} form
* @param {object} ajaxParams
* @return {object}
*/
Plugin.prototype.prepareRequestData = function (form, ajaxParams) {
// Set ajax type and url from form method and action, if not otherwise set.
var ajaxDefaults = {
type: this.$element.attr('method'),
url: this.$element.attr('action'),
contentType: (typeof this.$element.attr('enctype') === 'undefined') ?
'application/x-www-form-urlencoded; charset=UTF-8' : this.$element.attr('enctype')
};
ajaxParams = $.extend(true, {}, ajaxDefaults, ajaxParams);
// Get the form encoding type from the users HTML, and chose an encoding form.
if (ajaxParams.contentType.toLowerCase() === 'multipart/form-data') {
ajaxParams.data = this._multipartData(form);
// Disable caching
ajaxParams.cache = false;
// Prevent serialization of FormData object
ajaxParams.processData = false;
// Allow the browser to set the Content-Type header instead,
// so it can include the boundary value
ajaxParams.contentType = false;
} else {
ajaxParams.data = this._urlencodeData(form);
}
return ajaxParams;
};
/**
* Helper function for encoding data as urlencoded
*
* @param {Element} form
* @return {string}
*/
Plugin.prototype._urlencodeData = function (form) {
// Serialize and post to the backend script in ajax mode
var serializedData;
if (this.settings.binaryCheckboxes) {
serializedData = $(form).find(':input').not(':checkbox').serialize();
// Get unchecked checkbox values, set them to 0
$(form).find('input[type=checkbox]:enabled').each(function () {
if ($(this).is(':checked')) {
serializedData += '&' + encodeURIComponent(this.name) + '=1';
} else {
serializedData += '&' + encodeURIComponent(this.name) + '=0';
}
});
} else {
serializedData = $(form).find(':input').serialize();
}
return serializedData;
};
/**
* Helper function for encoding data as multipart/form-data
*
* @param {Element} form
* @return {FormData}
*/
Plugin.prototype._multipartData = function (form) {
// Use FormData to wrap form contents.
// https://developer.mozilla.org/en/docs/Web/API/FormData
var formData = new FormData(form);
// Serialize and post to the backend script in ajax mode
if (this.settings.binaryCheckboxes) {
// Get unchecked checkbox values, set them to 0.
var checkboxes = $(form).find('input[type=checkbox]:enabled');
// Feature detection. Several browsers don't support `set`
if (typeof formData.set !== 'function') {
this.settings.msgTarget.ufAlerts('push', 'danger', "Your browser is missing a required feature. This form will still attempt to submit, but if it fails, you'll need to use Chrome for desktop or FireFox for desktop.");
} else {
checkboxes.each(function () {
if ($(this).is(':checked')) {
// this replaces checkbox value with 1 (as we're using binaryCheckboxes).
formData.set(this.name, 1);
// this explicitly adds unchecked boxes.
} else {
formData.set(this.name, 0);
}
});
}
}
return formData;
};
/**
* Implements a delay for revalidating the form.
*
* @param {Element} form
* @param {Event} event
*/
Plugin.prototype._onKeyUp = function (element, event) {
var validator = this.validator;
// See http://stackoverflow.com/questions/41363409/jquery-validate-add-delay-to-keyup-validation
setTimeout(function () {
// Avoid revalidate the field when pressing one of the following keys
// Shift => 16
// Ctrl => 17
// Alt => 18
// Caps lock => 20
// End => 35
// Home => 36
// Left arrow => 37
// Up arrow => 38
// Right arrow => 39
// Down arrow => 40
// Insert => 45
// Num lock => 144
// AltGr key => 225
var excludedKeys = [
16, 17, 18, 20, 35, 36, 37,
38, 39, 40, 45, 144, 225
];
if (event.which === 9 && validator.elementValue(element) === '' || $.inArray(event.keyCode, excludedKeys) !== -1) {
return;
} else if (element.name in validator.submitted || element.name in validator.invalid) {
validator.element(element);
}
}, this.settings.keyupDelay);
};
// Handles instantiation and access to non-private methods.
$.fn[pluginName] = function (methodOrOptions) {
// Grab plugin instance
var instance = $(this).data(pluginName);
// If undefined or object, initialize plugin.
if (methodOrOptions === undefined || typeof methodOrOptions === 'object') {
// Only initialize if not previously done.
if (!instance) {
$(this).data(pluginName, new Plugin(this, methodOrOptions));
}
return this;
}
// Otherwise ensure first parameter is a valid string, and is the name of an actual function.
else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') {
// Ensure not a private function
if (methodOrOptions.indexOf('_') !== 0) {
return instance[methodOrOptions](Array.prototype.slice.call(arguments, 1));
} else {
console.warn('Method ' + methodOrOptions + ' is private!');
}
} else {
console.warn('Method ' + methodOrOptions + ' does not exist.');
}
};
})(jQuery, window, document);