Proyectos de Subversion Moodle

Rev

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

{"version":3,"file":"dynamicform.min.js","sources":["../src/dynamicform.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 an embedded form, it is only loaded and reloaded inside its container\n *\n *\n * @module     core_form/dynamicform\n * @copyright  2019 Marina Glancy\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * See also https://docs.moodle.org/dev/Modal_and_AJAX_forms\n *\n * @example\n *    import DynamicForm from 'core_form/dynamicform';\n *\n *    const dynamicForm = new DynamicForm(document.querySelector('#mycontainer', 'pluginname\\\\form\\\\formname');\n *    dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => {\n *        e.preventDefault();\n *        window.console.log(e.detail);\n *        dynamicForm.container.innerHTML = 'Thank you, your form is submitted!';\n *    });\n *    dynamicForm.load();\n *\n */\n\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport * as FormEvents from 'core_form/events';\nimport Ajax from 'core/ajax';\nimport Fragment from 'core/fragment';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Templates from 'core/templates';\nimport {getStrings} from 'core/str';\nimport {serialize} from './util';\n\n/**\n * @class core_form/dynamicform\n */\nexport default class DynamicForm {\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 (in order to prevent default behavior to clear the container).\n        FORM_SUBMITTED: 'core_form_dynamicform_formsubmitted',\n        // Cancel button was pressed.\n        // Cancellable (in order to prevent default behavior to clear the container).\n        FORM_CANCELLED: 'core_form_dynamicform_formcancelled',\n        // User attempted to submit the form but there was client-side validation error.\n        CLIENT_VALIDATION_ERROR: 'core_form_dynamicform_clientvalidationerror',\n        // User attempted to submit the form but server returned validation error.\n        SERVER_VALIDATION_ERROR: 'core_form_dynamicform_validationerror',\n        // Error occurred while performing request to the server.\n        // Cancellable (by default calls Notification.exception).\n        ERROR: 'core_form_dynamicform_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_dynamicform_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_dynamicform_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_dynamicform_cancelbutton',\n    };\n\n    /**\n     * Constructor\n     *\n     * Creates an instance\n     *\n     * @param {Element} container - the parent element for the form\n     * @param {string} formClass full name of the php class that extends \\core_form\\modal , must be in autoloaded location\n     */\n    constructor(container, formClass) {\n        this.formClass = formClass;\n        this.container = container;\n\n        // Ensure strings required for shortforms are always available.\n        getStrings([\n            {key: 'collapseall', component: 'moodle'},\n            {key: 'expandall', component: 'moodle'}\n        ]).catch(Notification.exception);\n\n        // Register delegated events handlers in vanilla JS.\n        this.container.addEventListener('click', e => {\n            if (e.target.matches('form input[type=submit][data-cancel]')) {\n                e.preventDefault();\n                const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED, e.target);\n                if (!event.defaultPrevented) {\n                    this.processCancelButton();\n                }\n            } else if (e.target.matches('form input[type=submit][data-no-submit=\"1\"]')) {\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\n        this.container.addEventListener('submit', e => {\n            if (e.target.matches('form')) {\n                e.preventDefault();\n                const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED);\n                if (!event.defaultPrevented) {\n                    this.submitFormAjax();\n                }\n            }\n        });\n    }\n\n    /**\n     * Loads the form via AJAX and shows it inside a given container\n     *\n     * @param {Object} args\n     * @return {Promise}\n     * @public\n     */\n    load(args = null) {\n        const formData = serialize(args || {});\n        const pendingPromise = new Pending('core_form/dynamicform:load');\n        return this.getBody(formData)\n        .then((resp) => this.updateForm(resp))\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.container.dispatchEvent(e);\n        return e;\n    }\n\n    /**\n     * Add listener for an event\n     *\n     * @param {array} args\n     * @example:\n     *    const dynamicForm = new DynamicForm(...);\n     *    dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => {\n     *        e.preventDefault();\n     *        window.console.log(e.detail);\n     *        dynamicForm.container.innerHTML = 'Thank you, your form is submitted!';\n     *    });\n     */\n    addEventListener(...args) {\n        this.container.addEventListener(...args);\n    }\n\n    /**\n     * Get form body\n     *\n     * @param {String} formDataString form data in format of a query string\n     * @private\n     * @return {Promise}\n     */\n    getBody(formDataString) {\n        return Ajax.call([{\n            methodname: 'core_form_dynamic_form',\n            args: {\n                formdata: formDataString,\n                form: this.formClass,\n            }\n        }])[0]\n        .then(response => {\n            return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)};\n        });\n    }\n\n    /**\n     * On form submit\n     *\n     * @param {*} response Response received from the form's \"process\" method\n     */\n    onSubmitSuccess(response) {\n        const event = this.trigger(this.events.FORM_SUBMITTED, response);\n        if (event.defaultPrevented) {\n            return;\n        }\n\n        // Default implementation is to remove the form. Event listener should either remove or reload the form\n        // since its contents is no longer correct. For example, if an element was created as a result of\n        // form submission, the \"id\" in the form would be still zero. Also the server-side validation\n        // errors from the previous submission may still be present.\n        this.container.innerHTML = '';\n    }\n\n    /**\n     * On exception during form processing\n     *\n     * @private\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     * Click on a \"submit\" button that is marked in the form as registerNoSubmitButton()\n     *\n     * @method submitButtonPressed\n     * @param {Element} button that was pressed\n     * @fires event:formSubmittedByJavascript\n     */\n    processNoSubmitButton(button) {\n        const pendingPromise = new Pending('core_form/dynamicform:nosubmit');\n        const form = this.getFormNode();\n        const formData = new URLSearchParams([...(new FormData(form)).entries()]);\n        formData.append(button.getAttribute('name'), button.getAttribute('value'));\n\n        FormEvents.notifyFormSubmittedByJavascript(form, true);\n\n        // Add the button name to the form data and submit it.\n        this.disableButtons();\n\n        this.getBody(formData.toString())\n        .then(resp => this.updateForm(resp))\n        .then(pendingPromise.resolve)\n        .catch(exception => this.onSubmitError(exception));\n    }\n\n    /**\n     * Get the form node from the Dialogue.\n     *\n     * @returns {HTMLFormElement}\n     */\n    getFormNode() {\n        return this.container.querySelector('form');\n    }\n\n    /**\n     * Notifies listeners that form dirty state should be reset.\n     *\n     * @fires event:formSubmittedByJavascript\n     */\n    notifyResetFormChanges() {\n        FormEvents.notifyFormSubmittedByJavascript(this.getFormNode(), true);\n        FormChangeChecker.resetFormDirtyState(this.getFormNode());\n    }\n\n    /**\n     * Click on a \"cancel\" button\n     */\n    processCancelButton() {\n        // Notify listeners that the form is about to be submitted (this will reset atto autosave).\n        this.notifyResetFormChanges();\n\n        const event = this.trigger(this.events.FORM_CANCELLED);\n        if (!event.defaultPrevented) {\n            // By default removes the form from the DOM.\n            this.container.innerHTML = '';\n        }\n    }\n\n    /**\n     * Update form contents\n     *\n     * @param {object} param\n     * @param {string} param.html\n     * @param {string} param.js\n     * @returns {Promise}\n     */\n    updateForm({html, js}) {\n        return Templates.replaceNodeContents(this.container, html, js);\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        // Notify listeners that the form is about to be submitted (this will reset atto autosave).\n        FormEvents.notifyFormSubmittedByJavascript(this.getFormNode());\n\n        // Now the change events have run, see if there are any \"invalid\" form fields.\n        const invalid = [...this.container.querySelectorAll('[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[0].focus();\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Disable buttons during form submission\n     */\n    disableButtons() {\n        this.container.querySelectorAll('form input[type=\"submit\"]')\n            .forEach(el => el.setAttribute('disabled', true));\n    }\n\n    /**\n     * Enable buttons after form submission (on validation error)\n     */\n    enableButtons() {\n        this.container.querySelectorAll('form input[type=\"submit\"]')\n            .forEach(el => el.removeAttribute('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 form = this.container.querySelector('form');\n        const formData = new URLSearchParams([...(new FormData(form)).entries()]);\n\n        // Now we can continue...\n        Ajax.call([{\n            methodname: 'core_form_dynamic_form',\n            args: {\n                formdata: formData.toString(),\n                form: this.formClass\n            }\n        }])[0]\n        .then((response) => {\n            if (!response.submitted) {\n                // Form was not submitted, it could be either because validation failed or because no-submit button was pressed.\n                this.updateForm({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)});\n                this.enableButtons();\n                this.trigger(this.events.SERVER_VALIDATION_ERROR, null, false);\n            } else {\n                // Form was submitted properly.\n                const data = JSON.parse(response.data);\n                this.enableButtons();\n                this.notifyResetFormChanges();\n                this.onSubmitSuccess(data);\n            }\n            return null;\n        })\n        .catch(exception => this.onSubmitError(exception));\n    }\n}\n"],"names":["constructor","container","formClass","FORM_SUBMITTED","FORM_CANCELLED","CLIENT_VALIDATION_ERROR","SERVER_VALIDATION_ERROR","ERROR","NOSUBMIT_BUTTON_PRESSED","SUBMIT_BUTTON_PRESSED","CANCEL_BUTTON_PRESSED","key","component","catch","Notification","exception","addEventListener","e","target","matches","preventDefault","this","trigger","events","defaultPrevented","processCancelButton","processNoSubmitButton","submitFormAjax","load","args","formData","pendingPromise","Pending","getBody","then","resp","updateForm","resolve","eventName","CustomEvent","detail","cancelable","dispatchEvent","formDataString","Ajax","call","methodname","formdata","form","response","html","js","Fragment","processCollectedJavascript","javascript","onSubmitSuccess","innerHTML","onSubmitError","button","getFormNode","URLSearchParams","FormData","entries","append","getAttribute","FormEvents","notifyFormSubmittedByJavascript","disableButtons","toString","querySelector","notifyResetFormChanges","FormChangeChecker","resetFormDirtyState","Templates","replaceNodeContents","validateElements","invalid","querySelectorAll","length","focus","forEach","el","setAttribute","enableButtons","removeAttribute","submitted","data","JSON","parse"],"mappings":"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"}