mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-29 23:49:33 +00:00
616 lines
21 KiB
JavaScript
616 lines
21 KiB
JavaScript
/* Use with:
|
|
*
|
|
* $('#edit-recipient-modal form').modalHandler({ ... })
|
|
*/
|
|
class ModalHandler {
|
|
|
|
constructor(form, options) {
|
|
|
|
if (typeof options.csrf === "undefined") {
|
|
throw new Error("ModalHandler::Missing CSRF token!");
|
|
}
|
|
|
|
this.element = form;
|
|
this.dialog = $(form).closest(".modal");
|
|
|
|
this.options = options;
|
|
this.csrf = options.csrf;
|
|
this.dontDisableSubmit = options.dontDisableSubmit;
|
|
|
|
this.observer = new MutationObserver((list) => {
|
|
this.bindFormValidation();
|
|
this.toggleFormSubmission();
|
|
this.initDataPatterns();
|
|
});
|
|
|
|
this.observer.observe(this.element[0], {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
|
|
this.initialState = null;
|
|
this.currentState = null;
|
|
this.firstCloseAttempt = false;
|
|
this.isSubmitting = false;
|
|
|
|
const submitButton = $(this.element).find(`[type='submit']`);
|
|
if (submitButton.length == 0) {
|
|
throw new Error("ModalHandler::The submit button was not found inside the form!");
|
|
}
|
|
|
|
this.toggleFormSubmission();
|
|
|
|
}
|
|
|
|
initDataPatterns() {
|
|
NtopUtils.initDataPatterns();
|
|
}
|
|
|
|
/**
|
|
* Create a form's snapshot to save a form state
|
|
*/
|
|
createFormSnapshot() {
|
|
|
|
const snapshot = {
|
|
inputs: {},
|
|
hidden: []
|
|
};
|
|
|
|
$(this.element).find('textarea,select,input[type!="radio"]').each(function () {
|
|
|
|
const type = $(this).prop('nodeName').toLowerCase();
|
|
const name = $(this).attr('name');
|
|
snapshot.inputs[`${type}[name='${name}']`] = $(this).val();
|
|
});
|
|
|
|
$(this.element).find(`[style='display: none;'], span.invalid-feedback`).each(function () {
|
|
snapshot.hidden.push($(this));
|
|
});
|
|
|
|
return snapshot;
|
|
}
|
|
|
|
compareFormSnaphsot(s1, s2) {
|
|
|
|
if (s1 == null || s2 == null) return true;
|
|
|
|
for (let [key, value] of Object.entries(s1.inputs)) {
|
|
if (s2.inputs[key] != value) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
delegateModalClosing() {
|
|
|
|
const self = this;
|
|
|
|
$(this.dialog).find('button.cancel').off('click').click(function () {
|
|
|
|
self.firstCloseAttempt = false;
|
|
$(self.element)[0].reportValidity();
|
|
$(self.dialog).find('.confirm-closing').fadeOut(100, function () {
|
|
$(self.dialog).find('button.btn-close').fadeIn(100);
|
|
});
|
|
});
|
|
|
|
$(this.dialog).off('hide.bs.modal').on('hide.bs.modal', function (event) {
|
|
|
|
if (self.isSubmitting) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// if the form state hasn't changed then don't show the message
|
|
if (self.compareFormSnaphsot(self.currentState, self.initialState)) {
|
|
return;
|
|
}
|
|
|
|
if (self.firstCloseAttempt) return;
|
|
// abort the modal closing event
|
|
event.preventDefault();
|
|
|
|
// flag a close attempt has been invoked
|
|
self.firstCloseAttempt = true;
|
|
|
|
// show an alert to inform the user
|
|
$(self.dialog).find('button.btn-close').fadeOut(100, function () {
|
|
$(self.dialog).find('.confirm-closing').fadeIn(100);
|
|
});
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
$(this.dialog).off('hidden.bs.modal').on('hidden.bs.modal', function (event) {
|
|
|
|
// for each input inside the form restore the initial value
|
|
// from the snapshot taken at init
|
|
for (const [selector, value] of Object.entries(self.initialState.inputs)) {
|
|
$(self.dialog).find(selector).val(value);
|
|
$(self.dialog).find(selector).removeClass('is-invalid');
|
|
}
|
|
|
|
// hide the shwon elements
|
|
self.initialState.hidden.forEach(($hidden) => {
|
|
$hidden.hide();
|
|
});
|
|
|
|
self.element.find(`[type='submit']`).attr("disabled", "disabled");
|
|
self.currentState = null;
|
|
self.firstCloseAttempt = false;
|
|
|
|
$(self.dialog).find('.confirm-closing').fadeOut(100, function () {
|
|
$(self.dialog).find('button.btn-close').fadeIn(100);
|
|
});
|
|
|
|
// clean the form when the modal is closed
|
|
// to prevent the fields flickering
|
|
self.cleanForm();
|
|
});
|
|
}
|
|
|
|
fillFormModal() {
|
|
return this.options.loadFormData();
|
|
}
|
|
|
|
invokeModalInit(data = {}) {
|
|
|
|
const self = this;
|
|
|
|
// reset form values when the modal closes
|
|
this.delegateModalClosing();
|
|
this.data = data || this.fillFormModal();
|
|
this.options.onModalInit(this.data, this);
|
|
|
|
$(this.element).parents('.modal').on('show.bs.modal', function () {
|
|
self.options.onModalShow();
|
|
});
|
|
|
|
// create a initial form snapshot to restore elements on closing
|
|
this.initialState = this.createFormSnapshot();
|
|
this.currentState = null;
|
|
|
|
this.delegateResetButton();
|
|
}
|
|
|
|
delegateSubmit() {
|
|
|
|
this.bindFormValidation();
|
|
|
|
const self = this;
|
|
|
|
this.submitHandler = function (e) {
|
|
if (!self.options.isSyncRequest) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
self.makeRequest();
|
|
}
|
|
};
|
|
|
|
$(this.element).on('submit', this.submitHandler);
|
|
}
|
|
|
|
bindFormValidation() {
|
|
|
|
const self = this;
|
|
|
|
// handle input validation
|
|
$(this.element).find(`input,select,textarea`).each(async function (i, input) {
|
|
|
|
// jQuery object of the current input
|
|
const $input = $(this);
|
|
// id to handle the current timeout set to show errors
|
|
let timeoutId = -1;
|
|
|
|
const validHostname = async () => {
|
|
|
|
// show the spinner to the user and set the input to readonly
|
|
const $spinner = $input.parent().find('.spinner-border');
|
|
$input.attr("readonly", true);
|
|
$spinner.show();
|
|
|
|
const response = await NtopUtils.resolveDNS($(input).val());
|
|
|
|
// hide the spinner and renable write to the input
|
|
$input.removeAttr("readonly");
|
|
$spinner.hide();
|
|
|
|
// if the response was negative then alert the user
|
|
if (response.rc < 0) {
|
|
input.setCustomValidity(response.rc_str);
|
|
return [false, response.rc_str_hr];
|
|
}
|
|
|
|
// return success for valid resolved hostnmae
|
|
input.setCustomValidity("");
|
|
|
|
return [true, "Success"];
|
|
}
|
|
|
|
const validInput = async (validation) => {
|
|
// if the input require to validate host name then perform a DNS resolve
|
|
if (validation.data.resolveDNS && $input.val().match(NtopUtils.REGEXES.domainName)) {
|
|
return await validHostname();
|
|
}
|
|
|
|
if (validation.data.cannotBeEmpty && validation.isInputEmpty) {
|
|
// trigger input validation flag
|
|
input.setCustomValidity("Please fill the input.");
|
|
return [false, validation.data.validationEmptyMessage || i18n_ext.missing_field];
|
|
}
|
|
|
|
if (input.validity.patternMismatch) {
|
|
input.setCustomValidity("Pattern mismatch.");
|
|
return [false, validation.data.validationMessage || i18n_ext.invalid_field];
|
|
}
|
|
|
|
if (input.validity.rangeOverflow) {
|
|
input.setCustomValidity("Value exceed the maximum value.");
|
|
return [false, validation.data.rangeOverflowMessage || i18n_ext.invalid_field];
|
|
}
|
|
|
|
if (input.validity.rangeUnderflow) {
|
|
input.setCustomValidity("Value is under the minimum value.");
|
|
return [false, validation.data.rangeUnderflowMessage || i18n_ext.invalid_field];
|
|
}
|
|
|
|
if (input.validity.typeMismatch) {
|
|
input.setCustomValidity("Pattern mismatch.");
|
|
return [false, validation.data.validationMessage || i18n_ext.invalid_field];
|
|
}
|
|
|
|
// set validation to true
|
|
input.setCustomValidity("");
|
|
return [true, "Success"];
|
|
}
|
|
|
|
const checkValidation = async () => {
|
|
const validation = {
|
|
data: {
|
|
validationMessage: $input.data('validationMessage'),
|
|
validationEmptyMessage: $input.data('validationEmptyMessage'),
|
|
cannotBeEmpty: ($input.attr('required') === "required") || ($input.data("validationNotEmpty") == true),
|
|
resolveDNS: $input.data('validationResolvedns'),
|
|
rangeOverflowMessage: $input.data('validationRangeOverflowMessage'),
|
|
rangeUnderflowMessage: $input.data('validationUnderflowOverflowMessage'),
|
|
},
|
|
isInputEmpty: (typeof($input.val()) === "string" ? $input.val().trim() == "" : false)
|
|
};
|
|
|
|
const [isValid, messageToShow] = await validInput(validation);
|
|
let $error = $input.parent().find(`.invalid-feedback`);
|
|
|
|
// if the error element doesn't exist then create a new one
|
|
if ($error.length == 0) {
|
|
$error = $(`<span class='invalid-feedback'></span>`);
|
|
}
|
|
|
|
// display the errors and color the input box
|
|
if (!isValid) {
|
|
$input.addClass('is-invalid');
|
|
$input.parent().append($error);
|
|
$error.text(messageToShow);
|
|
}
|
|
else {
|
|
// clean the validation message and remove the error
|
|
$input.removeClass('is-invalid');
|
|
$error.fadeOut(500, function () { $(this).remove(); });
|
|
}
|
|
}
|
|
|
|
$(this).off('input').on('input', function (e) {
|
|
|
|
self.currentState = self.createFormSnapshot();
|
|
|
|
// if exists already a Timeout then clear it
|
|
if (timeoutId != -1) clearTimeout(timeoutId);
|
|
|
|
if (!$input.attr("formnovalidate")) {
|
|
// trigger input validation after 300msec
|
|
timeoutId = setTimeout(() => {
|
|
checkValidation();
|
|
// trigger form validation to enable the submit button
|
|
self.toggleFormSubmission();
|
|
}, 300);
|
|
// the user has changed the input, we can abort the first close attempt
|
|
self.firstCloseAttempt = false;
|
|
}
|
|
});
|
|
|
|
$(this).off('invalid').on('invalid', function (e) {
|
|
e.preventDefault();
|
|
if (!$input.attr("formnovalidate")) {
|
|
checkValidation();
|
|
}
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
getModalID() {
|
|
return $(this.element).parents('.modal').attr('id');
|
|
}
|
|
|
|
toggleFormSubmission() {
|
|
|
|
let isValid = true;
|
|
|
|
// if each input is marked as valid then enable the form submit button
|
|
$(this.element).find('input:not(:disabled),select:not(:disabled),textarea:not(:disabled)').each(function (idx, input) {
|
|
// make a concatenate & between valid flags
|
|
isValid &= input.validity.valid;
|
|
});
|
|
|
|
isValid
|
|
? $(this.element).find(`[type='submit'],[type='test']`).removeAttr("disabled")
|
|
: $(this.element).find(`[type='submit'],[type='test']`).attr("disabled", "disabled");
|
|
}
|
|
|
|
cleanForm() {
|
|
/* remove validation class from fields */
|
|
$(this.element).find('input,textarea,select').each(function (i, input) {
|
|
$(this).removeClass(`is-valid`).removeClass(`is-invalid`);
|
|
});
|
|
/* reset all the values */
|
|
$(this.element)[0].reset();
|
|
}
|
|
|
|
makeRequest() {
|
|
|
|
const $feedbackLabel = $(this.element).find(`.invalid-feedback`);
|
|
const submitButton = $(this.element).find(`[type='submit']`);
|
|
let dataToSend = this.options.beforeSumbit(this.data);
|
|
|
|
dataToSend.csrf = this.csrf;
|
|
dataToSend = $.extend(dataToSend, this.options.submitOptions);
|
|
|
|
/* clean previous state and disable button */
|
|
submitButton.attr("disabled", "disabled");
|
|
|
|
const self = this;
|
|
|
|
if (this.options.endpoint) {
|
|
let request;
|
|
|
|
if (self.options.method == "post") {
|
|
request = $.ajax({
|
|
url: this.options.endpoint,
|
|
data: JSON.stringify(dataToSend),
|
|
method: self.options.method,
|
|
dataType: "json",
|
|
contentType: "application/json; charset=utf-8"
|
|
});
|
|
}
|
|
else {
|
|
request = $.get(this.options.endpoint, dataToSend);
|
|
}
|
|
|
|
this.isSubmitting = true;
|
|
|
|
request.done(function (response, textStatus) {
|
|
|
|
// clear submitting state
|
|
self.isSubmitting = false;
|
|
// clear the current form state
|
|
self.currentState = null;
|
|
|
|
if (self.options.resetAfterSubmit) self.cleanForm();
|
|
$feedbackLabel.hide();
|
|
|
|
const success = self.options.onSubmitSuccess(response, dataToSend, self);
|
|
// if the submit return a true boolean then close the modal
|
|
if (success) {
|
|
if(self.dialog.modal)
|
|
self.dialog.modal('hide')
|
|
else {
|
|
self.dialog[0].hidden = true;
|
|
$(`.modal-backdrop.fade.show`).remove()
|
|
}
|
|
}
|
|
|
|
/* unbind the old closure on submit event and bind a new one */
|
|
$(self.element).off('submit', self.submitHandler);
|
|
self.delegateSubmit();
|
|
})
|
|
.fail(function (jqxhr, textStatus, errorThrown) {
|
|
|
|
self.isSubmitting = false;
|
|
const response = jqxhr.responseJSON;
|
|
if (response.rc !== undefined && response.rc < 0) {
|
|
$feedbackLabel.html(response.rc_str_hr).show();
|
|
}
|
|
|
|
self.options.onSubmitError(response, dataToSend, textStatus, errorThrown);
|
|
})
|
|
.always(function (d) {
|
|
submitButton.removeAttr("disabled");
|
|
});
|
|
|
|
} else { // no endpoint
|
|
|
|
// clear the current form state
|
|
self.currentState = null;
|
|
|
|
//if (self.options.resetAfterSubmit) self.cleanForm();
|
|
$feedbackLabel.hide();
|
|
|
|
const success = self.options.onSubmitSuccess({}, dataToSend, self);
|
|
// if the submit return a true boolean then close the modal
|
|
if (success) {
|
|
if(self.dialog.modal)
|
|
self.dialog.modal('hide');
|
|
else
|
|
self.dialog[0].hidden = true;
|
|
}
|
|
|
|
/* unbind the old closure on submit event and bind a new one */
|
|
$(self.element).off('submit', self.submitHandler);
|
|
self.delegateSubmit();
|
|
|
|
submitButton.removeAttr("disabled");
|
|
}
|
|
}
|
|
|
|
delegateResetButton() {
|
|
|
|
const self = this;
|
|
const resetButton = $(this.element).find(`[type='reset']`);
|
|
if (resetButton.length == 0) return;
|
|
|
|
const defaultValues = NtopUtils.serializeFormArray($(this.element).serializeArray());
|
|
|
|
resetButton.click(function (e) {
|
|
|
|
e.preventDefault();
|
|
|
|
// reset the previous values
|
|
$(self.element).find('input:visible,select').each(function (i, input) {
|
|
const key = $(input).attr('name');
|
|
$(input).val(defaultValues[key])
|
|
.removeClass('is-invalid').removeClass('is-valid');
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
const modalHandler = function (args) {
|
|
|
|
if (this.length != 1) throw new Error("Only a form element can by initialized!");
|
|
|
|
const options = $.extend({
|
|
csrf: '',
|
|
endpoint: '',
|
|
resetAfterSubmit: true,
|
|
/* True to skip the are-you-sure check on the dialog */
|
|
dontDisableSubmit: false,
|
|
/* True if the request isn't done by AJAX request */
|
|
isSyncRequest: false,
|
|
method: 'get',
|
|
/**
|
|
* Fetch data asynchronusly from the server or
|
|
* loads data directly from the current page.
|
|
* The function must returns the fetched data.
|
|
*
|
|
* @returns Returns the fetched data.
|
|
* @example Below there is an example showing
|
|
* how to use the function when fetching data from the server
|
|
* ```
|
|
* loadFormData: async function() {
|
|
* const data = await fetch(`endpoint/to/data`);
|
|
* const user = await data.json();
|
|
* return user;
|
|
* }
|
|
* ```
|
|
*/
|
|
loadFormData: function () { },
|
|
|
|
/**
|
|
* onModalInit() is invoked when the plugin has been initialized.
|
|
* This function is used to load the fetched data from `loadFormData()`
|
|
* inside the form modal inputs.
|
|
*
|
|
* @param {object} loadedData This argument contains the fetched data obtained
|
|
* from `loadFormData()`
|
|
* @example Below there is an example showing how to use
|
|
* the function (we suppose that loadFormData() returns the following
|
|
* object: `loadedUser = {firstname: 'Foo', lastname: 'Bar', id: 1428103}`)
|
|
* ```
|
|
* onModalInit: function(loadedUser) {
|
|
* $(`#userModal form input#firstname`).val(loadedUser.firstname);
|
|
* $(`#userModal form input#lastname`).val(loadedUser.lastname);
|
|
* $(`#userModal form input#id`).val(loadedUser.id);
|
|
* }
|
|
* ```
|
|
*/
|
|
onModalInit: function (loadedData) { },
|
|
|
|
onModalShow: function () { },
|
|
|
|
/**
|
|
* The function beforeSubmit() is invoked after the user
|
|
* submit the form. The function must return the data to
|
|
* send to the endpoint. If the chosen method is `post`
|
|
* a csrf will be add to the returned object.
|
|
*
|
|
* @example We show below a simple example how to use the function:
|
|
* ```
|
|
* beforeSubmit: function() {
|
|
* const body = {
|
|
* action: 'edit',
|
|
* JSON: JSON.stringify(serializeArrayForm($(`form`).serializeArray()))
|
|
* };
|
|
* return body;
|
|
* }
|
|
* ```
|
|
*/
|
|
beforeSumbit: function () { return {} },
|
|
|
|
/**
|
|
* This function is invoked when the request to the endpoint
|
|
* terminates successfully (200). Before the call of this function
|
|
* a new csrf retrived from the server will be set for
|
|
* future calls.
|
|
*
|
|
* @param {object} response This object contains the response
|
|
* from the server
|
|
*
|
|
* @example Below there is an example showing a simple user case:
|
|
* ```
|
|
* onSubmitSuccess: function(response) {
|
|
* if (response.success) {
|
|
* console.log(`The user info has been edit with success!`);
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
onSubmitSuccess: function (response) { },
|
|
|
|
/**
|
|
* This function is invoked when the request to the endpoint
|
|
* terminates with failure (!= 200). Before the call of this function
|
|
* a new csrf retrived from the server will be set for
|
|
* future calls.
|
|
*
|
|
* @param {object} sent This object contains the sent data to the endpoint
|
|
* @param {string} textStatus It contains the error text status obtained
|
|
* @param {object} errorThrown This object contains info about the error
|
|
*
|
|
* @example Below there is an example showing a simple user case:
|
|
* ```
|
|
* onSubmitError: function(sent, textStatus, errorThrown) {
|
|
* if (errorThrown) {
|
|
* console.error(`Ops, something went wrong!`);
|
|
* console.error(errorThrown);
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
onSubmitError: function (sent, textStatus, errorThrown) { },
|
|
|
|
/**
|
|
* This function is invoked when the user click the reset input
|
|
* inside the form.
|
|
*
|
|
* @param {object} defaultData It contains the fetched data from
|
|
* `loadFormData()`.
|
|
*
|
|
* @example Below there is an example how to use the function:
|
|
* ```
|
|
* onModalReset: function(defaultData) {
|
|
* $(`input#id`).val(defaultData.id);
|
|
* $(`input#name`).val(defaultData.name);
|
|
* $(`input#address`).val(defaultData.address);
|
|
* }
|
|
* ```
|
|
*/
|
|
onModalReset: function (defaultData) { },
|
|
}, args);
|
|
|
|
const mh = new ModalHandler(this, options);
|
|
mh.delegateSubmit();
|
|
|
|
return mh;
|
|
}
|
|
|
|
export default modalHandler
|