From cbe19158e95f1d3597bfc89c12fc76efd573dedd Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 12 Oct 2020 14:35:06 +0800 Subject: [PATCH] MDL-69918 core: Rewrite Form Change Checker a ES6 --- lib/form/amd/build/changechecker.min.js | 2 + lib/form/amd/build/changechecker.min.js.map | 1 + lib/form/amd/src/changechecker.js | 546 ++++++++++++++++++ lib/upgrade.txt | 2 + .../moodle-core-formchangechecker-debug.js | 279 ++------- .../moodle-core-formchangechecker-min.js | 2 +- .../moodle-core-formchangechecker.js | 279 ++------- .../formchangechecker/js/formchangechecker.js | 285 ++------- 8 files changed, 699 insertions(+), 697 deletions(-) create mode 100644 lib/form/amd/build/changechecker.min.js create mode 100644 lib/form/amd/build/changechecker.min.js.map create mode 100644 lib/form/amd/src/changechecker.js 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 * \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(); + }); + }, };