Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
{"version":3,"file":"changechecker.min.js","sources":["../src/changechecker.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 * This module provides change detection to forms, allowing a browser to warn the user before navigating away if changes\n * have been made.\n *\n * Two flags are stored for each form:\n * * a 'dirty' flag; and\n * * a 'submitted' flag.\n *\n * When the page is unloaded each watched form is checked. If the 'dirty' flag is set for any form, and the 'submitted'\n * flag is not set for any form, then a warning is shown.\n *\n * The 'dirty' flag is set when any form element is modified within a watched form.\n * The flag can also be set programatically. This may be required for custom form elements.\n *\n * It is not possible to customise the warning message in any modern browser.\n *\n * Please note that some browsers have controls on when these alerts may or may not be shown.\n * See {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} for browser-specific\n * notes and references.\n *\n * @module     core_form/changechecker\n * @copyright  2021 Andrew Lyons <andrew@nicols.co.uk>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @example <caption>Usage where the FormElement is already held</caption>\n *\n * import {watchForm} from 'core_form/changechecker';\n *\n * // Fetch the form element somehow.\n * watchForm(formElement);\n *\n * @example <caption>Usage from the child of a form - i.e. an input, button, div, etc.</caption>\n *\n * import {watchForm} from 'core_form/changechecker';\n *\n * // Watch the form by using a child of it.\n * watchForm(document.querySelector('input[data-foo=\"bar\"]'););\n *\n * @example <caption>Usage from within a template</caption>\n * <form id=\"mod_example-entry-{{uniqid}}\" ...>\n *   <!--\n *\n *   -->\n * </form>\n * {{#js}}\n * require(['core_form/changechecker'], function(changeChecker) {\n *     watchFormById('mod_example-entry-{{uniqid}}');\n * });\n * {{/js}}\n */\n\nimport {eventTypes} from 'core_editor/events';\nimport {getString} from 'core/str';\n\n/**\n * @property {Bool} initialised Whether the change checker has been initialised\n * @private\n */\nlet initialised = false;\n\n/**\n * @property {String} warningString The warning string to show on form change failure\n * @private\n */\nlet warningString;\n\n/**\n * @property {Array} watchedForms The list of watched forms\n * @private\n */\nlet watchedForms = [];\n\n/**\n * @property {Bool} formChangeCheckerDisabled Whether the form change checker has been actively disabled\n * @private\n */\nlet formChangeCheckerDisabled = false;\n\n/**\n * Get the nearest form element from a child element.\n *\n * @param {HTMLElement} formChild\n * @returns {HTMLFormElement|null}\n * @private\n */\nconst getFormFromChild = formChild => formChild.closest('form');\n\n/**\n * Watch the specified form for changes.\n *\n * @method\n * @param   {HTMLElement} formNode\n */\nexport const watchForm = formNode => {\n    // Normalise the formNode.\n    formNode = getFormFromChild(formNode);\n\n    if (!formNode) {\n         // No form found.\n         return;\n    }\n\n    if (isWatchingForm(formNode)) {\n        // This form is already watched.\n        return;\n    }\n\n    watchedForms.push(formNode);\n};\n\n/**\n * Stop watching the specified form for changes.\n *\n * If the form was not watched, then no change is made.\n *\n * A child of the form may be passed instead.\n *\n * @method\n * @param   {HTMLElement} formNode\n * @example <caption>Stop watching a form for changes</caption>\n * import {unWatchForm} from 'core_form/changechecker';\n *\n * // ...\n * document.addEventListener('click', e => {\n *     if (e.target.closest('[data-action=\"changePage\"]')) {\n *         unWatchForm(e.target);\n *     }\n * });\n */\nexport const unWatchForm = formNode => {\n    watchedForms = watchedForms.filter(watchedForm => !!watchedForm.contains(formNode));\n};\n\n/**\n * Reset the 'dirty' flag for all watched forms.\n *\n * If a form was previously marked as 'dirty', then this flag will be cleared and when the page is unloaded no warning\n * will be shown.\n *\n * @method\n */\nexport const resetAllFormDirtyStates = () => {\n    watchedForms.forEach(watchedForm => {\n        watchedForm.dataset.formSubmitted = \"false\";\n        watchedForm.dataset.formDirty = \"false\";\n    });\n};\n\n/**\n * Reset the 'dirty' flag of the specified form.\n *\n * @method\n * @param   {HTMLElement} formNode\n */\nexport const resetFormDirtyState = formNode => {\n    formNode = getFormFromChild(formNode);\n\n    if (!formNode) {\n         return;\n    }\n\n    formNode.dataset.formSubmitted = \"false\";\n    formNode.dataset.formDirty = \"false\";\n};\n\n/**\n * Mark all forms as dirty.\n *\n * This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.\n * It will be removed in Moodle 4.4.\n *\n * @method\n */\nexport const markAllFormsAsDirty = () => {\n    watchedForms.forEach(watchedForm => {\n        watchedForm.dataset.formDirty = \"true\";\n    });\n};\n\n/**\n * Mark a specific form as dirty.\n *\n * This behaviour may be required for custom form elements which are not caught by the standard change listeners.\n *\n * @method\n * @param   {HTMLElement} formNode\n */\nexport const markFormAsDirty = formNode => {\n    formNode = getFormFromChild(formNode);\n\n    if (!formNode) {\n         return;\n    }\n\n    // Mark it as dirty.\n    formNode.dataset.formDirty = \"true\";\n};\n\n/**\n * Actively disable the form change checker.\n *\n * Please note that it cannot be re-enabled once disabled.\n *\n * @method\n */\nexport const disableAllChecks = () => {\n    formChangeCheckerDisabled = true;\n};\n\n/**\n * Check whether any watched from is dirty.\n *\n * @method\n * @returns {Bool}\n */\nexport const isAnyWatchedFormDirty = () => {\n    if (formChangeCheckerDisabled) {\n        // The form change checker is disabled.\n        return false;\n    }\n\n    const hasSubmittedForm = watchedForms.some(watchedForm => watchedForm.dataset.formSubmitted === \"true\");\n    if (hasSubmittedForm) {\n        // Do not warn about submitted forms, ever.\n        return false;\n    }\n\n    const hasDirtyForm = watchedForms.some(watchedForm => {\n        if (!watchedForm.isConnected) {\n            // The watched form is not connected to the DOM.\n            return false;\n        }\n\n        if (watchedForm.dataset.formDirty === \"true\") {\n            // The form has been marked as dirty.\n            return true;\n        }\n\n        // Elements currently holding focus will not have triggered change detection.\n        // Check whether the value matches the original value upon form load.\n        if (document.activeElement && document.activeElement.dataset.propertyIsEnumerable('initialValue')) {\n            const isActiveElementWatched = isWatchingForm(document.activeElement)\n                && !shouldIgnoreChangesForNode(document.activeElement);\n            const hasValueChanged = document.activeElement.dataset.initialValue !== document.activeElement.value;\n\n            if (isActiveElementWatched && hasValueChanged) {\n                return true;\n            }\n        }\n\n        return false;\n    });\n\n    if (hasDirtyForm) {\n        // At least one form is dirty.\n        return true;\n    }\n\n    // Handle TinyMCE editor instances.\n    // TinyMCE forms may not have been initialised at the time that startWatching is called.\n    // Check whether any tinyMCE editor is dirty.\n    if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {\n        if (window.tinyMCE.editors.some(editor => editor.isDirty())) {\n            return true;\n        }\n    }\n\n    // No dirty forms detected.\n    return false;\n};\n\n/**\n * Get the watched form for the specified target.\n *\n * @method\n * @param   {HTMLNode} target\n * @returns {HTMLFormElement}\n * @private\n */\nconst getFormForNode = target => watchedForms.find(watchedForm => watchedForm.contains(target));\n\n/**\n * Whether the specified target is a watched form.\n *\n * @method\n * @param   {HTMLNode} target\n * @returns {Bool}\n * @private\n */\nconst isWatchingForm = target => watchedForms.some(watchedForm => watchedForm.contains(target));\n\n/**\n * Whether the specified target should ignore changes or not.\n *\n * @method\n * @param   {HTMLNode} target\n * @returns {Bool}\n * @private\n */\nconst shouldIgnoreChangesForNode = target => !!target.closest('.ignoredirty');\n\n/**\n * Mark a form as changed.\n *\n * @method\n * @param   {HTMLElement} changedNode An element in the form which was changed\n */\nexport const markFormChangedFromNode = changedNode => {\n    if (changedNode.dataset.formChangeCheckerOverride) {\n        // Changes to this form node disable the form change checker entirely.\n        // This is intended for select fields which cause an immediate redirect.\n        disableAllChecks();\n        return;\n    }\n\n    if (!isWatchingForm(changedNode)) {\n        return;\n    }\n\n    if (shouldIgnoreChangesForNode(changedNode)) {\n        return;\n    }\n\n    // Mark the form as dirty.\n    const formNode = getFormForNode(changedNode);\n    formNode.dataset.formDirty = \"true\";\n};\n\n/**\n * Mark a form as submitted.\n *\n * @method\n * @param   {HTMLElement} formNode An element in the form to mark as submitted\n */\nexport const markFormSubmitted = formNode => {\n    formNode = getFormFromChild(formNode);\n\n    if (!formNode) {\n         return;\n    }\n\n    formNode.dataset.formSubmitted = \"true\";\n};\n\n/**\n * Mark all forms as submitted.\n *\n * This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.\n * It will be removed in Moodle 4.4.\n *\n * @method\n */\nexport const markAllFormsSubmitted = () => {\n    watchedForms.forEach(watchedForm => markFormSubmitted(watchedForm));\n};\n\n/**\n * Handle the beforeunload event.\n *\n * @method\n * @param   {Event} e\n * @returns {string|null}\n * @private\n */\nconst beforeUnloadHandler = e => {\n    // Please note: The use of Promises in this function is forbidden.\n    // This is an event handler and _cannot_ be asynchronous.\n    let warnBeforeUnload = isAnyWatchedFormDirty() && !M.cfg.behatsiterunning;\n    if (warnBeforeUnload) {\n        // According to the specification, to show the confirmation dialog an event handler should call preventDefault()\n        // on the event.\n        e.preventDefault();\n\n        // However note that not all browsers support this method, and some instead require the event handler to\n        // implement one of two legacy methods:\n        // * assigning a string to the event's returnValue property; and\n        // * returning a string from the event handler.\n\n        // Assigning a string to the event's returnValue property.\n        e.returnValue = warningString;\n\n        // Returning a string from the event handler.\n        return e.returnValue;\n    }\n\n    // Attaching an event handler/listener to window or document's beforeunload event prevents browsers from using\n    // in-memory page navigation caches, like Firefox's Back-Forward cache or WebKit's Page Cache.\n    // Remove the handler.\n    window.removeEventListener('beforeunload', beforeUnloadHandler);\n\n    return null;\n};\n\n/**\n * Start watching for form changes.\n *\n * This function is called on module load, and should not normally be called.\n *\n * @method\n * @protected\n */\nexport const startWatching = () => {\n    if (initialised) {\n        return;\n    }\n\n    document.addEventListener('change', e => {\n        if (!isWatchingForm(e.target)) {\n            return;\n        }\n\n        markFormChangedFromNode(e.target);\n    });\n\n    document.addEventListener('click', e => {\n        const ignoredButton = e.target.closest('[data-formchangechecker-ignore-submit]');\n        if (!ignoredButton) {\n            return;\n        }\n\n        const ownerForm = getFormFromChild(e.target);\n        if (ownerForm) {\n            ownerForm.dataset.ignoreSubmission = \"true\";\n        }\n    });\n\n    document.addEventListener('focusin', e => {\n        if (e.target.matches('input, textarea, select')) {\n            if (e.target.dataset.propertyIsEnumerable('initialValue')) {\n                // The initial value has already been set.\n                return;\n            }\n            e.target.dataset.initialValue = e.target.value;\n        }\n    });\n\n    document.addEventListener('submit', e => {\n        const formNode = getFormFromChild(e.target);\n        if (!formNode) {\n            // Weird, but watch for this anyway.\n            return;\n        }\n\n        if (formNode.dataset.ignoreSubmission) {\n            // This form was submitted by a button which requested that the form checked should not mark it as submitted.\n            formNode.dataset.ignoreSubmission = \"false\";\n            return;\n        }\n\n        markFormSubmitted(formNode);\n    });\n\n    document.addEventListener(eventTypes.editorContentRestored, e => {\n        if (e.target != document) {\n            resetFormDirtyState(e.target);\n        } else {\n            resetAllFormDirtyStates();\n        }\n    });\n\n    getString('changesmadereallygoaway', 'moodle')\n    .then(changesMadeString => {\n        warningString = changesMadeString;\n        return;\n    })\n    .catch();\n\n    window.addEventListener('beforeunload', beforeUnloadHandler);\n};\n\n/**\n * Watch the form matching the specified ID for changes.\n *\n * @method\n * @param   {String} formId\n */\nexport const watchFormById = formId => {\n    watchForm(document.getElementById(formId));\n};\n\n/**\n * Reset the dirty state of the form matching the specified ID..\n *\n * @method\n * @param   {String} formId\n */\nexport const resetFormDirtyStateById = formId => {\n    resetFormDirtyState(document.getElementById(formId));\n};\n\n/**\n * Mark the form matching the specified ID as dirty.\n *\n * @method\n * @param   {String} formId\n */\nexport const markFormAsDirtyById = formId => {\n    markFormAsDirty(document.getElementById(formId));\n};\n\n// Configure all event listeners.\nstartWatching();\n"],"names":["warningString","watchedForms","formChangeCheckerDisabled","getFormFromChild","formChild","closest","watchForm","formNode","isWatchingForm","push","filter","watchedForm","contains","resetAllFormDirtyStates","forEach","dataset","formSubmitted","formDirty","resetFormDirtyState","markFormAsDirty","disableAllChecks","isAnyWatchedFormDirty","some","isConnected","document","activeElement","propertyIsEnumerable","isActiveElementWatched","shouldIgnoreChangesForNode","hasValueChanged","initialValue","value","window","tinyMCE","editors","editor","isDirty","target","markFormChangedFromNode","changedNode","formChangeCheckerOverride","find","markFormSubmitted","beforeUnloadHandler","e","M","cfg","behatsiterunning","preventDefault","returnValue","removeEventListener","startWatching","addEventListener","ownerForm","ignoreSubmission","matches","eventTypes","editorContentRestored","then","changesMadeString","catch","formId","getElementById"],"mappings":"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"}