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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"}
|