{"version":3,"file":"changechecker.min.js","sources":["https:\/\/climatefinance.afci.de\/lib\/form\/amd\/src\/changechecker.js"],"sourcesContent":["\/\/ This file is part of Moodle - http:\/\/moodle.org\/\n\/\/\n\/\/ Moodle is free software: you can redistribute it and\/or modify\n\/\/ it under the terms of the GNU General Public License as published by\n\/\/ the Free Software Foundation, either version 3 of the License, or\n\/\/ (at your option) any later version.\n\/\/\n\/\/ Moodle is distributed in the hope that it will be useful,\n\/\/ but WITHOUT ANY WARRANTY; without even the implied warranty of\n\/\/ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n\/\/ GNU General Public License for more details.\n\/\/\n\/\/ You should have received a copy of the GNU General Public License\n\/\/ along with Moodle. If not, see .\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<\/caption>\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.<\/caption>\n *\n * import {watchForm} from 'core_form\/changechecker';\n *\n * \/\/ Watch the form by using a child of it.\n * watchForm(document.querySelector('input[data-foo=\"bar\"]'););\n *\n * @example Usage from within a template<\/caption>\n *
\n * \n * <\/form>\n * {{#js}}\n * require(['core_form\/changechecker'], function(changeChecker) {\n * watchFormById('mod_example-entry-{{uniqid}}');\n * });\n * {{\/js}}\n *\/\n\nimport {eventTypes} from 'core_editor\/events';\nimport {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<\/caption>\n * import {unWatchForm} from 'core_form\/changechecker';\n *\n * \/\/ ...\n * document.addEventListener('click', e => {\n * if (e.target.closest('[data-action=\"changePage\"]')) {\n * unWatchForm(e.target);\n * }\n * });\n *\/\nexport const unWatchForm = formNode => {\n watchedForms = watchedForms.filter(watchedForm => !!watchedForm.contains(formNode));\n};\n\n\/**\n * Reset the 'dirty' flag for all watched forms.\n *\n * If a form was previously marked as 'dirty', then this flag will be cleared and when the page is unloaded no warning\n * will be shown.\n *\n * @method\n *\/\nexport const resetAllFormDirtyStates = () => {\n watchedForms.forEach(watchedForm => {\n watchedForm.dataset.formSubmitted = \"false\";\n watchedForm.dataset.formDirty = \"false\";\n });\n};\n\n\/**\n * Reset the 'dirty' flag of the specified form.\n *\n * @method\n * @param {HTMLElement} formNode\n *\/\nexport const resetFormDirtyState = formNode => {\n formNode = getFormFromChild(formNode);\n\n if (!formNode) {\n return;\n }\n\n formNode.dataset.formSubmitted = \"false\";\n formNode.dataset.formDirty = \"false\";\n};\n\n\/**\n * Mark all forms as dirty.\n *\n * This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.\n * It will be removed in Moodle 4.4.\n *\n * @method\n *\/\nexport const markAllFormsAsDirty = () => {\n watchedForms.forEach(watchedForm => {\n watchedForm.dataset.formDirty = \"true\";\n });\n};\n\n\/**\n * Mark a specific form as dirty.\n *\n * This behaviour may be required for custom form elements which are not caught by the standard change listeners.\n *\n * @method\n * @param {HTMLElement} formNode\n *\/\nexport const markFormAsDirty = formNode => {\n formNode = getFormFromChild(formNode);\n\n if (!formNode) {\n return;\n }\n\n \/\/ Mark it as dirty.\n formNode.dataset.formDirty = \"true\";\n};\n\n\/**\n * Actively disable the form change checker.\n *\n * Please note that it cannot be re-enabled once disabled.\n *\n * @method\n *\/\nexport const disableAllChecks = () => {\n formChangeCheckerDisabled = true;\n};\n\n\/**\n * Check whether any watched from is dirty.\n *\n * @method\n * @returns {Bool}\n *\/\nexport const isAnyWatchedFormDirty = () => {\n if (formChangeCheckerDisabled) {\n \/\/ The form change checker is disabled.\n return false;\n }\n\n const hasSubmittedForm = watchedForms.some(watchedForm => watchedForm.dataset.formSubmitted === \"true\");\n if (hasSubmittedForm) {\n \/\/ Do not warn about submitted forms, ever.\n return false;\n }\n\n const hasDirtyForm = watchedForms.some(watchedForm => {\n if (!watchedForm.isConnected) {\n \/\/ The watched form is not connected to the DOM.\n return false;\n }\n\n if (watchedForm.dataset.formDirty === \"true\") {\n \/\/ The form has been marked as dirty.\n return true;\n }\n\n \/\/ Elements currently holding focus will not have triggered change detection.\n \/\/ Check whether the value matches the original value upon form load.\n if (document.activeElement && document.activeElement.dataset.propertyIsEnumerable('initialValue')) {\n const isActiveElementWatched = isWatchingForm(document.activeElement);\n const hasValueChanged = document.activeElement.dataset.initialValue !== document.activeElement.value;\n\n if (isActiveElementWatched && hasValueChanged) {\n return true;\n }\n }\n\n return false;\n });\n\n if (hasDirtyForm) {\n \/\/ At least one form is dirty.\n return true;\n }\n\n \/\/ Handle TinyMCE editor instances.\n \/\/ TinyMCE forms may not have been initialised at the time that startWatching is called.\n \/\/ Check whether any tinyMCE editor is dirty.\n if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {\n if (window.tinyMCE.editors.some(editor => editor.isDirty())) {\n return true;\n }\n }\n\n \/\/ No dirty forms detected.\n return false;\n};\n\n\/**\n * Get the watched form for the specified target.\n *\n * @method\n * @param {HTMLNode} target\n * @returns {HTMLFormElement}\n * @private\n *\/\nconst getFormForNode = target => watchedForms.find(watchedForm => watchedForm.contains(target));\n\n\/**\n * Whether the specified target is a watched form.\n *\n * @method\n * @param {HTMLNode} target\n * @returns {Bool}\n * @private\n *\/\nconst isWatchingForm = target => watchedForms.some(watchedForm => watchedForm.contains(target));\n\n\/**\n * Whether the specified target should ignore changes or not.\n *\n * @method\n * @param {HTMLNode} target\n * @returns {Bool}\n * @private\n *\/\nconst shouldIgnoreChangesForNode = target => !!target.closest('.ignoredirty');\n\n\/**\n * Mark a form as changed.\n *\n * @method\n * @param {HTMLElement} changedNode An element in the form which was changed\n *\/\nexport const markFormChangedFromNode = changedNode => {\n if (changedNode.dataset.formChangeCheckerOverride) {\n \/\/ Changes to this form node disable the form change checker entirely.\n \/\/ This is intended for select fields which cause an immediate redirect.\n disableAllChecks();\n return;\n }\n\n if (!isWatchingForm(changedNode)) {\n return;\n }\n\n if (shouldIgnoreChangesForNode(changedNode)) {\n return;\n }\n\n \/\/ Mark the form as dirty.\n const formNode = getFormForNode(changedNode);\n formNode.dataset.formDirty = \"true\";\n};\n\n\/**\n * Mark a form as submitted.\n *\n * @method\n * @param {HTMLElement} formNode An element in the form to mark as submitted\n *\/\nexport const markFormSubmitted = formNode => {\n formNode = getFormFromChild(formNode);\n\n if (!formNode) {\n return;\n }\n\n formNode.dataset.formSubmitted = \"true\";\n};\n\n\/**\n * Mark all forms as submitted.\n *\n * This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.\n * It will be removed in Moodle 4.4.\n *\n * @method\n *\/\nexport const markAllFormsSubmitted = () => {\n watchedForms.forEach(watchedForm => markFormSubmitted(watchedForm));\n};\n\n\/**\n * Handle the beforeunload event.\n *\n * @method\n * @param {Event} e\n * @returns {string|null}\n * @private\n *\/\nconst beforeUnloadHandler = e => {\n \/\/ Please note: The use of Promises in this function is forbidden.\n \/\/ This is an event handler and _cannot_ be asynchronous.\n let warnBeforeUnload = isAnyWatchedFormDirty() && !M.cfg.behatsiterunning;\n if (warnBeforeUnload) {\n \/\/ According to the specification, to show the confirmation dialog an event handler should call preventDefault()\n \/\/ on the event.\n e.preventDefault();\n\n \/\/ However note that not all browsers support this method, and some instead require the event handler to\n \/\/ implement one of two legacy methods:\n \/\/ * assigning a string to the event's returnValue property; and\n \/\/ * returning a string from the event handler.\n\n \/\/ Assigning a string to the event's returnValue property.\n e.returnValue = warningString;\n\n \/\/ Returning a string from the event handler.\n return e.returnValue;\n }\n\n \/\/ Attaching an event handler\/listener to window or document's beforeunload event prevents browsers from using\n \/\/ in-memory page navigation caches, like Firefox's Back-Forward cache or WebKit's Page Cache.\n \/\/ Remove the handler.\n window.removeEventListener('beforeunload', beforeUnloadHandler);\n\n return null;\n};\n\n\/**\n * Start watching for form changes.\n *\n * This function is called on module load, and should not normally be called.\n *\n * @method\n * @protected\n *\/\nexport const startWatching = () => {\n if (initialised) {\n return;\n }\n\n \/\/ 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"],"names":["warningString","watchedForms","formChangeCheckerDisabled","getFormFromChild","formChild","closest","watchForm","formNode","isWatchingForm","push","filter","watchedForm","contains","resetAllFormDirtyStates","forEach","dataset","formSubmitted","formDirty","resetFormDirtyState","markAllFormsAsDirty","markFormAsDirty","disableAllChecks","isAnyWatchedFormDirty","some","isConnected","document","activeElement","propertyIsEnumerable","isActiveElementWatched","hasValueChanged","initialValue","value","window","tinyMCE","editors","editor","isDirty","target","markFormChangedFromNode","changedNode","formChangeCheckerOverride","find","getFormForNode","markFormSubmitted","markAllFormsSubmitted","beforeUnloadHandler","e","M","cfg","behatsiterunning","preventDefault","returnValue","removeEventListener","startWatching","addLegacyFunctions","addEventListener","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"],"mappings":"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\/BxD,gBAAgBK,SAASoD,eAAeD,UAI5CvB"}