diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 780038d4cec86..f6aa32f383edf 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -248,6 +248,7 @@ $string['idnumbercoursecategory'] = 'Category ID number'; $string['idnumbercoursecategory_help'] = 'The ID number of a course category is only used when matching the category against external systems and is not displayed anywhere on the site. If the category has an official code name it may be entered, otherwise the field can be left blank.'; $string['categoryupdated'] = 'The category \'{$a}\' was updated'; +$string['changesmade'] = 'Changes made'; $string['changesmadereallygoaway'] = 'You have made changes. Are you sure you want to navigate away and lose your changes?'; $string['city'] = 'City/town'; $string['cleaningtempdata'] = 'Cleaning temp data'; diff --git a/lib/amd/build/dynamic_tabs.min.js b/lib/amd/build/dynamic_tabs.min.js index 9f33266c5be9a..51bdd53451922 100644 --- a/lib/amd/build/dynamic_tabs.min.js +++ b/lib/amd/build/dynamic_tabs.min.js @@ -1,2 +1,2 @@ -define ("core/dynamic_tabs",["exports","jquery","core/templates","core/notification","core/pending","core/local/repository/dynamic_tabs"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=g(b);c=g(c);d=g(d);e=g(e);function g(a){return a&&a.__esModule?a:{default:a}}function h(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function i(a){for(var b=1,c;ba.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Dynamic Tabs UI element with AJAX loading of tabs content\n *\n * @module core/dynamic_tabs\n * @copyright 2021 David Matamoros based on code from Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {getContent} from 'core/local/repository/dynamic_tabs';\n\nconst SELECTORS = {\n dynamicTabs: '.dynamictabs',\n activeTab: '.dynamictabs .nav-link.active',\n allActiveTabs: '.dynamictabs .nav-link[data-toggle=\"tab\"]:not(.disabled)',\n tabContent: '.dynamictabs .tab-pane [data-tab-content]',\n tabToggle: 'a[data-toggle=\"tab\"]',\n tabPane: '.dynamictabs .tab-pane',\n};\n\nSELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content=\"${tabName}\"]`;\nSELECTORS.forTabId = tabName => `.dynamictabs [data-toggle=\"tab\"][href=\"#${tabName}\"]`;\n\n/**\n * Initialises the tabs view on the page (only one tabs view per page is supported)\n */\nexport const init = () => {\n // This code listens to Bootstrap event 'shown.bs.tab' which is triggered using JQuery and\n // can not be converted yet to native events.\n $(SELECTORS.tabToggle).on('shown.bs.tab', function() {\n const tab = $($(this).attr('href'));\n if (tab.length !== 1) {\n return;\n }\n loadTab(tab.attr('id'));\n });\n\n if (!openTabFromHash()) {\n const tabs = document.querySelector(SELECTORS.allActiveTabs);\n if (tabs) {\n openTab(tabs.getAttribute('aria-controls'));\n } else {\n // We may hide tabs if there is only one available, just load the contents of the first tab.\n const tabPane = document.querySelector(SELECTORS.tabPane);\n if (tabPane) {\n tabPane.classList.add('active', 'show');\n loadTab(tabPane.getAttribute('id'));\n }\n }\n }\n};\n\n/**\n * Show \"loading\" template instead of a node\n *\n * @param {HTMLElement} node\n * @return {Promise}\n */\nconst indicateNodeIsLoading = (node) => {\n return Templates.render('core/loading', {})\n .then((html, js) => {\n return Templates.replaceNodeContents(node, html, js);\n }).catch(Notification.exception);\n};\n\n/**\n * Returns id/name of the currently active tab\n *\n * @return {String|null}\n */\nconst getActiveTabName = () => {\n const element = document.querySelector(SELECTORS.activeTab);\n return element?.getAttribute('aria-controls') || null;\n};\n\n/**\n * Returns the id/name of the first tab\n *\n * @return {String|null}\n */\nconst getFirstTabName = () => {\n const element = document.querySelector(SELECTORS.tabContent);\n return element?.dataset.tabContent || null;\n};\n\n/**\n * Loads contents of a tab using an AJAX request\n *\n * @param {String} tabName\n * @param {Object} additionalData additional data to pass to WS\n */\nconst loadTab = (tabName, additionalData = {}) => {\n // If tabName is not specified find the active tab, or if is not defined, the first available tab.\n tabName = tabName ?? getActiveTabName() ?? getFirstTabName();\n\n const tab = document.querySelector(SELECTORS.forTabName(tabName));\n if (!tab) {\n return;\n }\n\n const pendingPromise = new Pending('core/dynamic_tabs:loadTab:' + tabName);\n const tabdata = tab.closest(SELECTORS.dynamicTabs);\n const wsData = {\n 'reportid': tabdata.dataset.reportid,\n 'id': tabdata.dataset.id,\n ...additionalData\n };\n let tabjs = '';\n tab.textContent = '';\n\n indicateNodeIsLoading(tab)\n .then(() => {\n return getContent(tab.dataset.tabClass, JSON.stringify(wsData));\n })\n .then((data) => {\n tabjs = data.javascript;\n return Templates.render(data.template, JSON.parse(data.content));\n })\n .then((html, js) => {\n return Templates.replaceNodeContents(tab, html, js + tabjs);\n })\n .then(() => {\n pendingPromise.resolve();\n return null;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Return the tab given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTab = (tabName) => {\n return document.querySelector(SELECTORS.forTabId(tabName));\n};\n\n/**\n * Return the tab pane given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTabPane = (tabName) => {\n return document.getElementById(tabName);\n};\n\n/**\n * Open the tab on page load. If this script loads before theme_boost/tab we need to open tab ourselves\n *\n * @param {String} tabName\n * @return {Boolean}\n */\nconst openTab = (tabName) => {\n const tab = getTab(tabName);\n if (!tab) {\n return false;\n }\n\n loadTab(tabName);\n tab.classList.add('active');\n getTabPane(tabName).classList.add('active', 'show');\n return true;\n};\n\n/**\n * If there is a location hash that is the same as the tab name - open this tab.\n *\n * @return {Boolean}\n */\nconst openTabFromHash = () => {\n const hash = document.location.hash;\n if (hash.match(/^#\\w+$/g)) {\n return openTab(hash.replace(/^#/g, ''));\n }\n\n return false;\n};\n"],"file":"dynamic_tabs.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/dynamic_tabs.js"],"names":["SELECTORS","dynamicTabs","activeTab","allActiveTabs","tabContent","tabToggle","tabPane","forTabName","tabName","forTabId","init","on","event","preventDefault","stopPropagation","key","component","then","strChangesMade","strChangesMadeReally","strConfirm","Notification","confirm","target","trigger","type","catch","exception","tab","attr","length","loadTab","openTabFromHash","tabs","document","querySelector","openTab","getAttribute","classList","add","indicateNodeIsLoading","node","Templates","render","html","js","replaceNodeContents","getActiveTabName","element","getFirstTabName","dataset","additionalData","pendingPromise","Pending","tabdata","closest","wsData","reportid","id","tabjs","textContent","tabClass","JSON","stringify","data","javascript","template","parse","content","resolve","getTab","getTabPane","getElementById","hash","location","match","replace"],"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 * Dynamic Tabs UI element with AJAX loading of tabs content\n *\n * @module core/dynamic_tabs\n * @copyright 2021 David Matamoros based on code from Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_strings as getStrings} from 'core/str';\nimport {getContent} from 'core/local/repository/dynamic_tabs';\nimport {isAnyWatchedFormDirty, resetAllFormDirtyStates} from 'core_form/changechecker';\n\nconst SELECTORS = {\n dynamicTabs: '.dynamictabs',\n activeTab: '.dynamictabs .nav-link.active',\n allActiveTabs: '.dynamictabs .nav-link[data-toggle=\"tab\"]:not(.disabled)',\n tabContent: '.dynamictabs .tab-pane [data-tab-content]',\n tabToggle: 'a[data-toggle=\"tab\"]',\n tabPane: '.dynamictabs .tab-pane',\n};\n\nSELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content=\"${tabName}\"]`;\nSELECTORS.forTabId = tabName => `.dynamictabs [data-toggle=\"tab\"][href=\"#${tabName}\"]`;\n\n/**\n * Initialises the tabs view on the page (only one tabs view per page is supported)\n */\nexport const init = () => {\n const tabToggle = $(SELECTORS.tabToggle);\n\n // Listen to click, warn user if they are navigating away with unsaved form changes.\n tabToggle.on('click', (event) => {\n if (!isAnyWatchedFormDirty()) {\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n\n getStrings([\n {key: 'changesmade', component: 'moodle'},\n {key: 'changesmadereallygoaway', component: 'moodle'},\n {key: 'confirm', component: 'moodle'},\n ]).then(([strChangesMade, strChangesMadeReally, strConfirm]) =>\n // Reset form dirty state on confirmation, re-trigger the event.\n Notification.confirm(strChangesMade, strChangesMadeReally, strConfirm, null, () => {\n resetAllFormDirtyStates();\n $(event.target).trigger(event.type);\n })\n ).catch(Notification.exception);\n });\n\n // This code listens to Bootstrap event 'shown.bs.tab' which is triggered using JQuery and\n // can not be converted yet to native events.\n tabToggle.on('shown.bs.tab', function() {\n const tab = $($(this).attr('href'));\n if (tab.length !== 1) {\n return;\n }\n loadTab(tab.attr('id'));\n });\n\n if (!openTabFromHash()) {\n const tabs = document.querySelector(SELECTORS.allActiveTabs);\n if (tabs) {\n openTab(tabs.getAttribute('aria-controls'));\n } else {\n // We may hide tabs if there is only one available, just load the contents of the first tab.\n const tabPane = document.querySelector(SELECTORS.tabPane);\n if (tabPane) {\n tabPane.classList.add('active', 'show');\n loadTab(tabPane.getAttribute('id'));\n }\n }\n }\n};\n\n/**\n * Show \"loading\" template instead of a node\n *\n * @param {HTMLElement} node\n * @return {Promise}\n */\nconst indicateNodeIsLoading = (node) => {\n return Templates.render('core/loading', {})\n .then((html, js) => {\n return Templates.replaceNodeContents(node, html, js);\n }).catch(Notification.exception);\n};\n\n/**\n * Returns id/name of the currently active tab\n *\n * @return {String|null}\n */\nconst getActiveTabName = () => {\n const element = document.querySelector(SELECTORS.activeTab);\n return element?.getAttribute('aria-controls') || null;\n};\n\n/**\n * Returns the id/name of the first tab\n *\n * @return {String|null}\n */\nconst getFirstTabName = () => {\n const element = document.querySelector(SELECTORS.tabContent);\n return element?.dataset.tabContent || null;\n};\n\n/**\n * Loads contents of a tab using an AJAX request\n *\n * @param {String} tabName\n * @param {Object} additionalData additional data to pass to WS\n */\nconst loadTab = (tabName, additionalData = {}) => {\n // If tabName is not specified find the active tab, or if is not defined, the first available tab.\n tabName = tabName ?? getActiveTabName() ?? getFirstTabName();\n\n const tab = document.querySelector(SELECTORS.forTabName(tabName));\n if (!tab) {\n return;\n }\n\n const pendingPromise = new Pending('core/dynamic_tabs:loadTab:' + tabName);\n const tabdata = tab.closest(SELECTORS.dynamicTabs);\n const wsData = {\n 'reportid': tabdata.dataset.reportid,\n 'id': tabdata.dataset.id,\n ...additionalData\n };\n let tabjs = '';\n tab.textContent = '';\n\n indicateNodeIsLoading(tab)\n .then(() => {\n return getContent(tab.dataset.tabClass, JSON.stringify(wsData));\n })\n .then((data) => {\n tabjs = data.javascript;\n return Templates.render(data.template, JSON.parse(data.content));\n })\n .then((html, js) => {\n return Templates.replaceNodeContents(tab, html, js + tabjs);\n })\n .then(() => {\n pendingPromise.resolve();\n return null;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Return the tab given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTab = (tabName) => {\n return document.querySelector(SELECTORS.forTabId(tabName));\n};\n\n/**\n * Return the tab pane given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTabPane = (tabName) => {\n return document.getElementById(tabName);\n};\n\n/**\n * Open the tab on page load. If this script loads before theme_boost/tab we need to open tab ourselves\n *\n * @param {String} tabName\n * @return {Boolean}\n */\nconst openTab = (tabName) => {\n const tab = getTab(tabName);\n if (!tab) {\n return false;\n }\n\n loadTab(tabName);\n tab.classList.add('active');\n getTabPane(tabName).classList.add('active', 'show');\n return true;\n};\n\n/**\n * If there is a location hash that is the same as the tab name - open this tab.\n *\n * @return {Boolean}\n */\nconst openTabFromHash = () => {\n const hash = document.location.hash;\n if (hash.match(/^#\\w+$/g)) {\n return openTab(hash.replace(/^#/g, ''));\n }\n\n return false;\n};\n"],"file":"dynamic_tabs.min.js"} \ No newline at end of file diff --git a/lib/amd/src/dynamic_tabs.js b/lib/amd/src/dynamic_tabs.js index c89f621d280a9..dfe57829e5544 100644 --- a/lib/amd/src/dynamic_tabs.js +++ b/lib/amd/src/dynamic_tabs.js @@ -25,7 +25,9 @@ import $ from 'jquery'; import Templates from 'core/templates'; import Notification from 'core/notification'; import Pending from 'core/pending'; +import {get_strings as getStrings} from 'core/str'; import {getContent} from 'core/local/repository/dynamic_tabs'; +import {isAnyWatchedFormDirty, resetAllFormDirtyStates} from 'core_form/changechecker'; const SELECTORS = { dynamicTabs: '.dynamictabs', @@ -43,9 +45,33 @@ SELECTORS.forTabId = tabName => `.dynamictabs [data-toggle="tab"][href="#${tabNa * Initialises the tabs view on the page (only one tabs view per page is supported) */ export const init = () => { + const tabToggle = $(SELECTORS.tabToggle); + + // Listen to click, warn user if they are navigating away with unsaved form changes. + tabToggle.on('click', (event) => { + if (!isAnyWatchedFormDirty()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + getStrings([ + {key: 'changesmade', component: 'moodle'}, + {key: 'changesmadereallygoaway', component: 'moodle'}, + {key: 'confirm', component: 'moodle'}, + ]).then(([strChangesMade, strChangesMadeReally, strConfirm]) => + // Reset form dirty state on confirmation, re-trigger the event. + Notification.confirm(strChangesMade, strChangesMadeReally, strConfirm, null, () => { + resetAllFormDirtyStates(); + $(event.target).trigger(event.type); + }) + ).catch(Notification.exception); + }); + // This code listens to Bootstrap event 'shown.bs.tab' which is triggered using JQuery and // can not be converted yet to native events. - $(SELECTORS.tabToggle).on('shown.bs.tab', function() { + tabToggle.on('shown.bs.tab', function() { const tab = $($(this).attr('href')); if (tab.length !== 1) { return; diff --git a/lib/form/amd/build/changechecker.min.js b/lib/form/amd/build/changechecker.min.js index 54977b454372f..5098348e600f7 100644 --- a/lib/form/amd/build/changechecker.min.js +++ b/lib/form/amd/build/changechecker.min.js @@ -1,2 +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",u)}},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()}); +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.isAnyWatchedFormDirty=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};a.isAnyWatchedFormDirty=o;var 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",u)}},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 index b62a1e61a2c8b..43e3e44c31c33 100644 --- a/lib/form/amd/build/changechecker.min.js.map +++ b/lib/form/amd/build/changechecker.min.js.map @@ -1 +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","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', markAllFormsSubmitted),\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 +{"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","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 */\nexport const isAnyWatchedFormDirty = () => {\n if (formChangeCheckerDisabled) {\n // The form change checker is disabled.\n return false;\n }\n\n const hasSubmittedForm = watchedForms.some(watchedForm => watchedForm.dataset.formSubmitted === \"true\");\n if (hasSubmittedForm) {\n // Do not warn about submitted forms, ever.\n return false;\n }\n\n const hasDirtyForm = watchedForms.some(watchedForm => {\n if (!watchedForm.isConnected) {\n // The watched form is not connected to the DOM.\n return false;\n }\n\n if (watchedForm.dataset.formDirty === \"true\") {\n // The form has been marked as dirty.\n return true;\n }\n\n // Elements currently holding focus will not have triggered change detection.\n // Check whether the value matches the original value upon form load.\n if (document.activeElement && document.activeElement.dataset.propertyIsEnumerable('initialValue')) {\n const isActiveElementWatched = isWatchingForm(document.activeElement);\n 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', markAllFormsSubmitted),\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 index 68f9a6cd26b7d..6c4e2481094f2 100644 --- a/lib/form/amd/src/changechecker.js +++ b/lib/form/amd/src/changechecker.js @@ -226,9 +226,8 @@ export const disableAllChecks = () => { * * @method * @returns {Bool} - * @private */ -const isAnyWatchedFormDirty = () => { +export const isAnyWatchedFormDirty = () => { if (formChangeCheckerDisabled) { // The form change checker is disabled. return false;