diff --git a/lib/form/amd/build/changechecker.min.js b/lib/form/amd/build/changechecker.min.js
new file mode 100644
index 00000000000..98f4db93a83
--- /dev/null
+++ b/lib/form/amd/build/changechecker.min.js
@@ -0,0 +1,2 @@
+define ("core_form/changechecker",["exports","core_editor/events","core/str"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.markFormAsDirtyById=a.resetFormDirtyStateById=a.watchFormById=a.startWatching=a.markAllFormsSubmitted=a.markFormSubmitted=a.markFormChangedFromNode=a.disableAllChecks=a.markFormAsDirty=a.markAllFormsAsDirty=a.resetFormDirtyState=a.resetAllFormDirtyStates=a.unWatchForm=a.watchForm=void 0;var d,e=[],f=!1,g=function(a){return a.closest("form")},h=function(a){a=g(a);if(!a){return}if(q(a)){return}e.push(a)};a.watchForm=h;var i=function(a){e=e.filter(function(b){return!!b.contains(a)})};a.unWatchForm=i;var j=function(){e.forEach(function(a){a.dataset.formSubmitted="false";a.dataset.formDirty="false"})};a.resetAllFormDirtyStates=j;var k=function(a){a=g(a);if(!a){return}a.dataset.formSubmitted="false";a.dataset.formDirty="false"};a.resetFormDirtyState=k;var l=function(){e.forEach(function(a){a.dataset.formDirty="true"})};a.markAllFormsAsDirty=l;var m=function(a){a=g(a);if(!a){return}a.dataset.formDirty="true"};a.markFormAsDirty=m;var n=function(){f=!0};a.disableAllChecks=n;var o=function(){if(f){return!1}var a=e.some(function(a){return"true"===a.dataset.formSubmitted});if(a){return!1}var b=e.some(function(a){if(!a.isConnected){return!1}if("true"===a.dataset.formDirty){return!0}if(document.activeElement&&document.activeElement.dataset.propertyIsEnumerable("initialValue")){var b=q(document.activeElement),c=document.activeElement.dataset.initialValue!==document.activeElement.value;if(b&&c){return!0}}return!1});if(b){return!0}if("undefined"!=typeof window.tinyMCE){if(window.tinyMCE.editors.some(function(a){return a.isDirty()})){return!0}}return!1},p=function(a){return e.find(function(b){return b.contains(a)})},q=function(a){return e.some(function(b){return b.contains(a)})},r=function(a){return!!a.closest(".ignoredirty")},s=function(a){if(a.dataset.formChangeCheckerOverride){n();return}if(!q(a)){return}if(r(a)){return}var b=p(a);b.dataset.formDirty="true"};a.markFormChangedFromNode=s;var t=function(a){a=g(a);if(!a){return}a.dataset.formSubmitted="true"};a.markFormSubmitted=t;var u=function(){e.forEach(function(a){return t(a)})};a.markAllFormsSubmitted=u;var v=function(a){var b=o()&&!M.cfg.behatsiterunning;if(b){a.preventDefault();a.returnValue=d;return a.returnValue}window.removeEventListener("beforeunload",v);return null},w=function(){x();document.addEventListener("change",function(a){if(!q(a.target)){return}s(a.target)});document.addEventListener("click",function(a){var b=a.target.closest("[data-formchangechecker-ignore-submit]");if(!b){return}var c=g(a.target);if(c){c.dataset.ignoreSubmission="true"}});document.addEventListener("focusin",function(a){if(a.target.matches("input, textarea, select")){if(a.target.dataset.propertyIsEnumerable("initialValue")){return}a.target.dataset.initialValue=a.target.value}});document.addEventListener("submit",function(a){var b=g(a.target);if(!b){return}if(b.dataset.ignoreSubmission){b.dataset.ignoreSubmission="false";return}t(b)});document.addEventListener(b.eventTypes.editorContentRestored,function(a){if(a.target!=document){k(a.target)}else{j()}});(0,c.get_string)("changesmadereallygoaway","moodle").then(function(a){d=a}).catch();window.addEventListener("beforeunload",v)};a.startWatching=w;var x=function(){var a=function(a,b,c){return function(){window.console.warn("The moodle-core-formchangechecker has been deprecated and replaced with core_form/changechecker. "+"The ".concat(a," function has been replaced with ").concat(b,"."));c.apply(void 0,arguments)}};window.M.core_formchangechecker={init:a("init","watchFormById",y),reset_form_dirty_state:a("reset_form_dirty_state","resetFormDirtyState",j),set_form_changed:a("set_form_changed","markFormAsDirty",l),set_form_submitted:a("set_form_submitted","markFormSubmitted",markAllFormsAsSubmitted)}},y=function(a){h(document.getElementById(a))};a.watchFormById=y;a.resetFormDirtyStateById=function resetFormDirtyStateById(a){k(document.getElementById(a))};a.markFormAsDirtyById=function markFormAsDirtyById(a){m(document.getElementById(a))};w()});
+//# sourceMappingURL=changechecker.min.js.map
diff --git a/lib/form/amd/build/changechecker.min.js.map b/lib/form/amd/build/changechecker.min.js.map
new file mode 100644
index 00000000000..09e6269f71c
--- /dev/null
+++ b/lib/form/amd/build/changechecker.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/changechecker.js"],"names":["warningString","watchedForms","formChangeCheckerDisabled","getFormFromChild","formChild","closest","watchForm","formNode","isWatchingForm","push","unWatchForm","filter","watchedForm","contains","resetAllFormDirtyStates","forEach","dataset","formSubmitted","formDirty","resetFormDirtyState","markAllFormsAsDirty","markFormAsDirty","disableAllChecks","isAnyWatchedFormDirty","hasSubmittedForm","some","hasDirtyForm","isConnected","document","activeElement","propertyIsEnumerable","isActiveElementWatched","hasValueChanged","initialValue","value","window","tinyMCE","editors","editor","isDirty","getFormForNode","target","find","shouldIgnoreChangesForNode","markFormChangedFromNode","changedNode","formChangeCheckerOverride","markFormSubmitted","markAllFormsSubmitted","beforeUnloadHandler","e","warnBeforeUnload","M","cfg","behatsiterunning","preventDefault","returnValue","removeEventListener","startWatching","addLegacyFunctions","addEventListener","ignoredButton","ownerForm","ignoreSubmission","matches","eventTypes","editorContentRestored","then","changesMadeString","catch","getLoggedLegacyFallback","oldFunctionName","newFunctionName","newFunction","console","warn","core_formchangechecker","init","watchFormById","reset_form_dirty_state","set_form_changed","set_form_submitted","markAllFormsAsSubmitted","formId","getElementById","resetFormDirtyStateById","markFormAsDirtyById"],"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 * 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 \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @example Usage where the FormElement is already held\n *\n * import {watchForm} from 'core_form/changechecker';\n *\n * // Fetch the form element somehow.\n * watchForm(formElement);\n *\n * @example Usage from the child of a form - i.e. an input, button, div, etc.\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 Usage from within a template\n * \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 {get_string as 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 Stop watching a form for changes\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 * @private\n */\nconst 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 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') {\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 // Add legacy support to provide b/c for the old YUI version.\n addLegacyFunctions();\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 * Add legacy functions for backwards compatability.\n *\n * @method\n * @private\n */\nconst addLegacyFunctions = () => {\n // Create a curried function to log use of the old function and provide detail on its replacement.\n const getLoggedLegacyFallback = (oldFunctionName, newFunctionName, newFunction) => (...args) => {\n window.console.warn(\n `The moodle-core-formchangechecker has been deprecated ` +\n `and replaced with core_form/changechecker. ` +\n `The ${oldFunctionName} function has been replaced with ${newFunctionName}.`\n );\n newFunction(...args);\n };\n\n /* eslint-disable */\n window.M.core_formchangechecker = {\n init: getLoggedLegacyFallback('init', 'watchFormById', watchFormById),\n reset_form_dirty_state: getLoggedLegacyFallback('reset_form_dirty_state', 'resetFormDirtyState', resetAllFormDirtyStates),\n set_form_changed: getLoggedLegacyFallback('set_form_changed', 'markFormAsDirty', markAllFormsAsDirty),\n set_form_submitted: getLoggedLegacyFallback('set_form_submitted', 'markFormSubmitted', markAllFormsAsSubmitted),\n };\n /* eslint-enable */\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"],"file":"changechecker.min.js"}
\ No newline at end of file
diff --git a/lib/form/amd/src/changechecker.js b/lib/form/amd/src/changechecker.js
new file mode 100644
index 00000000000..4dfcfb933ec
--- /dev/null
+++ b/lib/form/amd/src/changechecker.js
@@ -0,0 +1,546 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * This module provides change detection to forms, allowing a browser to warn the user before navigating away if changes
+ * have been made.
+ *
+ * Two flags are stored for each form:
+ * * a 'dirty' flag; and
+ * * a 'submitted' flag.
+ *
+ * When the page is unloaded each watched form is checked. If the 'dirty' flag is set for any form, and the 'submitted'
+ * flag is not set for any form, then a warning is shown.
+ *
+ * The 'dirty' flag is set when any form element is modified within a watched form.
+ * The flag can also be set programatically. This may be required for custom form elements.
+ *
+ * It is not possible to customise the warning message in any modern browser.
+ *
+ * Please note that some browsers have controls on when these alerts may or may not be shown.
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} for browser-specific
+ * notes and references.
+ *
+ * @module core_form/changechecker
+ * @copyright 2021 Andrew Lyons
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @example Usage where the FormElement is already held
+ *
+ * import {watchForm} from 'core_form/changechecker';
+ *
+ * // Fetch the form element somehow.
+ * watchForm(formElement);
+ *
+ * @example Usage from the child of a form - i.e. an input, button, div, etc.
+ *
+ * import {watchForm} from 'core_form/changechecker';
+ *
+ * // Watch the form by using a child of it.
+ * watchForm(document.querySelector('input[data-foo="bar"]'););
+ *
+ * @example Usage from within a template
+ *
+ * {{#js}}
+ * require(['core_form/changechecker'], function(changeChecker) {
+ * watchFormById('mod_example-entry-{{uniqid}}');
+ * });
+ * {{/js}}
+ */
+
+import {eventTypes} from 'core_editor/events';
+import {get_string as getString} from 'core/str';
+
+/**
+ * @property {Bool} initialised Whether the change checker has been initialised
+ * @private
+ */
+let initialised = false;
+
+/**
+ * @property {String} warningString The warning string to show on form change failure
+ * @private
+ */
+let warningString;
+
+/**
+ * @property {Array} watchedForms The list of watched forms
+ * @private
+ */
+let watchedForms = [];
+
+/**
+ * @property {Bool} formChangeCheckerDisabled Whether the form change checker has been actively disabled
+ * @private
+ */
+let formChangeCheckerDisabled = false;
+
+/**
+ * Get the nearest form element from a child element.
+ *
+ * @param {HTMLElement} formChild
+ * @returns {HTMLFormElement|null}
+ * @private
+ */
+const getFormFromChild = formChild => formChild.closest('form');
+
+/**
+ * Watch the specified form for changes.
+ *
+ * @method
+ * @param {HTMLElement} formNode
+ */
+export const watchForm = formNode => {
+ // Normalise the formNode.
+ formNode = getFormFromChild(formNode);
+
+ if (!formNode) {
+ // No form found.
+ return;
+ }
+
+ if (isWatchingForm(formNode)) {
+ // This form is already watched.
+ return;
+ }
+
+ watchedForms.push(formNode);
+};
+
+/**
+ * Stop watching the specified form for changes.
+ *
+ * If the form was not watched, then no change is made.
+ *
+ * A child of the form may be passed instead.
+ *
+ * @method
+ * @param {HTMLElement} formNode
+ * @example Stop watching a form for changes
+ * import {unWatchForm} from 'core_form/changechecker';
+ *
+ * // ...
+ * document.addEventListener('click', e => {
+ * if (e.target.closest('[data-action="changePage"]')) {
+ * unWatchForm(e.target);
+ * }
+ * });
+ */
+export const unWatchForm = formNode => {
+ watchedForms = watchedForms.filter(watchedForm => !!watchedForm.contains(formNode));
+};
+
+/**
+ * Reset the 'dirty' flag for all watched forms.
+ *
+ * If a form was previously marked as 'dirty', then this flag will be cleared and when the page is unloaded no warning
+ * will be shown.
+ *
+ * @method
+ */
+export const resetAllFormDirtyStates = () => {
+ watchedForms.forEach(watchedForm => {
+ watchedForm.dataset.formSubmitted = "false";
+ watchedForm.dataset.formDirty = "false";
+ });
+};
+
+/**
+ * Reset the 'dirty' flag of the specified form.
+ *
+ * @method
+ * @param {HTMLElement} formNode
+ */
+export const resetFormDirtyState = formNode => {
+ formNode = getFormFromChild(formNode);
+
+ if (!formNode) {
+ return;
+ }
+
+ formNode.dataset.formSubmitted = "false";
+ formNode.dataset.formDirty = "false";
+};
+
+/**
+ * Mark all forms as dirty.
+ *
+ * This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.
+ * It will be removed in Moodle 4.4.
+ *
+ * @method
+ */
+export const markAllFormsAsDirty = () => {
+ watchedForms.forEach(watchedForm => {
+ watchedForm.dataset.formDirty = "true";
+ });
+};
+
+/**
+ * Mark a specific form as dirty.
+ *
+ * This behaviour may be required for custom form elements which are not caught by the standard change listeners.
+ *
+ * @method
+ * @param {HTMLElement} formNode
+ */
+export const markFormAsDirty = formNode => {
+ formNode = getFormFromChild(formNode);
+
+ if (!formNode) {
+ return;
+ }
+
+ // Mark it as dirty.
+ formNode.dataset.formDirty = "true";
+};
+
+/**
+ * Actively disable the form change checker.
+ *
+ * Please note that it cannot be re-enabled once disabled.
+ *
+ * @method
+ */
+export const disableAllChecks = () => {
+ formChangeCheckerDisabled = true;
+};
+
+/**
+ * Check whether any watched from is dirty.
+ *
+ * @method
+ * @returns {Bool}
+ * @private
+ */
+const isAnyWatchedFormDirty = () => {
+ if (formChangeCheckerDisabled) {
+ // The form change checker is disabled.
+ return false;
+ }
+
+ const hasSubmittedForm = watchedForms.some(watchedForm => watchedForm.dataset.formSubmitted === "true");
+ if (hasSubmittedForm) {
+ // Do not warn about submitted forms, ever.
+ return false;
+ }
+
+ const hasDirtyForm = watchedForms.some(watchedForm => {
+ if (!watchedForm.isConnected) {
+ // The watched form is not connected to the DOM.
+ return false;
+ }
+
+ if (watchedForm.dataset.formDirty === "true") {
+ // The form has been marked as dirty.
+ return true;
+ }
+
+ // Elements currently holding focus will not have triggered change detection.
+ // Check whether the value matches the original value upon form load.
+ if (document.activeElement && document.activeElement.dataset.propertyIsEnumerable('initialValue')) {
+ const isActiveElementWatched = isWatchingForm(document.activeElement);
+ const hasValueChanged = document.activeElement.dataset.initialValue !== document.activeElement.value;
+
+ if (isActiveElementWatched && hasValueChanged) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ if (hasDirtyForm) {
+ // At least one form is dirty.
+ return true;
+ }
+
+ // Handle TinyMCE editor instances.
+ // TinyMCE forms may not have been initialised at the time that startWatching is called.
+ // Check whether any tinyMCE editor is dirty.
+ if (typeof window.tinyMCE !== 'undefined') {
+ if (window.tinyMCE.editors.some(editor => editor.isDirty())) {
+ return true;
+ }
+ }
+
+ // No dirty forms detected.
+ return false;
+};
+
+/**
+ * Get the watched form for the specified target.
+ *
+ * @method
+ * @param {HTMLNode} target
+ * @returns {HTMLFormElement}
+ * @private
+ */
+const getFormForNode = target => watchedForms.find(watchedForm => watchedForm.contains(target));
+
+/**
+ * Whether the specified target is a watched form.
+ *
+ * @method
+ * @param {HTMLNode} target
+ * @returns {Bool}
+ * @private
+ */
+const isWatchingForm = target => watchedForms.some(watchedForm => watchedForm.contains(target));
+
+/**
+ * Whether the specified target should ignore changes or not.
+ *
+ * @method
+ * @param {HTMLNode} target
+ * @returns {Bool}
+ * @private
+ */
+const shouldIgnoreChangesForNode = target => !!target.closest('.ignoredirty');
+
+/**
+ * Mark a form as changed.
+ *
+ * @method
+ * @param {HTMLElement} changedNode An element in the form which was changed
+ */
+export const markFormChangedFromNode = changedNode => {
+ if (changedNode.dataset.formChangeCheckerOverride) {
+ // Changes to this form node disable the form change checker entirely.
+ // This is intended for select fields which cause an immediate redirect.
+ disableAllChecks();
+ return;
+ }
+
+ if (!isWatchingForm(changedNode)) {
+ return;
+ }
+
+ if (shouldIgnoreChangesForNode(changedNode)) {
+ return;
+ }
+
+ // Mark the form as dirty.
+ const formNode = getFormForNode(changedNode);
+ formNode.dataset.formDirty = "true";
+};
+
+/**
+ * Mark a form as submitted.
+ *
+ * @method
+ * @param {HTMLElement} formNode An element in the form to mark as submitted
+ */
+export const markFormSubmitted = formNode => {
+ formNode = getFormFromChild(formNode);
+
+ if (!formNode) {
+ return;
+ }
+
+ formNode.dataset.formSubmitted = "true";
+};
+
+/**
+ * Mark all forms as submitted.
+ *
+ * This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.
+ * It will be removed in Moodle 4.4.
+ *
+ * @method
+ */
+export const markAllFormsSubmitted = () => {
+ watchedForms.forEach(watchedForm => markFormSubmitted(watchedForm));
+};
+
+/**
+ * Handle the beforeunload event.
+ *
+ * @method
+ * @param {Event} e
+ * @returns {string|null}
+ * @private
+ */
+const beforeUnloadHandler = e => {
+ // Please note: The use of Promises in this function is forbidden.
+ // This is an event handler and _cannot_ be asynchronous.
+ let warnBeforeUnload = isAnyWatchedFormDirty() && !M.cfg.behatsiterunning;
+ if (warnBeforeUnload) {
+ // According to the specification, to show the confirmation dialog an event handler should call preventDefault()
+ // on the event.
+ e.preventDefault();
+
+ // However note that not all browsers support this method, and some instead require the event handler to
+ // implement one of two legacy methods:
+ // * assigning a string to the event's returnValue property; and
+ // * returning a string from the event handler.
+
+ // Assigning a string to the event's returnValue property.
+ e.returnValue = warningString;
+
+ // Returning a string from the event handler.
+ return e.returnValue;
+ }
+
+ // Attaching an event handler/listener to window or document's beforeunload event prevents browsers from using
+ // in-memory page navigation caches, like Firefox's Back-Forward cache or WebKit's Page Cache.
+ // Remove the handler.
+ window.removeEventListener('beforeunload', beforeUnloadHandler);
+
+ return null;
+};
+
+/**
+ * Start watching for form changes.
+ *
+ * This function is called on module load, and should not normally be called.
+ *
+ * @method
+ * @protected
+ */
+export const startWatching = () => {
+ if (initialised) {
+ return;
+ }
+
+ // Add legacy support to provide b/c for the old YUI version.
+ addLegacyFunctions();
+
+ document.addEventListener('change', e => {
+ if (!isWatchingForm(e.target)) {
+ return;
+ }
+
+ markFormChangedFromNode(e.target);
+ });
+
+ document.addEventListener('click', e => {
+ const ignoredButton = e.target.closest('[data-formchangechecker-ignore-submit]');
+ if (!ignoredButton) {
+ return;
+ }
+
+ const ownerForm = getFormFromChild(e.target);
+ if (ownerForm) {
+ ownerForm.dataset.ignoreSubmission = "true";
+ }
+ });
+
+ document.addEventListener('focusin', e => {
+ if (e.target.matches('input, textarea, select')) {
+ if (e.target.dataset.propertyIsEnumerable('initialValue')) {
+ // The initial value has already been set.
+ return;
+ }
+ e.target.dataset.initialValue = e.target.value;
+ }
+ });
+
+ document.addEventListener('submit', e => {
+ const formNode = getFormFromChild(e.target);
+ if (!formNode) {
+ // Weird, but watch for this anyway.
+ return;
+ }
+
+ if (formNode.dataset.ignoreSubmission) {
+ // This form was submitted by a button which requested that the form checked should not mark it as submitted.
+ formNode.dataset.ignoreSubmission = "false";
+ return;
+ }
+
+ markFormSubmitted(formNode);
+ });
+
+ document.addEventListener(eventTypes.editorContentRestored, e => {
+ if (e.target != document) {
+ resetFormDirtyState(e.target);
+ } else {
+ resetAllFormDirtyStates();
+ }
+ });
+
+ getString('changesmadereallygoaway', 'moodle')
+ .then(changesMadeString => {
+ warningString = changesMadeString;
+ return;
+ })
+ .catch();
+
+ window.addEventListener('beforeunload', beforeUnloadHandler);
+};
+
+/**
+ * Add legacy functions for backwards compatability.
+ *
+ * @method
+ * @private
+ */
+const addLegacyFunctions = () => {
+ // Create a curried function to log use of the old function and provide detail on its replacement.
+ const getLoggedLegacyFallback = (oldFunctionName, newFunctionName, newFunction) => (...args) => {
+ window.console.warn(
+ `The moodle-core-formchangechecker has been deprecated ` +
+ `and replaced with core_form/changechecker. ` +
+ `The ${oldFunctionName} function has been replaced with ${newFunctionName}.`
+ );
+ newFunction(...args);
+ };
+
+ /* eslint-disable */
+ window.M.core_formchangechecker = {
+ init: getLoggedLegacyFallback('init', 'watchFormById', watchFormById),
+ reset_form_dirty_state: getLoggedLegacyFallback('reset_form_dirty_state', 'resetFormDirtyState', resetAllFormDirtyStates),
+ set_form_changed: getLoggedLegacyFallback('set_form_changed', 'markFormAsDirty', markAllFormsAsDirty),
+ set_form_submitted: getLoggedLegacyFallback('set_form_submitted', 'markFormSubmitted', markAllFormsAsSubmitted),
+ };
+ /* eslint-enable */
+};
+
+/**
+ * Watch the form matching the specified ID for changes.
+ *
+ * @method
+ * @param {String} formId
+ */
+export const watchFormById = formId => {
+ watchForm(document.getElementById(formId));
+};
+
+/**
+ * Reset the dirty state of the form matching the specified ID..
+ *
+ * @method
+ * @param {String} formId
+ */
+export const resetFormDirtyStateById = formId => {
+ resetFormDirtyState(document.getElementById(formId));
+};
+
+/**
+ * Mark the form matching the specified ID as dirty.
+ *
+ * @method
+ * @param {String} formId
+ */
+export const markFormAsDirtyById = formId => {
+ markFormAsDirty(document.getElementById(formId));
+};
+
+// Configure all event listeners.
+startWatching();
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 2ba9d7d7497..5d23415ccaa 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -58,6 +58,8 @@ information provided here is intended especially for developers.
completion_info::internal_set_data() to reaggregate completions that have been marked for instant course completion.
* The following functions have been finally deprecated and can not be used anymore:
- generate_uuid
+* The YUI moodle-core-formchangechecker module has been deprecated and replaced with a new AMD module
+ core_form/changechecker.
=== 3.11.2 ===
* For security reasons, filelib has been updated so all requests now use emulated redirects.
diff --git a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
index c433452cc55..61da4e2f703 100644
--- a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
+++ b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
@@ -9,247 +9,64 @@ YUI.add('moodle-core-formchangechecker', function (Y, NAME) {
/**
* A utility to check for form changes before navigating away from a page.
*
- * To initialise, call M.core_formchangechecker.init({formid: 'myform'}); or perhaps
- *
- * Y.use('moodle-core-formchangechecker', function() {
- * M.core_formchangechecker.init({formid: 'myform'});
- * });
- *
- * If you have some fields in your form that you don't want to have tracked, then add
- * a data-formchangechecker-ignore-dirty to the field, or any parent element, and it
- * will be ignored.
- *
- * If you have a submit button in your form that does not actually save the data,
- * then add a data-formchangechecker-ignore-submit attribute to it.
+ * Please note that this YUI module has been deprecated in favour of the core_form/changechecker AMD module.
*
* @class M.core.formchangechecker
- * @constructor
+ * @deprecated
*/
-var FORMCHANGECHECKERNAME = 'core-formchangechecker',
-
- FORMCHANGECHECKER = function() {
- FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
- };
-
-Y.extend(FORMCHANGECHECKER, Y.Base, {
-
- // The delegated listeners we need to detach after the initial value has been stored once
- initialvaluelisteners: [],
-
- /**
- * Initialize the module
- *
- * @method initializer
- */
- initializer: function() {
- var formid = 'form#' + this.get('formid'),
- currentform = Y.one(formid);
-
- if (!currentform) {
- // If the form was not found, then we can't check for changes.
- return;
- }
-
- if (!M.core_formchangechecker.stateinformation.formchanged) {
- M.core_formchangechecker.stateinformation.formchanged = this.get('initialdirtystate');
- }
-
- // Add a listener here for an editor restore event.
- Y.on(M.core.event.EDITOR_CONTENT_RESTORED, M.core_formchangechecker.reset_form_dirty_state, this);
-
- // Add change events to the form elements
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'input', this);
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'textarea', this);
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'select', this);
-
- // Add a focus event to check for changes which are made without triggering a change event
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'input', this));
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'textarea', this));
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'select', this));
-
- currentform.delegate('click', function() {
- currentform.setData('ignoreSubmission', true);
- }, '[data-formchangechecker-ignore-submit]');
-
- // We need any submit buttons on the form to set the submitted flag.
- Y.one(formid).on('submit', function() {
- if (currentform.getData('ignoreSubmission')) {
- // But not if we have been told to ignore this button.
- currentform.clearData('ignoreSubmission');
- return;
- }
- M.core_formchangechecker.set_form_submitted();
- }, this);
-
- // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
- // a result, the has_changed must stay in the DOM too
- window.onbeforeunload = M.core_formchangechecker.report_form_dirty_state;
- },
-
- /**
- * Store the initial value of the currently focussed element
- *
- * If an element has been focussed and changed but not yet blurred, the on change
- * event won't be fired. We need to store it's initial value to compare it in the
- * get_form_dirty_state function later.
- *
- * @method store_initial_value
- * @param {EventFacade} e
- */
- store_initial_value: function(e) {
- var thisevent;
-
- // Don't warn on elements we have been told to ignore.
- if (e.target.ancestor('.ignoredirty', true) ||
- e.target.ancestor('[data-formchangechecker-ignore-dirty]', true)) {
- return;
- }
-
- if (M.core_formchangechecker.get_form_dirty_state()) {
- // Detach all listen events to prevent duplicate initial value setting
- while (this.initialvaluelisteners.length) {
- thisevent = this.initialvaluelisteners.shift();
- thisevent.detach();
- }
-
- return;
- }
-
- // Make a note of the current element so that it can be interrogated and
- // compared in the get_form_dirty_state function
- M.core_formchangechecker.stateinformation.focused_element = {
- element: e.target,
- initial_value: e.target.get('value')
- };
- }
- },
- {
- NAME: FORMCHANGECHECKERNAME,
- ATTRS: {
- formid: {
- 'value': ''
- },
- initialdirtystate: {
- 'value': false
- }
- }
- }
+window.console.warn(
+ 'The moodle-core-formchangechecker has been deprecated ' +
+ 'and replaced with core_form/changechecker. ' +
+ 'Please update your code to make use of the new module.'
);
-M.core_formchangechecker = M.core_formchangechecker || {};
+require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.startWatching();
+});
-// We might have multiple instances of the form change protector
-M.core_formchangechecker.instances = M.core_formchangechecker.instances || [];
-M.core_formchangechecker.init = function(config) {
- var formchangechecker = new FORMCHANGECHECKER(config);
- M.core_formchangechecker.instances.push(formchangechecker);
- return formchangechecker;
-};
+// The following are provided to prevent race conditions.
+// Because the AMD module is loaded asynchronously after the YUI module is loaded, there is a possibility that the
+// calling code may call the YUI function calls before the AMD module has finished loading.
+// These will be removed in future and are automatically overwritten by the legacy helper provided as part of the new
+// changechecker AMD module.
+// eslint-disable-next-line camelcase
+M.core_formchangechecker = M.core_formchangechecker || {
+ init: function(config) {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.watchFormById(config.formid);
+ });
+ },
-// Store state information
-M.core_formchangechecker.stateinformation = [];
+ /**
+ * Set the form changed state to true
+ */
+ // eslint-disable-next-line camelcase
+ set_form_changed: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.markAllFormsAsDirty();
+ });
+ },
-/*
- * Set the form changed state to true
- */
-M.core_formchangechecker.set_form_changed = function(e) {
- // Don't warn on elements we have been told to ignore.
- if (e && e.target && (e.target.ancestor('.ignoredirty', true) ||
- e.target.ancestor('[data-formchangechecker-ignore-dirty]', true))) {
- return;
- }
+ /**
+ * Set the form submitted state to true
+ */
+ // eslint-disable-next-line camelcase
+ set_form_submitted: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.markAllFormsSubmitted();
+ });
+ },
- M.core_formchangechecker.stateinformation.formchanged = 1;
-
- // Once the form has been marked as dirty, we no longer need to keep track of form elements
- // which haven't yet blurred
- delete M.core_formchangechecker.stateinformation.focused_element;
-};
-
-/*
- * Set the form submitted state to true
- */
-M.core_formchangechecker.set_form_submitted = function() {
- M.core_formchangechecker.stateinformation.formsubmitted = 1;
-};
-
-/*
- * Attempt to determine whether the form has been modified in any way and
- * is thus 'dirty'
- *
- * @return Integer 1 is the form is dirty; 0 if not
- */
-M.core_formchangechecker.get_form_dirty_state = function() {
- var state = M.core_formchangechecker.stateinformation,
- editor;
-
- // If the form was submitted, then return a non-dirty state
- if (state.formsubmitted) {
- return 0;
- }
-
- // If any fields have been marked dirty, return a dirty state
- if (state.formchanged) {
- return 1;
- }
-
- // If a field has been focused and changed, but still has focus then the browser won't fire the
- // onChange event. We check for this eventuality here
- if (state.focused_element) {
- if (state.focused_element.element.get('value') !== state.focused_element.initial_value) {
- return 1;
- }
- }
-
- // Handle TinyMCE editor instances
- // We can't add a listener in the initializer as the editors may not have been created by that point
- // so we do so here instead
- if (typeof window.tinyMCE !== 'undefined') {
- for (editor in window.tinyMCE.editors) {
- if (window.tinyMCE.editors[editor].isDirty()) {
- return 1;
- }
- }
- }
-
- // If we reached here, then the form hasn't met any of the dirty conditions
- return 0;
-};
-
-/*
- * Reset the form state
- */
-M.core_formchangechecker.reset_form_dirty_state = function() {
- M.core_formchangechecker.stateinformation.formsubmitted = false;
- M.core_formchangechecker.stateinformation.formchanged = false;
-};
-
-/*
- * Return a suitable message if changes have been made to a form
- */
-M.core_formchangechecker.report_form_dirty_state = function(e) {
- if (!M.core_formchangechecker.get_form_dirty_state()) {
- // the form is not dirty, so don't display any message
- return;
- }
-
- // This is the error message that we'll show to browsers which support it
- var warningmessage = M.util.get_string('changesmadereallygoaway', 'moodle');
-
- if (M.cfg.behatsiterunning) {
- // If the behat site is running we don't want browser alerts.
- return;
- }
-
- // Most browsers are happy with the returnValue being set on the event
- // But some browsers do not consistently pass the event
- if (e) {
- e.returnValue = warningmessage;
- }
-
- // But some require it to be returned instead
- return warningmessage;
+ /**
+ * Reset the form state
+ */
+ // eslint-disable-next-line camelcase
+ reset_form_dirty_state: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.resetAllFormDirtyStates();
+ });
+ },
};
diff --git a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
index f9d121ab719..4f08967838a 100644
--- a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
+++ b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
@@ -1 +1 @@
-YUI.add("moodle-core-formchangechecker",function(r,e){var c=function(){c.superclass.constructor.apply(this,arguments)};r.extend(c,r.Base,{initialvaluelisteners:[],initializer:function(){var e="form#"+this.get("formid"),t=r.one(e);t&&(M.core_formchangechecker.stateinformation.formchanged||(M.core_formchangechecker.stateinformation.formchanged=this.get("initialdirtystate")),r.on(M.core.event.EDITOR_CONTENT_RESTORED,M.core_formchangechecker.reset_form_dirty_state,this),t.delegate("change",M.core_formchangechecker.set_form_changed,"input",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"textarea",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"select",this),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"input",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"textarea",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"select",this)),t.delegate("click",function(){t.setData("ignoreSubmission",!0)},"[data-formchangechecker-ignore-submit]"),r.one(e).on("submit",function(){t.getData("ignoreSubmission")?t.clearData("ignoreSubmission"):M.core_formchangechecker.set_form_submitted()},this),window.onbeforeunload=M.core_formchangechecker.report_form_dirty_state)},store_initial_value:function(e){if(!e.target.ancestor(".ignoredirty",!0)&&!e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))if(M.core_formchangechecker.get_form_dirty_state())for(;this.initialvaluelisteners.length;)this.initialvaluelisteners.shift().detach();else M.core_formchangechecker.stateinformation.focused_element={element:e.target,initial_value:e.target.get("value")}}},{NAME:"core-formchangechecker",ATTRS:{formid:{value:""},initialdirtystate:{value:!1}}}),M.core_formchangechecker=M.core_formchangechecker||{},M.core_formchangechecker.instances=M.core_formchangechecker.instances||[],M.core_formchangechecker.init=function(e){var t=new c(e);return M.core_formchangechecker.instances.push(t),t},M.core_formchangechecker.stateinformation=[],M.core_formchangechecker.set_form_changed=function(e){e&&e.target&&(e.target.ancestor(".ignoredirty",!0)||e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))||(M.core_formchangechecker.stateinformation.formchanged=1,delete M.core_formchangechecker.stateinformation.focused_element)},M.core_formchangechecker.set_form_submitted=function(){M.core_formchangechecker.stateinformation.formsubmitted=1},M.core_formchangechecker.get_form_dirty_state=function(){var e,t=M.core_formchangechecker.stateinformation;if(t.formsubmitted)return 0;if(t.formchanged)return 1;if(t.focused_element&&t.focused_element.element.get("value")!==t.focused_element.initial_value)return 1;if("undefined"!=typeof window.tinyMCE)for(e in window.tinyMCE.editors)if(window.tinyMCE.editors[e].isDirty())return 1;return 0},M.core_formchangechecker.reset_form_dirty_state=function(){M.core_formchangechecker.stateinformation.formsubmitted=!1,M.core_formchangechecker.stateinformation.formchanged=!1},M.core_formchangechecker.report_form_dirty_state=function(e){if(M.core_formchangechecker.get_form_dirty_state()){var t=M.util.get_string("changesmadereallygoaway","moodle");if(!M.cfg.behatsiterunning)return e&&(e.returnValue=t),t}}},"@VERSION@",{requires:["base","event-focus","moodle-core-event"]});
\ No newline at end of file
+YUI.add("moodle-core-formchangechecker",function(e,r){window.console.warn("The moodle-core-formchangechecker has been deprecated and replaced with core_form/changechecker. Please update your code to make use of the new module."),require(["core_form/changechecker"],function(e){e.startWatching()}),M.core_formchangechecker=M.core_formchangechecker||{init:function(r){require(["core_form/changechecker"],function(e){e.watchFormById(r.formid)})},set_form_changed:function(){require(["core_form/changechecker"],function(e){e.markAllFormsAsDirty()})},set_form_submitted:function(){require(["core_form/changechecker"],function(e){e.markAllFormsSubmitted()})},reset_form_dirty_state:function(){require(["core_form/changechecker"],function(e){e.resetAllFormDirtyStates()})}}},"@VERSION@",{requires:["base","event-focus","moodle-core-event"]});
\ No newline at end of file
diff --git a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
index c433452cc55..61da4e2f703 100644
--- a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
+++ b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
@@ -9,247 +9,64 @@ YUI.add('moodle-core-formchangechecker', function (Y, NAME) {
/**
* A utility to check for form changes before navigating away from a page.
*
- * To initialise, call M.core_formchangechecker.init({formid: 'myform'}); or perhaps
- *
- * Y.use('moodle-core-formchangechecker', function() {
- * M.core_formchangechecker.init({formid: 'myform'});
- * });
- *
- * If you have some fields in your form that you don't want to have tracked, then add
- * a data-formchangechecker-ignore-dirty to the field, or any parent element, and it
- * will be ignored.
- *
- * If you have a submit button in your form that does not actually save the data,
- * then add a data-formchangechecker-ignore-submit attribute to it.
+ * Please note that this YUI module has been deprecated in favour of the core_form/changechecker AMD module.
*
* @class M.core.formchangechecker
- * @constructor
+ * @deprecated
*/
-var FORMCHANGECHECKERNAME = 'core-formchangechecker',
-
- FORMCHANGECHECKER = function() {
- FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
- };
-
-Y.extend(FORMCHANGECHECKER, Y.Base, {
-
- // The delegated listeners we need to detach after the initial value has been stored once
- initialvaluelisteners: [],
-
- /**
- * Initialize the module
- *
- * @method initializer
- */
- initializer: function() {
- var formid = 'form#' + this.get('formid'),
- currentform = Y.one(formid);
-
- if (!currentform) {
- // If the form was not found, then we can't check for changes.
- return;
- }
-
- if (!M.core_formchangechecker.stateinformation.formchanged) {
- M.core_formchangechecker.stateinformation.formchanged = this.get('initialdirtystate');
- }
-
- // Add a listener here for an editor restore event.
- Y.on(M.core.event.EDITOR_CONTENT_RESTORED, M.core_formchangechecker.reset_form_dirty_state, this);
-
- // Add change events to the form elements
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'input', this);
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'textarea', this);
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'select', this);
-
- // Add a focus event to check for changes which are made without triggering a change event
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'input', this));
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'textarea', this));
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'select', this));
-
- currentform.delegate('click', function() {
- currentform.setData('ignoreSubmission', true);
- }, '[data-formchangechecker-ignore-submit]');
-
- // We need any submit buttons on the form to set the submitted flag.
- Y.one(formid).on('submit', function() {
- if (currentform.getData('ignoreSubmission')) {
- // But not if we have been told to ignore this button.
- currentform.clearData('ignoreSubmission');
- return;
- }
- M.core_formchangechecker.set_form_submitted();
- }, this);
-
- // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
- // a result, the has_changed must stay in the DOM too
- window.onbeforeunload = M.core_formchangechecker.report_form_dirty_state;
- },
-
- /**
- * Store the initial value of the currently focussed element
- *
- * If an element has been focussed and changed but not yet blurred, the on change
- * event won't be fired. We need to store it's initial value to compare it in the
- * get_form_dirty_state function later.
- *
- * @method store_initial_value
- * @param {EventFacade} e
- */
- store_initial_value: function(e) {
- var thisevent;
-
- // Don't warn on elements we have been told to ignore.
- if (e.target.ancestor('.ignoredirty', true) ||
- e.target.ancestor('[data-formchangechecker-ignore-dirty]', true)) {
- return;
- }
-
- if (M.core_formchangechecker.get_form_dirty_state()) {
- // Detach all listen events to prevent duplicate initial value setting
- while (this.initialvaluelisteners.length) {
- thisevent = this.initialvaluelisteners.shift();
- thisevent.detach();
- }
-
- return;
- }
-
- // Make a note of the current element so that it can be interrogated and
- // compared in the get_form_dirty_state function
- M.core_formchangechecker.stateinformation.focused_element = {
- element: e.target,
- initial_value: e.target.get('value')
- };
- }
- },
- {
- NAME: FORMCHANGECHECKERNAME,
- ATTRS: {
- formid: {
- 'value': ''
- },
- initialdirtystate: {
- 'value': false
- }
- }
- }
+window.console.warn(
+ 'The moodle-core-formchangechecker has been deprecated ' +
+ 'and replaced with core_form/changechecker. ' +
+ 'Please update your code to make use of the new module.'
);
-M.core_formchangechecker = M.core_formchangechecker || {};
+require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.startWatching();
+});
-// We might have multiple instances of the form change protector
-M.core_formchangechecker.instances = M.core_formchangechecker.instances || [];
-M.core_formchangechecker.init = function(config) {
- var formchangechecker = new FORMCHANGECHECKER(config);
- M.core_formchangechecker.instances.push(formchangechecker);
- return formchangechecker;
-};
+// The following are provided to prevent race conditions.
+// Because the AMD module is loaded asynchronously after the YUI module is loaded, there is a possibility that the
+// calling code may call the YUI function calls before the AMD module has finished loading.
+// These will be removed in future and are automatically overwritten by the legacy helper provided as part of the new
+// changechecker AMD module.
+// eslint-disable-next-line camelcase
+M.core_formchangechecker = M.core_formchangechecker || {
+ init: function(config) {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.watchFormById(config.formid);
+ });
+ },
-// Store state information
-M.core_formchangechecker.stateinformation = [];
+ /**
+ * Set the form changed state to true
+ */
+ // eslint-disable-next-line camelcase
+ set_form_changed: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.markAllFormsAsDirty();
+ });
+ },
-/*
- * Set the form changed state to true
- */
-M.core_formchangechecker.set_form_changed = function(e) {
- // Don't warn on elements we have been told to ignore.
- if (e && e.target && (e.target.ancestor('.ignoredirty', true) ||
- e.target.ancestor('[data-formchangechecker-ignore-dirty]', true))) {
- return;
- }
+ /**
+ * Set the form submitted state to true
+ */
+ // eslint-disable-next-line camelcase
+ set_form_submitted: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.markAllFormsSubmitted();
+ });
+ },
- M.core_formchangechecker.stateinformation.formchanged = 1;
-
- // Once the form has been marked as dirty, we no longer need to keep track of form elements
- // which haven't yet blurred
- delete M.core_formchangechecker.stateinformation.focused_element;
-};
-
-/*
- * Set the form submitted state to true
- */
-M.core_formchangechecker.set_form_submitted = function() {
- M.core_formchangechecker.stateinformation.formsubmitted = 1;
-};
-
-/*
- * Attempt to determine whether the form has been modified in any way and
- * is thus 'dirty'
- *
- * @return Integer 1 is the form is dirty; 0 if not
- */
-M.core_formchangechecker.get_form_dirty_state = function() {
- var state = M.core_formchangechecker.stateinformation,
- editor;
-
- // If the form was submitted, then return a non-dirty state
- if (state.formsubmitted) {
- return 0;
- }
-
- // If any fields have been marked dirty, return a dirty state
- if (state.formchanged) {
- return 1;
- }
-
- // If a field has been focused and changed, but still has focus then the browser won't fire the
- // onChange event. We check for this eventuality here
- if (state.focused_element) {
- if (state.focused_element.element.get('value') !== state.focused_element.initial_value) {
- return 1;
- }
- }
-
- // Handle TinyMCE editor instances
- // We can't add a listener in the initializer as the editors may not have been created by that point
- // so we do so here instead
- if (typeof window.tinyMCE !== 'undefined') {
- for (editor in window.tinyMCE.editors) {
- if (window.tinyMCE.editors[editor].isDirty()) {
- return 1;
- }
- }
- }
-
- // If we reached here, then the form hasn't met any of the dirty conditions
- return 0;
-};
-
-/*
- * Reset the form state
- */
-M.core_formchangechecker.reset_form_dirty_state = function() {
- M.core_formchangechecker.stateinformation.formsubmitted = false;
- M.core_formchangechecker.stateinformation.formchanged = false;
-};
-
-/*
- * Return a suitable message if changes have been made to a form
- */
-M.core_formchangechecker.report_form_dirty_state = function(e) {
- if (!M.core_formchangechecker.get_form_dirty_state()) {
- // the form is not dirty, so don't display any message
- return;
- }
-
- // This is the error message that we'll show to browsers which support it
- var warningmessage = M.util.get_string('changesmadereallygoaway', 'moodle');
-
- if (M.cfg.behatsiterunning) {
- // If the behat site is running we don't want browser alerts.
- return;
- }
-
- // Most browsers are happy with the returnValue being set on the event
- // But some browsers do not consistently pass the event
- if (e) {
- e.returnValue = warningmessage;
- }
-
- // But some require it to be returned instead
- return warningmessage;
+ /**
+ * Reset the form state
+ */
+ // eslint-disable-next-line camelcase
+ reset_form_dirty_state: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.resetAllFormDirtyStates();
+ });
+ },
};
diff --git a/lib/yui/src/formchangechecker/js/formchangechecker.js b/lib/yui/src/formchangechecker/js/formchangechecker.js
index df48580cd33..28aea11d85b 100644
--- a/lib/yui/src/formchangechecker/js/formchangechecker.js
+++ b/lib/yui/src/formchangechecker/js/formchangechecker.js
@@ -7,245 +7,62 @@
/**
* A utility to check for form changes before navigating away from a page.
*
- * To initialise, call M.core_formchangechecker.init({formid: 'myform'}); or perhaps
- *
- * Y.use('moodle-core-formchangechecker', function() {
- * M.core_formchangechecker.init({formid: 'myform'});
- * });
- *
- * If you have some fields in your form that you don't want to have tracked, then add
- * a data-formchangechecker-ignore-dirty to the field, or any parent element, and it
- * will be ignored.
- *
- * If you have a submit button in your form that does not actually save the data,
- * then add a data-formchangechecker-ignore-submit attribute to it.
+ * Please note that this YUI module has been deprecated in favour of the core_form/changechecker AMD module.
*
* @class M.core.formchangechecker
- * @constructor
+ * @deprecated
*/
-var FORMCHANGECHECKERNAME = 'core-formchangechecker',
-
- FORMCHANGECHECKER = function() {
- FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
- };
-
-Y.extend(FORMCHANGECHECKER, Y.Base, {
-
- // The delegated listeners we need to detach after the initial value has been stored once
- initialvaluelisteners: [],
-
- /**
- * Initialize the module
- *
- * @method initializer
- */
- initializer: function() {
- var formid = 'form#' + this.get('formid'),
- currentform = Y.one(formid);
-
- if (!currentform) {
- // If the form was not found, then we can't check for changes.
- return;
- }
-
- if (!M.core_formchangechecker.stateinformation.formchanged) {
- M.core_formchangechecker.stateinformation.formchanged = this.get('initialdirtystate');
- }
-
- // Add a listener here for an editor restore event.
- Y.on(M.core.event.EDITOR_CONTENT_RESTORED, M.core_formchangechecker.reset_form_dirty_state, this);
-
- // Add change events to the form elements
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'input', this);
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'textarea', this);
- currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'select', this);
-
- // Add a focus event to check for changes which are made without triggering a change event
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'input', this));
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'textarea', this));
- this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'select', this));
-
- currentform.delegate('click', function() {
- currentform.setData('ignoreSubmission', true);
- }, '[data-formchangechecker-ignore-submit]');
-
- // We need any submit buttons on the form to set the submitted flag.
- Y.one(formid).on('submit', function() {
- if (currentform.getData('ignoreSubmission')) {
- // But not if we have been told to ignore this button.
- currentform.clearData('ignoreSubmission');
- return;
- }
- M.core_formchangechecker.set_form_submitted();
- }, this);
-
- // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
- // a result, the has_changed must stay in the DOM too
- window.onbeforeunload = M.core_formchangechecker.report_form_dirty_state;
- },
-
- /**
- * Store the initial value of the currently focussed element
- *
- * If an element has been focussed and changed but not yet blurred, the on change
- * event won't be fired. We need to store it's initial value to compare it in the
- * get_form_dirty_state function later.
- *
- * @method store_initial_value
- * @param {EventFacade} e
- */
- store_initial_value: function(e) {
- var thisevent;
-
- // Don't warn on elements we have been told to ignore.
- if (e.target.ancestor('.ignoredirty', true) ||
- e.target.ancestor('[data-formchangechecker-ignore-dirty]', true)) {
- return;
- }
-
- if (M.core_formchangechecker.get_form_dirty_state()) {
- // Detach all listen events to prevent duplicate initial value setting
- while (this.initialvaluelisteners.length) {
- thisevent = this.initialvaluelisteners.shift();
- thisevent.detach();
- }
-
- return;
- }
-
- // Make a note of the current element so that it can be interrogated and
- // compared in the get_form_dirty_state function
- M.core_formchangechecker.stateinformation.focused_element = {
- element: e.target,
- initial_value: e.target.get('value')
- };
- }
- },
- {
- NAME: FORMCHANGECHECKERNAME,
- ATTRS: {
- formid: {
- 'value': ''
- },
- initialdirtystate: {
- 'value': false
- }
- }
- }
+window.console.warn(
+ 'The moodle-core-formchangechecker has been deprecated ' +
+ 'and replaced with core_form/changechecker. ' +
+ 'Please update your code to make use of the new module.'
);
-M.core_formchangechecker = M.core_formchangechecker || {};
+require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.startWatching();
+});
-// We might have multiple instances of the form change protector
-M.core_formchangechecker.instances = M.core_formchangechecker.instances || [];
-M.core_formchangechecker.init = function(config) {
- var formchangechecker = new FORMCHANGECHECKER(config);
- M.core_formchangechecker.instances.push(formchangechecker);
- return formchangechecker;
-};
-
-// Store state information
-M.core_formchangechecker.stateinformation = [];
-
-/*
- * Set the form changed state to true
- */
-M.core_formchangechecker.set_form_changed = function(e) {
- // Don't warn on elements we have been told to ignore.
- if (e && e.target && (e.target.ancestor('.ignoredirty', true) ||
- e.target.ancestor('[data-formchangechecker-ignore-dirty]', true))) {
- return;
- }
-
- M.core_formchangechecker.stateinformation.formchanged = 1;
-
- // Once the form has been marked as dirty, we no longer need to keep track of form elements
- // which haven't yet blurred
- delete M.core_formchangechecker.stateinformation.focused_element;
-};
-
-/*
- * Set the form submitted state to true
- */
-M.core_formchangechecker.set_form_submitted = function() {
- M.core_formchangechecker.stateinformation.formsubmitted = 1;
-};
-
-/*
- * Attempt to determine whether the form has been modified in any way and
- * is thus 'dirty'
- *
- * @return Integer 1 is the form is dirty; 0 if not
- */
-M.core_formchangechecker.get_form_dirty_state = function() {
- var state = M.core_formchangechecker.stateinformation,
- editor;
-
- // If the form was submitted, then return a non-dirty state
- if (state.formsubmitted) {
- return 0;
- }
-
- // If any fields have been marked dirty, return a dirty state
- if (state.formchanged) {
- return 1;
- }
-
- // If a field has been focused and changed, but still has focus then the browser won't fire the
- // onChange event. We check for this eventuality here
- if (state.focused_element) {
- if (state.focused_element.element.get('value') !== state.focused_element.initial_value) {
- return 1;
- }
- }
-
- // Handle TinyMCE editor instances
- // We can't add a listener in the initializer as the editors may not have been created by that point
- // so we do so here instead
- if (typeof window.tinyMCE !== 'undefined') {
- for (editor in window.tinyMCE.editors) {
- if (window.tinyMCE.editors[editor].isDirty()) {
- return 1;
- }
- }
- }
-
- // If we reached here, then the form hasn't met any of the dirty conditions
- return 0;
-};
-
-/*
- * Reset the form state
- */
-M.core_formchangechecker.reset_form_dirty_state = function() {
- M.core_formchangechecker.stateinformation.formsubmitted = false;
- M.core_formchangechecker.stateinformation.formchanged = false;
-};
-
-/*
- * Return a suitable message if changes have been made to a form
- */
-M.core_formchangechecker.report_form_dirty_state = function(e) {
- if (!M.core_formchangechecker.get_form_dirty_state()) {
- // the form is not dirty, so don't display any message
- return;
- }
-
- // This is the error message that we'll show to browsers which support it
- var warningmessage = M.util.get_string('changesmadereallygoaway', 'moodle');
-
- if (M.cfg.behatsiterunning) {
- // If the behat site is running we don't want browser alerts.
- return;
- }
-
- // Most browsers are happy with the returnValue being set on the event
- // But some browsers do not consistently pass the event
- if (e) {
- e.returnValue = warningmessage;
- }
-
- // But some require it to be returned instead
- return warningmessage;
+// The following are provided to prevent race conditions.
+// Because the AMD module is loaded asynchronously after the YUI module is loaded, there is a possibility that the
+// calling code may call the YUI function calls before the AMD module has finished loading.
+// These will be removed in future and are automatically overwritten by the legacy helper provided as part of the new
+// changechecker AMD module.
+// eslint-disable-next-line camelcase
+M.core_formchangechecker = M.core_formchangechecker || {
+ init: function(config) {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.watchFormById(config.formid);
+ });
+ },
+
+ /**
+ * Set the form changed state to true
+ */
+ // eslint-disable-next-line camelcase
+ set_form_changed: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.markAllFormsAsDirty();
+ });
+ },
+
+ /**
+ * Set the form submitted state to true
+ */
+ // eslint-disable-next-line camelcase
+ set_form_submitted: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.markAllFormsSubmitted();
+ });
+ },
+
+ /**
+ * Reset the form state
+ */
+ // eslint-disable-next-line camelcase
+ reset_form_dirty_state: function() {
+ require(['core_form/changechecker'], function(ChangeChecker) {
+ ChangeChecker.resetAllFormDirtyStates();
+ });
+ },
};