Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"modalform.min.js","sources":["../src/modalform.js"],"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 <http://www.gnu.org/licenses/>.\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 * @copyright  2018 Mitxel Moriana <mitxel@tresipunt.>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport * as FormEvents from 'core_form/events';\nimport Fragment from 'core/fragment';\nimport ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {serialize} from './util';\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     * @paramy {String} config.formClass PHP class name that handles the form (should extend \\core_form\\modal )\n     * @paramy {String} config.moduleName module name to use if different to core/modal_save_cancel (optional)\n     * @paramy {Object} config.modalConfig modal config - title, header, footer, etc.\n     *              Default: {removeOnClose: true, large: true}\n     * @paramy {Object} config.args Arguments for the initial form rendering (for example, id of the edited entity)\n     * @paramy {String} config.saveButtonText the text to display on the Modal \"Save\" button (optional)\n     * @paramy {String} config.saveButtonClasses additional CSS classes for the Modal \"Save\" button\n     * @paramy {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            large: true,\n            ...(this.config.modalConfig || {}),\n        };\n        this.config.args = this.config.args || {};\n        this.futureListeners = [];\n    }\n\n    /**\n     * Loads the modal module and creates an instance\n     *\n     * @returns {Promise}\n     */\n    getModalModule() {\n        if (!this.config.moduleName && this.config.modalConfig.type && this.config.modalConfig.type !== 'SAVE_CANCEL') {\n            // Legacy loader for plugins that were not updated with Moodle 4.3 changes.\n            window.console.warn(\n                'Passing config.modalConfig.type to ModalForm has been deprecated since Moodle 4.3. ' +\n                'Please pass config.modalName instead with the full module name.',\n            );\n            return import('core/modal_factory')\n                .then((ModalFactory) => ModalFactory.create(this.config.modalConfig));\n        } else {\n            // New loader for Moodle 4.3 and above.\n            const moduleName = this.config.moduleName ?? 'core/modal_save_cancel';\n            return import(moduleName)\n                .then((module) => module.create(this.config.modalConfig));\n        }\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\n        return this.getModalModule()\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 = serialize(this.config.args || {});\n            const bodyContent = this.getBody(formParams);\n            this.modal.setBodyContent(bodyContent);\n            bodyContent.catch(Notification.exception);\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                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            });\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<unknown>}\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     * @param {array} args\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        .catch(exception => this.onSubmitError(exception));\n    }\n\n    /**\n     * On exception during form processing or initial rendering. 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     * @fires event:formSubmittedByJavascript\n     */\n    notifyResetFormChanges() {\n        const form = this.getFormNode();\n        if (!form) {\n            return;\n        }\n\n        FormEvents.notifyFormSubmittedByJavascript(form, true);\n\n        FormChangeChecker.resetFormDirtyState(form);\n    }\n\n    /**\n     * Get the form node from the Dialogue.\n     *\n     * @returns {HTMLFormElement}\n     */\n    getFormNode() {\n        return this.modal.getRoot().find('form')[0];\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     * @fires event:formSubmittedByJavascript\n     */\n    processNoSubmitButton(button) {\n        const form = this.getFormNode();\n        if (!form) {\n            return;\n        }\n\n        FormEvents.notifyFormSubmittedByJavascript(form, true);\n\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\n        const bodyContent = this.getBody(formData);\n        this.modal.setBodyContent(bodyContent);\n        bodyContent.catch(Notification.exception);\n    }\n\n    /**\n     * Validate form elements\n     * @return {Boolean} Whether client-side validation has passed, false if there are errors\n     * @fires event:formSubmittedByJavascript\n     */\n    validateElements() {\n        FormEvents.notifyFormSubmittedByJavascript(this.getFormNode());\n\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     * 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 (!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 form = this.modal.getRoot().find('form');\n        const formData = 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                FormChangeChecker.markFormSubmitted(form[0]);\n                const event = this.trigger(this.events.FORM_SUBMITTED, data);\n                if (!event.defaultPrevented) {\n                    this.modal.hide();\n                }\n            }\n            return null;\n        })\n        .catch(exception => {\n            this.enableButtons();\n            this.onSubmitError(exception);\n        });\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"],"names":["constructor","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","large","this","args","futureListeners","getModalModule","moduleName","type","window","console","warn","then","ModalFactory","create","module","show","pendingPromise","Pending","formParams","bodyContent","getBody","setBodyContent","catch","Notification","exception","getRoot","on","ModalEvents","hidden","notifyResetFormChanges","destroy","returnFocus","focus","getModal","addClass","e","preventDefault","trigger","events","target","defaultPrevented","processNoSubmitButton","submitFormAjax","saveButtonText","setSaveButtonText","saveButtonClasses","setSaveButtonClasses","save","find","submit","cancel","forEach","addEventListener","resolve","eventName","CustomEvent","detail","cancelable","dispatchEvent","push","formDataString","params","formdata","form","formClass","Ajax","call","methodname","response","html","js","Fragment","processCollectedJavascript","javascript","onSubmitError","getFormNode","FormEvents","notifyFormSubmittedByJavascript","FormChangeChecker","resetFormDirtyState","button","formData","serialize","encodeURIComponent","getAttribute","validateElements","invalid","length","first","disableButtons","getFooter","attr","enableButtons","removeAttr","submitted","data","JSON","parse","markFormSubmitted","hide","promise","Promise","value","Error","removeClass"],"mappings":"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"}