diff -r 000000000000 -r bca7a7e058a3 src/pyams_skin/resources/js/myams-form.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_skin/resources/js/myams-form.js Thu Feb 13 11:43:31 2020 +0100 @@ -0,0 +1,765 @@ +/** + * MyAMS forms management + */ +(function($, globals) { + + var ams = globals.MyAMS; + + ams.form = { + + /** + * Init forms to activate form change listeners + * + * @param element: the parent element + */ + init: function(element) { + + $('FORM', element).each(function() { + var form = $(this); + // Store value of hidden inputs + $('INPUT.select2[type="hidden"]', form).each(function() { + var input = $(this); + input.data('ams-select2-input-value', input.val()); + }); + }); + + // Activate form changes if required + var forms; + if (ams.warnOnFormChange) { + forms = $('FORM[data-ams-warn-on-change!="false"]', element); + } else { + forms = $('FORM[data-ams-warn-on-change="true"]', element); + } + forms.each(function() { + var form = $(this), + formChangedCallback = form.data('ams-form-changed-callback') || + ams.formChangedCallback; + $('INPUT[type="text"], ' + + 'INPUT[type="checkbox"], ' + + 'INPUT[type="radio"], ' + + 'SELECT, ' + + 'TEXTAREA, ' + + '[data-ams-changed-event]', form).each(function() { + var source = $(this); + if (source.data('ams-ignore-change') !== true) { + var event = source.data('ams-changed-event') || 'change'; + source.on(event, function () { + ams.form.setChanged(form); + ams.executeFunctionByName(formChangedCallback, form, source); + }); + } + }); + form.on('reset', function() { + ams.form.resetChanged($(this)); + }); + }); + }, + + /** + * Set focus to first container input + */ + setFocus: function(container) { + var focused = $('[data-ams-focus-target]', container).first(); + if (!focused.exists()) { + focused = $('input, select', container).first(); + } + if (focused.exists()) { + if (focused.hasClass('select2-input')) { + focused = focused.parents('.select2'); + } + if (focused.hasClass('select2')) { + setTimeout(function() { + focused.select2('focus'); + if (focused.data('ams-focus-open') === true) { + focused.select2('open'); + } + }, 100); + } else { + focused.focus(); + } + } + }, + + /** + * Check for modified forms before exiting + */ + checkBeforeUnload: function() { + var forms = $('FORM[data-ams-form-changed="true"]'); + if (forms.exists()) { + return ams.i18n.FORM_CHANGED_WARNING; + } + }, + + /** + * Check for modified forms before loading new inner content + */ + confirmChangedForm: function(element, callback, cancelCallback) { + if (typeof(element) === 'function') { + callback = element; + element = undefined; + } + var forms = $('FORM[data-ams-form-changed="true"]', element); + if (forms.exists()) { + if (cancelCallback) { + if (globals.confirm(ams.i18n.FORM_CHANGED_WARNING, ams.i18n.WARNING)) { + callback.call(element); + } else { + cancelCallback.call(element); + } + } else { + ams.skin && ams.skin.bigBox({ + title: ams.i18n.WARNING, + content: '  ' + ams.i18n.FORM_CHANGED_WARNING, + buttons: ams.i18n.BTN_OK_CANCEL + }, function(button) { + if (button === ams.i18n.BTN_OK) { + callback.call(element); + } + }); + } + } else { + callback.call(element); + } + }, + + /** + * Update form "chenged" status flag + */ + setChanged: function(form) { + form.attr('data-ams-form-changed', true); + }, + + /** + * Reset form changed flag + */ + resetChanged: function(form) { + if (form !== undefined) { + $(form).removeAttr('data-ams-form-changed'); + } + }, + + /** + * Submit given form + */ + submit: function(form, handler, submitOptions) { + // Check params + form = $(form); + if (!form.exists()) { + return false; + } + if (typeof(handler) === 'object') { + submitOptions = handler; + handler = undefined; + } + // Prevent multiple submits of the same form + if (form.data('submitted')) { + if (!form.data('ams-form-hide-submitted')) { + ams.skin && ams.skin.messageBox('warning', { + title: ams.i18n.WAIT, + content: ams.i18n.FORM_SUBMITTED, + icon: 'fa fa-save shake animated', + timeout: form.data('ams-form-alert-timeout') || 5000 + }); + } + return false; + } + // Check submit validators + if (ams.form && !ams.form._checkSubmitValidators(form)) { + return false; + } + // Remove remaining status messages + $('.alert-danger, SPAN.state-error', form).not('.persistent').remove(); + $('.state-error', form).removeClassPrefix('state-'); + // Check submit button + var button = $(form.data('ams-submit-button')); + if (button && !button.data('ams-form-hide-loading')) { + button.data('ams-progress-content', button.html()); + button.button('loading'); + } + ams.ajax && ams.ajax.check($.fn.ajaxSubmit, + ams.baseURL + 'ext/jquery-form-3.49' + ams.devext + '.js', + function() { + + function _submitAjaxForm(form, options) { + + var button, + buttonData, + buttonTarget, + data = form.data(), + formOptions = data.amsFormOptions, + formData, + formDataCallback; + + var progressHandler, + progressInterval, + progressCallback, + progressEndCallback; + + // Inner progress status handler + function _getProgress(handler, progress_id) { + + var timeout; + + function _clearProgressStatus() { + clearTimeout(timeout); + ams.form.resetAfterSubmit(form, button); + button.html(button.data('ams-progress-content')); + ams.executeFunctionByName(progressEndCallback, form, button); + ams.form.resetChanged(form); + } + + function _getProgressStatus() { + ams.ajax && ams.ajax.post(handler, + {progress_id: progress_id}, + {error: _clearProgressStatus}, + ams.getFunctionByName(progressCallback) || function(result, status) { + if (status === 'success') { + if (result.status === 'running') { + if (result.message) { + button.text(result.message); + } else { + var text = button.data('ams-progress-text') || ams.i18n.PROGRESS; + if (result.current) { + text += ': ' + result.current + '/ ' + (result.length || 100); + } else { + text += '...'; + } + button.text(text); + } + timeout = setTimeout(_getProgressStatus, progressInterval); + } else if (result.status === 'finished') { + _clearProgressStatus(); + } + } else { + _clearProgressStatus(); + } + }); + } + + button.button('loading'); + timeout = setTimeout(_getProgressStatus, progressInterval); + } + + // Initialize form data + if (submitOptions) { + formDataCallback = submitOptions.formDataInitCallback; + } + if (formDataCallback) { + delete submitOptions.formDataInitCallback; + } else { + formDataCallback = data.amsFormDataInitCallback; + } + if (formDataCallback) { + var veto = {}; + formData = ams.executeFunctionByName(formDataCallback, form, veto); + if (veto.veto) { + button = form.data('ams-submit-button'); + if (button) { + button.button('reset'); + } + ams.form.finalizeSubmitFooter.call(form); + return false; + } + } else { + formData = data.amsFormData || {}; + } + + // Check submit button for custom action handler and target + button = $(form.data('ams-submit-button')); + if (button && button.exists()) { + buttonData = button.data(); + buttonTarget = buttonData.amsFormSubmitTarget; + } else { + buttonData = {}; + } + + // Check action URL + var url; + var formHandler = handler || buttonData.amsFormHandler || data.amsFormHandler || ''; + if (formHandler.startsWith(window.location.protocol)) { + url = formHandler; + } else { + var action = buttonData.amsFormAction || form.attr('action').replace(/#/, ''); + if (action.startsWith(window.location.protocol)) { + url = action; + } else { + url = ams.ajax && (ams.ajax.getAddr() + action); + } + url += formHandler; + } + progressHandler = buttonData.amsProgressHandler || data.amsProgressHandler || ''; + progressInterval = buttonData.amsProgressInterval || data.amsProgressInterval || 1000; + progressCallback = buttonData.amsProgressCallback || data.amsProgressCallback; + progressEndCallback = buttonData.amsProgressEndCallback || data.amsProgressEndCallback; + + // Initialize submit target with AJAX indicator + var target = null; + if (submitOptions && submitOptions.initSubmitTarget) { + ams.executeFunctionByName(submitOptions.initSubmitTarget, form); + } else { + if (data.amsFormInitSubmitTarget) { + target = $(buttonTarget || data.amsFormSubmitTarget || '#content'); + ams.executeFunctionByName(data.amsFormInitSubmit || 'MyAMS.form.initSubmit', form, target); + } else if (!data.amsFormHideSubmitFooter) { + ams.executeFunctionByName(data.amsFormInitSubmit || 'MyAMS.form.initSubmitFooter', form); + } + } + + // Complete form data + if (submitOptions) { + formData = $.extend({}, formData, submitOptions.form_data); + } + + // Check progress handler + var hasUpload; + if (progressHandler) { + formData.progress_id = ams.generateUUID(); + } else { + // Check progress meter via Apache progress module + hasUpload = typeof (options.uuid) !== 'undefined'; + if (hasUpload) { + if (url.indexOf('X-Progress-ID') < 0) { + url += "?X-Progress-ID=" + options.uuid; + } + delete options.uuid; + } + } + + // Initialize default AJAX settings + var defaults = { + url: url, + type: 'post', + cache: false, + data: formData, + dataType: data.amsFormDatatype, + beforeSerialize: function(/*form, options*/) { + form.trigger('myams.form.before-serialize'); + if (typeof (globals.tinyMCE) !== 'undefined') { + globals.tinyMCE.triggerSave(); + } + }, + beforeSubmit: function(data, form /*, options*/) { + form.trigger('myams.form.before-submit', [data]); + form.data('submitted', true); + if (form.data('ams-form-reset-before-submit')) { + setTimeout(function() { + ams.form.resetAfterSubmit(form); + }, 250); + } + }, + error: function(request, status, error, form) { + form.trigger('myams.form.submit-error', [request, status, error]); + if (target) { + ams.executeFunctionByName(data.amsFormSubmitError || 'MyAMS.form.finalizeSubmitOnError', form, target); + } + ams.form.resetAfterSubmit(form); + }, + iframe: hasUpload + }; + + // Initialize IFrame for custom download target + var downloadTarget = (submitOptions && submitOptions.downloadTarget) || data.amsFormDownloadTarget; + if (downloadTarget) { + var iframe = $('iframe[name="' + downloadTarget + '"]'); + if (!iframe.exists()) { + iframe = $('').hide() + .attr('name', downloadTarget) + .appendTo($('body')); + } + defaults = $.extend({}, defaults, { + iframe: true, + iframeTarget: iframe, + success: function(result, status, request, form) { + form.trigger('myams.form.after-submit', [result, status, request]); + var modal = $(form).parents('.modal-dialog'); + if (modal.exists()) { + ams.dialog && ams.dialog.close(form); + } else { + var callback, + button = form.data('ams-submit-button'); + if (button) { + callback = button.data('ams-form-submit-callback'); + } + if (!callback) { + callback = ams.getFunctionByName(data.amsFormSubmitCallback) || ams.form._submitCallback; + } + try { + callback.call(form, result, status, request, form); + } finally { + ams.form.resetAfterSubmit(form); + ams.form.resetChanged(form); + } + } + } + }); + } else { + defaults = $.extend({}, defaults, { + error: function(request, status, error, form) { + if (target) { + ams.executeFunctionByName(data.amsFormSubmitError || 'MyAMS.form.finalizeSubmitOnError', form, target); + } + ams.form.resetAfterSubmit(form); + }, + success: function(result, status, request, form) { + form.trigger('myams.form.after-submit', [result, status, request]); + var callback, + button = form.data('ams-submit-button'); + if (button) { + callback = button.data('ams-form-submit-callback'); + } + if (!callback) { + callback = ams.getFunctionByName(data.amsFormSubmitCallback) || ams.form._submitCallback; + } + try { + callback.call(form, result, status, request, form); + } finally { + ams.form.resetAfterSubmit(form); + ams.form.resetChanged(form); + } + }, + iframe: hasUpload + }); + } + var settings = $.extend({}, defaults, options, formOptions, submitOptions); + + // Initialize progress handler + if (progressHandler) { + _getProgress(progressHandler, formData.progress_id); + } + + // Submit form + $(form).ajaxSubmit(settings); + + // If external download target is specified, reset form submit button and footer + if (downloadTarget) { + var modal = $(form).parents('.modal-dialog'); + var keepModal = modal.exists() && button.exists() && button.data('ams-keep-modal'); + if (modal.exists() && (keepModal !== true)) { + ams.dialog && ams.dialog.close(form); + } else { + if (!progressHandler) { + setTimeout(function() { + ams.form.resetAfterSubmit(form, button); + ams.form.resetChanged(form); + }, button.data('ams-form-reset-timeout') || 2000); + } + } + } + } + + var hasUpload = (form.data('ams-form-ignore-uploads') !== true) && + ($('INPUT[type="file"]', form).length > 0); + if (hasUpload) { + // JQuery-progressbar plug-in must be loaded synchronously!! + // Otherwise, hidden input fields created by jquery-validate plug-in + // and matching named buttons will be deleted (on first form submit) + // before JQuery-form plug-in can get them when submitting the form... + ams.ajax && ams.ajax.check($.progressBar, + ams.baseURL + 'ext/jquery-progressbar' + ams.devext + '.js'); + var settings = $.extend({}, { + uuid: $.progressBar.submit(form) + }); + _submitAjaxForm(form, settings); + } else { + _submitAjaxForm(form, {}); + } + }); + return false; + }, + + /** + * Initialize AJAX submit call + * + * @param this: the submitted form + * @param target: the form submit container target + * @param message: the optional message + */ + initSubmit: function(target, message) { + var form = $(this), + spin = ''; + if (!message) { + message = form.data('ams-form-submit-message'); + } + if (message) { + spin += '' + message + ''; + } + $(target).html('
' + spin + '
'); + $(target).parents('.hidden').removeClass('hidden'); + }, + + /** + * Reset form status after submit + * + * @param form: the submitted form + */ + resetAfterSubmit: function(form) { + if (form.data('submitted')) { + if (form.is(':visible')) { + var button = form.data('ams-submit-button'); + if (button) { + button.button('reset'); + } + ams.form.finalizeSubmitFooter.call(form); + } + form.data('submitted', false); + form.removeData('ams-submit-button'); + form.trigger('myams.form.after-reset'); + } + }, + + /** + * Finalize AJAX submit call + * + * @param target: the form submit container target + */ + finalizeSubmitOnError: function(target) { + $('i', target).removeClass('fa-spin') + .removeClass('fa-gear') + .addClass('fa-ambulance'); + }, + + /** + * Initialize AJAX submit call in form footer + * + * @param this: the submitted form + * @param message: the optional submit message + */ + initSubmitFooter: function(message) { + var form = $(this), + spin = ''; + if (!message) { + message = $(this).data('ams-form-submit-message'); + } + if (message) { + spin += '' + message + ''; + } + var footer = $('footer', form); + $('button', footer).hide(); + footer.append('
' + spin + '
'); + }, + + /** + * Finalize AJAX submit call + * + * @param this: the submitted form + * @param target: the form submit container target + */ + finalizeSubmitFooter: function(/*target*/) { + var form = $(this), + footer = $('footer', form); + if (footer) { + $('.row', footer).remove(); + $('button', footer).show(); + } + }, + + /** + * Handle AJAX submit results + * + * Submit results are auto-detected via response content type, except when this content type + * is specified into form's data attributes. + * Submit response can be of several content types: + * - html or text: the response is directly included into a "target" container (#content by default) + * - json: a "status" attribute indicates how the request was handled and how the response should be + * treated: + * - error: indicates that an error occured; other response attributes indicate error messages + * - success: basic success, no other action is requested + * - callback: only call given function to handle the result + * - callbacks: only call given set of functions to handle the result + * - reload: page's body should be reloaded from a given URL + * - redirect: redirect browser to given URL + * Each JSON response can also specify an HTML content, a message and a callback ( + */ + _submitCallback: function(result, status, request, form) { + + var button; + if (form.is(':visible')) { + ams.form.finalizeSubmitFooter.call(form); + button = form.data('ams-submit-button'); + if (button) { + button.button('reset'); + } + } + + var data = form.data(), + dataType; + if (data.amsFormDatatype) { + dataType = data.amsFormDatatype; + } else { + var response = ams.ajax && ams.ajax.getResponse(request); + if (response) { + dataType = response.contentType; + result = response.data; + } + } + + var target; + if (button) { + target = $(button.data('ams-form-submit-target') || data.amsFormSubmitTarget || '#content'); + } else { + target = $(data.amsFormSubmitTarget || '#content'); + } + + switch (dataType) { + case 'json': + ams.ajax && ams.ajax.handleJSON(result, form, target); + break; + case 'script': + break; + case 'xml': + break; + case 'html': + /* falls through */ + case 'text': + /* falls through */ + default: + ams.form.resetChanged(form); + if (button && (button.data('ams-keep-modal') !== true)) { + ams.dialog && ams.dialog.close(form); + } + if (!target.exists()) { + target = $('body'); + } + target.parents('.hidden').removeClass('hidden'); + $('.alert', target.parents('.alerts-container')).remove(); + target.css({opacity: '0.0'}) + .html(result) + .delay(50) + .animate({opacity: '1.0'}, 300); + ams.initContent && ams.initContent(target); + ams.form.setFocus(target); + } + var callback = request.getResponseHeader('X-AMS-Callback'); + if (callback) { + var options = request.getResponseHeader('X-AMS-Callback-Options'); + ams.executeFunctionByName(callback, form, options === undefined ? {} : JSON.parse(options), request); + } + }, + + /** + * Get list of custom validators called before submit + */ + _getSubmitValidators: function(form) { + var validators = []; + var formValidator = form.data('ams-form-validator'); + if (formValidator) { + validators.push([form, formValidator]); + } + $('[data-ams-form-validator]', form).each(function() { + var source = $(this); + validators.push([source, source.data('ams-form-validator')]); + }); + return validators; + }, + + /** + * Call list of custom validators before submit + * + * Each validator can return: + * - a boolean 'false' value to just specify that an error occured + * - a string value containing an error message + * - an array containing a list of string error messages + * Any other value (undefined, null, True...) will lead to a successful submit. + */ + _checkSubmitValidators: function(form) { + var validators = ams.form._getSubmitValidators(form); + if (!validators.length) { + return true; + } + var output = [], + result = true; + for (var index=0; index < validators.length; index++) { + var validator = validators[index], + source = validator[0], + handler = validator[1], + validatorResult = ams.executeFunctionByName(handler, form, source); + if (validatorResult === false) { + result = false; + } else if (typeof(validatorResult) === 'string') { + output.push(validatorResult); + } else if (result.length && (result.length > 0)) { + output = output.concat(result); + } + } + if (output.length > 0) { + var header = output.length === 1 ? ams.i18n.ERROR_OCCURED : ams.i18n.ERRORS_OCCURED; + ams.skin && ams.skin.alert(form, 'danger', header, output); + return false; + } else { + return result; + } + }, + + /** + * Display JSON errors + * JSON errors should be defined in an object as is: + * {status: 'error', + * error_message: "Main error message", + * messages: ["Message 1", "Message 2",...] + * widgets: [{label: "First widget name", + * name: "field-name-1", + * message: "Error message"}, + * {label: "Second widget name", + * name: "field-name-2", + * message: "Second error message"},...]} + */ + showErrors: function(form, errors) { + var header; + if (typeof(errors) === 'string') { + ams.skin && ams.skin.alert(form, 'error', ams.i18n.ERROR_OCCURED, errors); + } else if (errors instanceof Array) { + header = errors.length === 1 ? ams.i18n.ERROR_OCCURED : ams.i18n.ERRORS_OCCURED; + ams.skin && ams.skin.alert(form, 'error', header, errors); + } else { + $('.state-error', form).removeClass('state-error'); + header = errors.error_header || + (errors.widgets && (errors.widgets.length > 1) ? ams.i18n.ERRORS_OCCURED : ams.i18n.ERROR_OCCURED); + var message = []; + var index; + if (errors.messages) { + for (index = 0; index < errors.messages.length; index++) { + var msg = errors.messages[index]; + if (msg.header) { + message.push('' + msg.header + '
' + msg.message); + } else { + message.push(msg.message || msg); + } + } + } + if (errors.widgets) { + for (index = 0; index < errors.widgets.length; index++) { + // set widget status message + var widgetData = errors.widgets[index]; + var widget = $('[name="' + widgetData.name + '"]', form); + if (!widget.exists()) { + widget = $('[name="' + widgetData.name + ':list"]', form); + } + if (widget.exists()) { + // Update widget state + widget.parents('label, .input') + .first() + .removeClassPrefix('state-') + .addClass('state-error') + .after('' + widgetData.message + ''); + } else { + // complete form alert message + if (widgetData.label) { + message.push(widgetData.label + ' : ' + widgetData.message); + } + } + // mark parent tab (if any) with error status + var tabIndex = widget.parents('.tab-pane').index() + 1; + if (tabIndex > 0) { + var navTabs = $('.nav-tabs', $(widget).parents('.tabforms')); + $('li:nth-child(' + tabIndex + ')', navTabs).removeClassPrefix('state-') + .addClass('state-error'); + $('li.state-error:first a', form).click(); + } + } + } + ams.skin && ams.skin.alert($('.form-group:first', form), errors.error_level || 'error', header, message, errors.error_message); + } + } + }; + +})(jQuery, this);