From 1e03a240b528afb065de9db1ae2fc328fea24a1b Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Thu, 18 Feb 2021 12:04:58 +0100 Subject: [PATCH] MDL-64554 form: reset form change checker before triggering submit event --- lib/form/amd/build/modalform.min.js | 2 +- lib/form/amd/build/modalform.min.js.map | 2 +- lib/form/amd/src/modalform.js | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) rewrite lib/form/amd/build/modalform.min.js.map (67%) diff --git a/lib/form/amd/build/modalform.min.js b/lib/form/amd/build/modalform.min.js index 2d8efbae64e..88b69d73d9a 100644 --- a/lib/form/amd/build/modalform.min.js +++ b/lib/form/amd/build/modalform.min.js @@ -1,2 +1,2 @@ -define ("core_form/modalform",["exports","core/modal_factory","core/modal_events","core/ajax","core/notification","core/yui","core/event","core/fragment","core/pending"],function(a,b,c,d,e,f,g,h,i){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=j(b);c=j(c);d=j(d);e=j(e);f=j(f);g=j(g);h=j(h);i=j(i);function j(a){return a&&a.__esModule?a:{default:a}}function k(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function l(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){k(h,d,e,f,g,"next",a)}function g(a){k(h,d,e,f,g,"throw",a)}f(void 0)})}}function m(a){return q(a)||p(a)||o(a)||n()}function n(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function o(a,b){if(!a)return;if("string"==typeof a)return r(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return r(a,b)}function p(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function q(a){if(Array.isArray(a))return r(a)}function r(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Display a form in a modal dialogue\n *\n * Example:\n * import ModalForm from 'core_form/modalform';\n *\n * const modalForm = new ModalForm({\n * formClass: 'pluginname\\\\form\\\\formname',\n * modalConfig: {title: 'Here comes the title'},\n * args: {categoryid: 123},\n * returnFocus: e.target,\n * });\n * modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (c) => window.console.log(c.detail));\n * modalForm.show();\n *\n * See also https://docs.moodle.org/dev/Modal_and_AJAX_forms\n *\n * @module core_form/modalform\n * @package core_form\n * @copyright 2018 Mitxel Moriana \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport Y from 'core/yui';\nimport Event from 'core/event';\nimport Fragment from 'core/fragment';\nimport Pending from 'core/pending';\n\nexport default class ModalForm {\n\n /**\n * Various events that can be observed.\n *\n * @type {Object}\n */\n events = {\n // Form was successfully submitted - the response is passed to the event listener.\n // Cancellable (but it's hardly ever needed to cancel this event).\n FORM_SUBMITTED: 'core_form_modalform_formsubmitted',\n // Cancel button was pressed.\n // Cancellable (but it's hardly ever needed to cancel this event).\n FORM_CANCELLED: 'core_form_modalform_formcancelled',\n // User attempted to submit the form but there was client-side validation error.\n CLIENT_VALIDATION_ERROR: 'core_form_modalform_clientvalidationerror',\n // User attempted to submit the form but server returned validation error.\n SERVER_VALIDATION_ERROR: 'core_form_modalform_validationerror',\n // Error occurred while performing request to the server.\n // Cancellable (by default calls Notification.exception).\n ERROR: 'core_form_modalform_error',\n // Right after user pressed no-submit button,\n // listen to this event if you want to add JS validation or processing for no-submit button.\n // Cancellable.\n NOSUBMIT_BUTTON_PRESSED: 'core_form_modalform_nosubmitbutton',\n // Right after user pressed submit button,\n // listen to this event if you want to add additional JS validation or confirmation dialog.\n // Cancellable.\n SUBMIT_BUTTON_PRESSED: 'core_form_modalform_submitbutton',\n // Right after user pressed cancel button,\n // listen to this event if you want to add confirmation dialog.\n // Cancellable.\n CANCEL_BUTTON_PRESSED: 'core_form_modalform_cancelbutton',\n // Modal was loaded and this.modal is available (but the form content may not be loaded yet).\n LOADED: 'core_form_modalform_loaded',\n };\n\n /**\n * Constructor\n *\n * Shows the required form inside a modal dialogue\n *\n * @param {Object} config parameters for the form and modal dialogue:\n * @property {String} config.formClass PHP class name that handles the form (should extend \\core_form\\modal )\n * @property {Object} config.modalConfig modal config - title, type, etc.\n * Default: {removeOnClose: true, type: ModalFactory.types.SAVE_CANCEL}\n * @property {Object} config.args Arguments for the initial form rendering (for example, id of the edited entity)\n * @property {String} config.saveButtonText the text to display on the Modal \"Save\" button (optional)\n * @property {String} config.saveButtonClasses additional CSS classes for the Modal \"Save\" button\n * @property {HTMLElement} config.returnFocus element to return focus to after the dialogue is closed\n */\n constructor(config) {\n this.modal = null;\n this.config = config;\n this.config.modalConfig = {\n removeOnClose: true,\n type: ModalFactory.types.SAVE_CANCEL,\n large: true,\n ...(this.config.modalConfig || {}),\n };\n this.config.args = this.config.args || {};\n this.futureListeners = [];\n }\n\n /**\n * Initialise the modal and shows it\n *\n * @return {Promise}\n */\n show() {\n const pendingPromise = new Pending('core_form/modalform:init');\n return ModalFactory.create(this.config.modalConfig)\n .then((modal) => {\n this.modal = modal;\n\n // Retrieve the form and set the modal body. We can not set the body in the modalConfig,\n // we need to make sure that the modal already exists when we render the form. Some form elements\n // such as date_selector inspect the existing elements on the page to find the highest z-index.\n const formParams = new URLSearchParams(Object.entries(this.config.args || {}));\n this.modal.setBodyContent(this.getBody(formParams.toString()));\n\n // After successfull submit, when we press \"Cancel\" or close the dialogue by clicking on X in the top right corner.\n this.modal.getRoot().on(ModalEvents.hidden, () => {\n this.notifyResetFormChanges()\n .then(() => {\n this.modal.destroy();\n // Focus on the element that actually launched the modal.\n if (this.config.returnFocus) {\n this.config.returnFocus.focus();\n }\n return null;\n })\n .catch(() => null);\n });\n\n // Add the class to the modal dialogue.\n this.modal.getModal().addClass('modal-form-dialogue');\n\n // We catch the press on submit buttons in the forms.\n this.modal.getRoot().on('click', 'form input[type=submit][data-no-submit]',\n (e) => {\n e.preventDefault();\n const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target);\n if (!event.defaultPrevented) {\n this.processNoSubmitButton(e.target);\n }\n });\n\n // We catch the form submit event and use it to submit the form with ajax.\n this.modal.getRoot().on('submit', 'form', (e) => {\n e.preventDefault();\n const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED);\n if (!event.defaultPrevented) {\n this.submitFormAjax();\n }\n });\n\n // Change the text for the save button.\n if (typeof this.config.saveButtonText !== 'undefined' &&\n typeof this.modal.setSaveButtonText !== 'undefined') {\n this.modal.setSaveButtonText(this.config.saveButtonText);\n }\n // Set classes for the save button.\n if (typeof this.config.saveButtonClasses !== 'undefined') {\n this.setSaveButtonClasses(this.config.saveButtonClasses);\n }\n // When Save button is pressed - submit the form.\n this.modal.getRoot().on(ModalEvents.save, (e) => {\n e.preventDefault();\n this.modal.getRoot().find('form').submit();\n });\n\n // When Cancel button is pressed - allow to intercept.\n this.modal.getRoot().on(ModalEvents.cancel, (e) => {\n const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED);\n if (event.defaultPrevented) {\n e.preventDefault();\n }\n });\n this.futureListeners.forEach(args => this.modal.getRoot()[0].addEventListener(...args));\n this.futureListeners = [];\n this.trigger(this.events.LOADED, null, false);\n return this.modal.show();\n })\n .then(pendingPromise.resolve);\n }\n\n /**\n * Triggers a custom event\n *\n * @private\n * @param {String} eventName\n * @param {*} detail\n * @param {Boolean} cancelable\n * @return {CustomEvent}\n */\n trigger(eventName, detail = null, cancelable = true) {\n const e = new CustomEvent(eventName, {detail, cancelable});\n this.modal.getRoot()[0].dispatchEvent(e);\n return e;\n }\n\n /**\n * Add listener for an event\n *\n * Example:\n * const modalForm = new ModalForm(...);\n * dynamicForm.addEventListener(modalForm.events.FORM_SUBMITTED, e => {\n * window.console.log(e.detail);\n * });\n */\n addEventListener(...args) {\n if (!this.modal) {\n this.futureListeners.push(args);\n } else {\n this.modal.getRoot()[0].addEventListener(...args);\n }\n }\n\n /**\n * Get form contents (to be used in ModalForm.setBodyContent())\n *\n * @param {String} formDataString form data in format of a query string\n * @method getBody\n * @private\n * @return {Promise}\n */\n getBody(formDataString) {\n const params = {\n formdata: formDataString,\n form: this.config.formClass\n };\n const pendingPromise = new Pending('core_form/modalform:form_body');\n return Ajax.call([{\n methodname: 'core_form_dynamic_form',\n args: params\n }])[0]\n .then(response => {\n pendingPromise.resolve();\n return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)};\n });\n }\n\n /**\n * On exception during form processing. Caller may override\n *\n * @param {Object} exception\n */\n onSubmitError(exception) {\n const event = this.trigger(this.events.ERROR, exception);\n if (event.defaultPrevented) {\n return;\n }\n\n Notification.exception(exception);\n }\n\n /**\n * Notifies listeners that form dirty state should be reset.\n *\n * @return {Promise}\n */\n notifyResetFormChanges() {\n return new Promise(resolve => {\n Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', () => {\n Event.notifyFormSubmitAjax(this.modal.getRoot().find('form')[0], true);\n M.core_formchangechecker.reset_form_dirty_state();\n resolve();\n });\n });\n }\n\n /**\n * Wrapper for Event.notifyFormSubmitAjax that waits for the module to load\n *\n * We often destroy the form right after calling this function and we need to make sure that it actually\n * completes before it, or otherwise it will try to work with a form that does not exist.\n *\n * @param {Boolean} skipValidation\n * @return {Promise}\n */\n notifyFormSubmitAjax(skipValidation = false) {\n return new Promise(resolve => {\n Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', () => {\n Event.notifyFormSubmitAjax(this.modal.getRoot().find('form')[0], skipValidation);\n resolve();\n });\n });\n }\n\n /**\n * Click on a \"submit\" button that is marked in the form as registerNoSubmitButton()\n *\n * @param {Element} button button that was pressed\n */\n processNoSubmitButton(button) {\n this.notifyFormSubmitAjax(true)\n .then(() => {\n // Add the button name to the form data and submit it.\n let formData = this.modal.getRoot().find('form').serialize();\n formData = formData + '&' + encodeURIComponent(button.getAttribute('name')) + '=' +\n encodeURIComponent(button.getAttribute('value'));\n this.modal.setBodyContent(this.getBody(formData));\n return null;\n })\n .catch(null);\n }\n\n /**\n * Validate form elements\n * @return {Promise} promise that returns true if client-side validation has passed, false if there are errors\n */\n validateElements() {\n return this.notifyFormSubmitAjax()\n .then(() => {\n // Now the change events have run, see if there are any \"invalid\" form fields.\n /** @var {jQuery} list of elements with errors */\n const invalid = this.modal.getRoot().find('[aria-invalid=\"true\"], .error');\n\n // If we found invalid fields, focus on the first one and do not submit via ajax.\n if (invalid.length) {\n invalid.first().focus();\n return false;\n }\n\n return true;\n });\n }\n\n /**\n * Disable buttons during form submission\n */\n disableButtons() {\n this.modal.getFooter().find('[data-action]').attr('disabled', true);\n }\n\n /**\n * Enable buttons after form submission (on validation error)\n */\n enableButtons() {\n this.modal.getFooter().find('[data-action]').removeAttr('disabled');\n }\n\n /**\n * Submit the form via AJAX call to the core_form_dynamic_form WS\n */\n async submitFormAjax() {\n // If we found invalid fields, focus on the first one and do not submit via ajax.\n if (!await this.validateElements()) {\n this.trigger(this.events.CLIENT_VALIDATION_ERROR, null, false);\n return;\n }\n this.disableButtons();\n\n // Convert all the form elements values to a serialised string.\n const formData = this.modal.getRoot().find('form').serialize();\n\n // Now we can continue...\n Ajax.call([{\n methodname: 'core_form_dynamic_form',\n args: {\n formdata: formData,\n form: this.config.formClass\n }\n }])[0]\n .then((response) => {\n if (!response.submitted) {\n // Form was not submitted because validation failed.\n const promise = new Promise(\n resolve => resolve({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}));\n this.modal.setBodyContent(promise);\n this.enableButtons();\n this.trigger(this.events.SERVER_VALIDATION_ERROR);\n } else {\n // Form was submitted properly. Hide the modal and execute callback.\n const data = JSON.parse(response.data);\n const event = this.trigger(this.events.FORM_SUBMITTED, data);\n if (!event.defaultPrevented) {\n this.modal.hide();\n }\n return null;\n }\n return null;\n })\n .catch(this.onSubmitError);\n }\n\n /**\n * Set the classes for the 'save' button.\n *\n * @method setSaveButtonClasses\n * @param {(String)} value The 'save' button classes.\n */\n setSaveButtonClasses(value) {\n const button = this.modal.getFooter().find(\"[data-action='save']\");\n if (!button) {\n throw new Error(\"Unable to find the 'save' button\");\n }\n button.removeClass().addClass(value);\n }\n}\n"],"file":"modalform.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/modalform.js"],"names":["ModalForm","config","FORM_SUBMITTED","FORM_CANCELLED","CLIENT_VALIDATION_ERROR","SERVER_VALIDATION_ERROR","ERROR","NOSUBMIT_BUTTON_PRESSED","SUBMIT_BUTTON_PRESSED","CANCEL_BUTTON_PRESSED","LOADED","modal","modalConfig","removeOnClose","type","ModalFactory","types","SAVE_CANCEL","large","args","futureListeners","pendingPromise","Pending","create","then","formParams","URLSearchParams","Object","entries","setBodyContent","getBody","toString","getRoot","on","ModalEvents","hidden","notifyResetFormChanges","destroy","returnFocus","focus","catch","getModal","addClass","e","preventDefault","event","trigger","events","target","defaultPrevented","processNoSubmitButton","submitFormAjax","saveButtonText","setSaveButtonText","saveButtonClasses","setSaveButtonClasses","save","find","submit","cancel","forEach","addEventListener","show","resolve","eventName","detail","cancelable","CustomEvent","dispatchEvent","push","formDataString","params","formdata","form","formClass","Ajax","call","methodname","response","html","js","Fragment","processCollectedJavascript","javascript","exception","Notification","Promise","Y","use","Event","notifyFormSubmitAjax","M","core_formchangechecker","reset_form_dirty_state","skipValidation","button","formData","serialize","encodeURIComponent","getAttribute","invalid","length","first","getFooter","attr","removeAttr","validateElements","disableButtons","submitted","promise","enableButtons","data","JSON","parse","hide","onSubmitError","value","Error","removeClass"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Display a form in a modal dialogue\n *\n * Example:\n * import ModalForm from 'core_form/modalform';\n *\n * const modalForm = new ModalForm({\n * formClass: 'pluginname\\\\form\\\\formname',\n * modalConfig: {title: 'Here comes the title'},\n * args: {categoryid: 123},\n * returnFocus: e.target,\n * });\n * modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (c) => window.console.log(c.detail));\n * modalForm.show();\n *\n * See also https://docs.moodle.org/dev/Modal_and_AJAX_forms\n *\n * @module core_form/modalform\n * @package core_form\n * @copyright 2018 Mitxel Moriana \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport Y from 'core/yui';\nimport Event from 'core/event';\nimport Fragment from 'core/fragment';\nimport Pending from 'core/pending';\n\nexport default class ModalForm {\n\n /**\n * Various events that can be observed.\n *\n * @type {Object}\n */\n events = {\n // Form was successfully submitted - the response is passed to the event listener.\n // Cancellable (but it's hardly ever needed to cancel this event).\n FORM_SUBMITTED: 'core_form_modalform_formsubmitted',\n // Cancel button was pressed.\n // Cancellable (but it's hardly ever needed to cancel this event).\n FORM_CANCELLED: 'core_form_modalform_formcancelled',\n // User attempted to submit the form but there was client-side validation error.\n CLIENT_VALIDATION_ERROR: 'core_form_modalform_clientvalidationerror',\n // User attempted to submit the form but server returned validation error.\n SERVER_VALIDATION_ERROR: 'core_form_modalform_validationerror',\n // Error occurred while performing request to the server.\n // Cancellable (by default calls Notification.exception).\n ERROR: 'core_form_modalform_error',\n // Right after user pressed no-submit button,\n // listen to this event if you want to add JS validation or processing for no-submit button.\n // Cancellable.\n NOSUBMIT_BUTTON_PRESSED: 'core_form_modalform_nosubmitbutton',\n // Right after user pressed submit button,\n // listen to this event if you want to add additional JS validation or confirmation dialog.\n // Cancellable.\n SUBMIT_BUTTON_PRESSED: 'core_form_modalform_submitbutton',\n // Right after user pressed cancel button,\n // listen to this event if you want to add confirmation dialog.\n // Cancellable.\n CANCEL_BUTTON_PRESSED: 'core_form_modalform_cancelbutton',\n // Modal was loaded and this.modal is available (but the form content may not be loaded yet).\n LOADED: 'core_form_modalform_loaded',\n };\n\n /**\n * Constructor\n *\n * Shows the required form inside a modal dialogue\n *\n * @param {Object} config parameters for the form and modal dialogue:\n * @property {String} config.formClass PHP class name that handles the form (should extend \\core_form\\modal )\n * @property {Object} config.modalConfig modal config - title, type, etc.\n * Default: {removeOnClose: true, type: ModalFactory.types.SAVE_CANCEL}\n * @property {Object} config.args Arguments for the initial form rendering (for example, id of the edited entity)\n * @property {String} config.saveButtonText the text to display on the Modal \"Save\" button (optional)\n * @property {String} config.saveButtonClasses additional CSS classes for the Modal \"Save\" button\n * @property {HTMLElement} config.returnFocus element to return focus to after the dialogue is closed\n */\n constructor(config) {\n this.modal = null;\n this.config = config;\n this.config.modalConfig = {\n removeOnClose: true,\n type: ModalFactory.types.SAVE_CANCEL,\n large: true,\n ...(this.config.modalConfig || {}),\n };\n this.config.args = this.config.args || {};\n this.futureListeners = [];\n }\n\n /**\n * Initialise the modal and shows it\n *\n * @return {Promise}\n */\n show() {\n const pendingPromise = new Pending('core_form/modalform:init');\n return ModalFactory.create(this.config.modalConfig)\n .then((modal) => {\n this.modal = modal;\n\n // Retrieve the form and set the modal body. We can not set the body in the modalConfig,\n // we need to make sure that the modal already exists when we render the form. Some form elements\n // such as date_selector inspect the existing elements on the page to find the highest z-index.\n const formParams = new URLSearchParams(Object.entries(this.config.args || {}));\n this.modal.setBodyContent(this.getBody(formParams.toString()));\n\n // After successfull submit, when we press \"Cancel\" or close the dialogue by clicking on X in the top right corner.\n this.modal.getRoot().on(ModalEvents.hidden, () => {\n this.notifyResetFormChanges()\n .then(() => {\n this.modal.destroy();\n // Focus on the element that actually launched the modal.\n if (this.config.returnFocus) {\n this.config.returnFocus.focus();\n }\n return null;\n })\n .catch(() => null);\n });\n\n // Add the class to the modal dialogue.\n this.modal.getModal().addClass('modal-form-dialogue');\n\n // We catch the press on submit buttons in the forms.\n this.modal.getRoot().on('click', 'form input[type=submit][data-no-submit]',\n (e) => {\n e.preventDefault();\n const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target);\n if (!event.defaultPrevented) {\n this.processNoSubmitButton(e.target);\n }\n });\n\n // We catch the form submit event and use it to submit the form with ajax.\n this.modal.getRoot().on('submit', 'form', (e) => {\n e.preventDefault();\n const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED);\n if (!event.defaultPrevented) {\n this.submitFormAjax();\n }\n });\n\n // Change the text for the save button.\n if (typeof this.config.saveButtonText !== 'undefined' &&\n typeof this.modal.setSaveButtonText !== 'undefined') {\n this.modal.setSaveButtonText(this.config.saveButtonText);\n }\n // Set classes for the save button.\n if (typeof this.config.saveButtonClasses !== 'undefined') {\n this.setSaveButtonClasses(this.config.saveButtonClasses);\n }\n // When Save button is pressed - submit the form.\n this.modal.getRoot().on(ModalEvents.save, (e) => {\n e.preventDefault();\n this.modal.getRoot().find('form').submit();\n });\n\n // When Cancel button is pressed - allow to intercept.\n this.modal.getRoot().on(ModalEvents.cancel, (e) => {\n const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED);\n if (event.defaultPrevented) {\n e.preventDefault();\n }\n });\n this.futureListeners.forEach(args => this.modal.getRoot()[0].addEventListener(...args));\n this.futureListeners = [];\n this.trigger(this.events.LOADED, null, false);\n return this.modal.show();\n })\n .then(pendingPromise.resolve);\n }\n\n /**\n * Triggers a custom event\n *\n * @private\n * @param {String} eventName\n * @param {*} detail\n * @param {Boolean} cancelable\n * @return {CustomEvent}\n */\n trigger(eventName, detail = null, cancelable = true) {\n const e = new CustomEvent(eventName, {detail, cancelable});\n this.modal.getRoot()[0].dispatchEvent(e);\n return e;\n }\n\n /**\n * Add listener for an event\n *\n * Example:\n * const modalForm = new ModalForm(...);\n * dynamicForm.addEventListener(modalForm.events.FORM_SUBMITTED, e => {\n * window.console.log(e.detail);\n * });\n */\n addEventListener(...args) {\n if (!this.modal) {\n this.futureListeners.push(args);\n } else {\n this.modal.getRoot()[0].addEventListener(...args);\n }\n }\n\n /**\n * Get form contents (to be used in ModalForm.setBodyContent())\n *\n * @param {String} formDataString form data in format of a query string\n * @method getBody\n * @private\n * @return {Promise}\n */\n getBody(formDataString) {\n const params = {\n formdata: formDataString,\n form: this.config.formClass\n };\n const pendingPromise = new Pending('core_form/modalform:form_body');\n return Ajax.call([{\n methodname: 'core_form_dynamic_form',\n args: params\n }])[0]\n .then(response => {\n pendingPromise.resolve();\n return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)};\n });\n }\n\n /**\n * On exception during form processing. Caller may override\n *\n * @param {Object} exception\n */\n onSubmitError(exception) {\n const event = this.trigger(this.events.ERROR, exception);\n if (event.defaultPrevented) {\n return;\n }\n\n Notification.exception(exception);\n }\n\n /**\n * Notifies listeners that form dirty state should be reset.\n *\n * @return {Promise}\n */\n notifyResetFormChanges() {\n return new Promise(resolve => {\n Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', () => {\n Event.notifyFormSubmitAjax(this.modal.getRoot().find('form')[0], true);\n M.core_formchangechecker.reset_form_dirty_state();\n resolve();\n });\n });\n }\n\n /**\n * Wrapper for Event.notifyFormSubmitAjax that waits for the module to load\n *\n * We often destroy the form right after calling this function and we need to make sure that it actually\n * completes before it, or otherwise it will try to work with a form that does not exist.\n *\n * @param {Boolean} skipValidation\n * @return {Promise}\n */\n notifyFormSubmitAjax(skipValidation = false) {\n return new Promise(resolve => {\n Y.use('event', 'moodle-core-event', 'moodle-core-formchangechecker', () => {\n Event.notifyFormSubmitAjax(this.modal.getRoot().find('form')[0], skipValidation);\n resolve();\n });\n });\n }\n\n /**\n * Click on a \"submit\" button that is marked in the form as registerNoSubmitButton()\n *\n * @param {Element} button button that was pressed\n */\n processNoSubmitButton(button) {\n this.notifyFormSubmitAjax(true)\n .then(() => {\n // Add the button name to the form data and submit it.\n let formData = this.modal.getRoot().find('form').serialize();\n formData = formData + '&' + encodeURIComponent(button.getAttribute('name')) + '=' +\n encodeURIComponent(button.getAttribute('value'));\n this.modal.setBodyContent(this.getBody(formData));\n return null;\n })\n .catch(null);\n }\n\n /**\n * Validate form elements\n * @return {Promise} promise that returns true if client-side validation has passed, false if there are errors\n */\n validateElements() {\n return this.notifyFormSubmitAjax()\n .then(() => {\n // Now the change events have run, see if there are any \"invalid\" form fields.\n /** @var {jQuery} list of elements with errors */\n const invalid = this.modal.getRoot().find('[aria-invalid=\"true\"], .error');\n\n // If we found invalid fields, focus on the first one and do not submit via ajax.\n if (invalid.length) {\n invalid.first().focus();\n return false;\n }\n\n return true;\n });\n }\n\n /**\n * Disable buttons during form submission\n */\n disableButtons() {\n this.modal.getFooter().find('[data-action]').attr('disabled', true);\n }\n\n /**\n * Enable buttons after form submission (on validation error)\n */\n enableButtons() {\n this.modal.getFooter().find('[data-action]').removeAttr('disabled');\n }\n\n /**\n * Submit the form via AJAX call to the core_form_dynamic_form WS\n */\n async submitFormAjax() {\n // If we found invalid fields, focus on the first one and do not submit via ajax.\n if (!await this.validateElements()) {\n this.trigger(this.events.CLIENT_VALIDATION_ERROR, null, false);\n return;\n }\n this.disableButtons();\n\n // Convert all the form elements values to a serialised string.\n const formData = this.modal.getRoot().find('form').serialize();\n\n // Now we can continue...\n Ajax.call([{\n methodname: 'core_form_dynamic_form',\n args: {\n formdata: formData,\n form: this.config.formClass\n }\n }])[0]\n .then((response) => {\n if (!response.submitted) {\n // Form was not submitted because validation failed.\n const promise = new Promise(\n resolve => resolve({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}));\n this.modal.setBodyContent(promise);\n this.enableButtons();\n this.trigger(this.events.SERVER_VALIDATION_ERROR);\n } else {\n // Form was submitted properly. Hide the modal and execute callback.\n const data = JSON.parse(response.data);\n return this.notifyResetFormChanges()\n .then(() => {\n const event = this.trigger(this.events.FORM_SUBMITTED, data);\n if (!event.defaultPrevented) {\n this.modal.hide();\n }\n return null;\n });\n }\n return null;\n })\n .catch(this.onSubmitError);\n }\n\n /**\n * Set the classes for the 'save' button.\n *\n * @method setSaveButtonClasses\n * @param {(String)} value The 'save' button classes.\n */\n setSaveButtonClasses(value) {\n const button = this.modal.getFooter().find(\"[data-action='save']\");\n if (!button) {\n throw new Error(\"Unable to find the 'save' button\");\n }\n button.removeClass().addClass(value);\n }\n}\n"],"file":"modalform.min.js"} \ No newline at end of file diff --git a/lib/form/amd/src/modalform.js b/lib/form/amd/src/modalform.js index 3b011f76a5a..285a37aab1d 100644 --- a/lib/form/amd/src/modalform.js +++ b/lib/form/amd/src/modalform.js @@ -381,11 +381,14 @@ export default class ModalForm { } else { // Form was submitted properly. Hide the modal and execute callback. const data = JSON.parse(response.data); - const event = this.trigger(this.events.FORM_SUBMITTED, data); - if (!event.defaultPrevented) { - this.modal.hide(); - } - return null; + return this.notifyResetFormChanges() + .then(() => { + const event = this.trigger(this.events.FORM_SUBMITTED, data); + if (!event.defaultPrevented) { + this.modal.hide(); + } + return null; + }); } return null; }) -- 2.11.4.GIT