From e395fe3a9ae1bc09e8099043cbbfd378756f5678 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 23 Apr 2025 16:12:29 +0100 Subject: [PATCH] MDL-85264 output: fix form change checker when switching tabs. During the Bootstrap 5.0 upgrade the event handling within dynamic tabs appears to have changed such that the "click" event happens too late for the form change checker module to determine whether there were any "dirty" forms on the tab being switched from. This is related to, although not specifically caused by, changes made in 0c586b41. --- lib/amd/build/dynamic_tabs.min.js | 2 +- lib/amd/build/dynamic_tabs.min.js.map | 2 +- lib/amd/src/dynamic_tabs.js | 50 ++++++++++++++++----------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/lib/amd/build/dynamic_tabs.min.js b/lib/amd/build/dynamic_tabs.min.js index 7a4ea705f26..249ef7bf02d 100644 --- a/lib/amd/build/dynamic_tabs.min.js +++ b/lib/amd/build/dynamic_tabs.min.js @@ -5,6 +5,6 @@ define("core/dynamic_tabs",["exports","jquery","core/templates","core/loadingico * @module core/dynamic_tabs * @copyright 2021 David Matamoros based on code from Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const SELECTORS={dynamicTabs:".dynamictabs",activeTab:".dynamictabs .nav-link.active",allActiveTabs:'.dynamictabs .nav-link[data-bs-toggle="tab"]:not(.disabled)',tabContent:".dynamictabs .tab-pane [data-tab-content]",tabToggle:'a[data-bs-toggle="tab"]',tabPane:".dynamictabs .tab-pane",forTabName:tabName=>'.dynamictabs [data-tab-content="'.concat(tabName,'"]'),forTabId:tabName=>'.dynamictabs [data-bs-toggle="tab"][href="#'.concat(tabName,'"]')};_exports.init=()=>{if(document.querySelectorAll(SELECTORS.tabToggle).forEach((tabToggle=>{tabToggle.addEventListener("click",(event=>{(0,_changechecker.isAnyWatchedFormDirty)()&&(event.preventDefault(),event.stopPropagation(),(0,_str.getStrings)([{key:"changesmade",component:"moodle"},{key:"changesmadereallygoaway",component:"moodle"},{key:"confirm",component:"moodle"}]).then((_ref=>{let[strChangesMade,strChangesMadeReally,strConfirm]=_ref;return _notification.default.confirm(strChangesMade,strChangesMadeReally,strConfirm,null,(()=>{(0,_changechecker.resetAllFormDirtyStates)(),(0,_jquery.default)(event.target).trigger(event.type)}))})).catch(_notification.default.exception))})),tabToggle.addEventListener("show.bs.tab",(()=>{const previousTabName=getActiveTabName();if(previousTabName){document.querySelector(SELECTORS.forTabName(previousTabName)).textContent=""}})),tabToggle.addEventListener("shown.bs.tab",(()=>{const tabPane=document.getElementById(tabToggle.getAttribute("href").replace(/^#/,""));tabPane&&loadTab(tabPane.id)}))})),!openTabFromHash()){const tabs=document.querySelector(SELECTORS.allActiveTabs);if(tabs)openTab(tabs.getAttribute("aria-controls"));else{const tabPane=document.querySelector(SELECTORS.tabPane);tabPane&&(tabPane.classList.add("active","show"),loadTab(tabPane.getAttribute("id")))}}};const getActiveTabName=()=>{const element=document.querySelector(SELECTORS.activeTab);return(null==element?void 0:element.getAttribute("aria-controls"))||null},loadTab=tabName=>{var _ref2,_tabName;tabName=null!==(_ref2=null!==(_tabName=tabName)&&void 0!==_tabName?_tabName:getActiveTabName())&&void 0!==_ref2?_ref2:(()=>{const element=document.querySelector(SELECTORS.tabContent);return(null==element?void 0:element.dataset.tabContent)||null})();const tab=document.querySelector(SELECTORS.forTabName(tabName));if(!tab)return;const pendingPromise=new _pending.default("core/dynamic_tabs:loadTab:"+tabName);(0,_loadingicon.addIconToContainer)(tab).then((()=>{let tabArgs={...tab.dataset};return delete tabArgs.tabClass,delete tabArgs.tabContent,(0,_dynamic_tabs.getContent)(tab.dataset.tabClass,JSON.stringify(tabArgs))})).then((response=>Promise.all([_jquery.default.parseHTML(response.javascript,null,!0).map((node=>node.innerHTML)).join("\n"),_templates.default.renderForPromise(response.template,JSON.parse(response.content))]))).then((_ref3=>{let[responseJs,{html:html,js:js}]=_ref3;return _templates.default.replaceNodeContents(tab,html,js+responseJs)})).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)},openTab=tabName=>{const tab=(tabName=>document.querySelector(SELECTORS.forTabId(tabName)))(tabName);return!!tab&&(loadTab(tabName),tab.classList.add("active"),(tabName=>document.getElementById(tabName))(tabName).classList.add("active","show"),!0)},openTabFromHash=()=>{const hash=document.location.hash;return!!hash.match(/^#\w+$/g)&&openTab(hash.replace(/^#/g,""))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const SELECTORS={dynamicTabs:".dynamictabs",activeTab:".dynamictabs .nav-link.active",allActiveTabs:'.dynamictabs .nav-link[data-bs-toggle="tab"]:not(.disabled)',tabContent:".dynamictabs .tab-pane [data-tab-content]",tabToggle:'a[data-bs-toggle="tab"]',tabPane:".dynamictabs .tab-pane",forTabName:tabName=>'.dynamictabs [data-tab-content="'.concat(tabName,'"]'),forTabId:tabName=>'.dynamictabs [data-bs-toggle="tab"][href="#'.concat(tabName,'"]')};let watchedFormDirtyNotification=!1;_exports.init=()=>{if(document.querySelectorAll(SELECTORS.tabToggle).forEach((tabToggle=>{tabToggle.addEventListener("show.bs.tab",(event=>{if((0,_changechecker.isAnyWatchedFormDirty)())return event.preventDefault(),event.stopPropagation(),void(watchedFormDirtyNotification||(watchedFormDirtyNotification=!0,_notification.default.saveCancelPromise((0,_str.getString)("changesmade"),(0,_str.getString)("changesmadereallygoaway"),(0,_str.getString)("confirm"),{triggerElement:tabToggle}).then((()=>{(0,_changechecker.resetAllFormDirtyStates)(),tabToggle.dispatchEvent(new Event("click",{bubbles:!0}))})).catch((()=>{})).finally((()=>{watchedFormDirtyNotification=!1}))));const previousTabName=getActiveTabName();if(previousTabName){document.querySelector(SELECTORS.forTabName(previousTabName)).textContent=""}})),tabToggle.addEventListener("shown.bs.tab",(()=>{const tabPane=document.getElementById(tabToggle.getAttribute("href").replace(/^#/,""));tabPane&&loadTab(tabPane.id)}))})),!openTabFromHash()){const tabs=document.querySelector(SELECTORS.allActiveTabs);if(tabs)openTab(tabs.getAttribute("aria-controls"));else{const tabPane=document.querySelector(SELECTORS.tabPane);tabPane&&(tabPane.classList.add("active","show"),loadTab(tabPane.getAttribute("id")))}}};const getActiveTabName=()=>{const element=document.querySelector(SELECTORS.activeTab);return(null==element?void 0:element.getAttribute("aria-controls"))||null},loadTab=tabName=>{var _ref,_tabName;tabName=null!==(_ref=null!==(_tabName=tabName)&&void 0!==_tabName?_tabName:getActiveTabName())&&void 0!==_ref?_ref:(()=>{const element=document.querySelector(SELECTORS.tabContent);return(null==element?void 0:element.dataset.tabContent)||null})();const tab=document.querySelector(SELECTORS.forTabName(tabName));if(!tab)return;const pendingPromise=new _pending.default("core/dynamic_tabs:loadTab:"+tabName);(0,_loadingicon.addIconToContainer)(tab).then((()=>{let tabArgs={...tab.dataset};return delete tabArgs.tabClass,delete tabArgs.tabContent,(0,_dynamic_tabs.getContent)(tab.dataset.tabClass,JSON.stringify(tabArgs))})).then((response=>Promise.all([_jquery.default.parseHTML(response.javascript,null,!0).map((node=>node.innerHTML)).join("\n"),_templates.default.renderForPromise(response.template,JSON.parse(response.content))]))).then((_ref2=>{let[responseJs,{html:html,js:js}]=_ref2;return _templates.default.replaceNodeContents(tab,html,js+responseJs)})).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)},openTab=tabName=>{const tab=(tabName=>document.querySelector(SELECTORS.forTabId(tabName)))(tabName);return!!tab&&(loadTab(tabName),tab.classList.add("active"),(tabName=>document.getElementById(tabName))(tabName).classList.add("active","show"),!0)},openTabFromHash=()=>{const hash=document.location.hash;return!!hash.match(/^#\w+$/g)&&openTab(hash.replace(/^#/g,""))}})); //# sourceMappingURL=dynamic_tabs.min.js.map \ No newline at end of file diff --git a/lib/amd/build/dynamic_tabs.min.js.map b/lib/amd/build/dynamic_tabs.min.js.map index 0d15792fddc..4f82aa1f408 100644 --- a/lib/amd/build/dynamic_tabs.min.js.map +++ b/lib/amd/build/dynamic_tabs.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dynamic_tabs.min.js","sources":["../src/dynamic_tabs.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 * 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 {addIconToContainer} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {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-bs-toggle=\"tab\"]:not(.disabled)',\n tabContent: '.dynamictabs .tab-pane [data-tab-content]',\n tabToggle: 'a[data-bs-toggle=\"tab\"]',\n tabPane: '.dynamictabs .tab-pane',\n};\n\nSELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content=\"${tabName}\"]`;\nSELECTORS.forTabId = tabName => `.dynamictabs [data-bs-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 tabToggles = document.querySelectorAll(SELECTORS.tabToggle);\n\n tabToggles.forEach(tabToggle => {\n // Listen to click, warn user if they are navigating away with unsaved form changes.\n tabToggle.addEventListener('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 tabToggle.addEventListener('show.bs.tab', () => {\n // Clean content from previous tab.\n const previousTabName = getActiveTabName();\n if (previousTabName) {\n const previousTab = document.querySelector(SELECTORS.forTabName(previousTabName));\n previousTab.textContent = '';\n }\n });\n\n tabToggle.addEventListener('shown.bs.tab', () => {\n const tabPane = document.getElementById(tabToggle.getAttribute('href').replace(/^#/, ''));\n if (tabPane) {\n loadTab(tabPane.id);\n }\n });\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 * 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 */\nconst loadTab = (tabName) => {\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 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\n addIconToContainer(tab)\n .then(() => {\n let tabArgs = {...tab.dataset};\n delete tabArgs.tabClass;\n delete tabArgs.tabContent;\n return getContent(tab.dataset.tabClass, JSON.stringify(tabArgs));\n })\n .then(response => Promise.all([\n $.parseHTML(response.javascript, null, true).map(node => node.innerHTML).join(\"\\n\"),\n Templates.renderForPromise(response.template, JSON.parse(response.content)),\n ]))\n .then(([responseJs, {html, js}]) => Templates.replaceNodeContents(tab, html, js + responseJs))\n .then(() => pendingPromise.resolve())\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"],"names":["SELECTORS","dynamicTabs","activeTab","allActiveTabs","tabContent","tabToggle","tabPane","tabName","document","querySelectorAll","forEach","addEventListener","event","preventDefault","stopPropagation","key","component","then","_ref","strChangesMade","strChangesMadeReally","strConfirm","Notification","confirm","target","trigger","type","catch","exception","previousTabName","getActiveTabName","querySelector","forTabName","textContent","getElementById","getAttribute","replace","loadTab","id","openTabFromHash","tabs","openTab","classList","add","element","dataset","getFirstTabName","tab","pendingPromise","Pending","tabArgs","tabClass","JSON","stringify","response","Promise","all","$","parseHTML","javascript","map","node","innerHTML","join","Templates","renderForPromise","template","parse","content","_ref3","responseJs","html","js","replaceNodeContents","resolve","forTabId","getTab","getTabPane","hash","location","match"],"mappings":";;;;;;;4QAgCMA,UAAY,CACdC,YAAa,eACbC,UAAW,gCACXC,cAAe,8DACfC,WAAY,4CACZC,UAAW,0BACXC,QAAS,yBAGbN,WAAuBO,mDAA8CA,cACrEP,SAAqBO,8DAAyDA,6BAK1D,QACGC,SAASC,iBAAiBT,UAAUK,WAE5CK,SAAQL,YAEfA,UAAUM,iBAAiB,SAAUC,SAC5B,4CAILA,MAAMC,iBACND,MAAME,sCAEK,CACP,CAACC,IAAK,cAAeC,UAAW,UAChC,CAACD,IAAK,0BAA2BC,UAAW,UAC5C,CAACD,IAAK,UAAWC,UAAW,YAC7BC,MAAKC,WAAEC,eAAgBC,qBAAsBC,wBAE5CC,sBAAaC,QAAQJ,eAAgBC,qBAAsBC,WAAY,MAAM,sEAEvET,MAAMY,QAAQC,QAAQb,MAAMc,YAEpCC,MAAML,sBAAaM,eAGzBvB,UAAUM,iBAAiB,eAAe,WAEhCkB,gBAAkBC,sBACpBD,gBAAiB,CACGrB,SAASuB,cAAc/B,UAAUgC,WAAWH,kBACpDI,YAAc,OAIlC5B,UAAUM,iBAAiB,gBAAgB,WACjCL,QAAUE,SAAS0B,eAAe7B,UAAU8B,aAAa,QAAQC,QAAQ,KAAM,KACjF9B,SACA+B,QAAQ/B,QAAQgC,WAKvBC,kBAAmB,OACdC,KAAOhC,SAASuB,cAAc/B,UAAUG,kBAC1CqC,KACAC,QAAQD,KAAKL,aAAa,sBACvB,OAEG7B,QAAUE,SAASuB,cAAc/B,UAAUM,SAC7CA,UACAA,QAAQoC,UAAUC,IAAI,SAAU,QAChCN,QAAQ/B,QAAQ6B,aAAa,iBAWvCL,iBAAmB,WACfc,QAAUpC,SAASuB,cAAc/B,UAAUE,kBAC1C0C,MAAAA,eAAAA,QAAST,aAAa,mBAAoB,MAkB/CE,QAAW9B,6BAEbA,uCAAUA,qCAAWuB,0CAZD,YACdc,QAAUpC,SAASuB,cAAc/B,UAAUI,mBAC1CwC,MAAAA,eAAAA,QAASC,QAAQzC,aAAc,MAUK0C,SACrCC,IAAMvC,SAASuB,cAAc/B,UAAUgC,WAAWzB,cACnDwC,iBAICC,eAAiB,IAAIC,iBAAQ,6BAA+B1C,6CAE/CwC,KAClB9B,MAAK,SACEiC,QAAU,IAAIH,IAAIF,uBACfK,QAAQC,gBACRD,QAAQ9C,YACR,4BAAW2C,IAAIF,QAAQM,SAAUC,KAAKC,UAAUH,aAE1DjC,MAAKqC,UAAYC,QAAQC,IAAI,CAC1BC,gBAAEC,UAAUJ,SAASK,WAAY,MAAM,GAAMC,KAAIC,MAAQA,KAAKC,YAAWC,KAAK,MAC9EC,mBAAUC,iBAAiBX,SAASY,SAAUd,KAAKe,MAAMb,SAASc,cAErEnD,MAAKoD,YAAEC,YAAYC,KAACA,KAADC,GAAOA,kBAASR,mBAAUS,oBAAoB1B,IAAKwB,KAAMC,GAAKF,eACjFrD,MAAK,IAAM+B,eAAe0B,YAC1B/C,MAAML,sBAAaM,YA6BlBa,QAAWlC,gBACPwC,IArBMxC,CAAAA,SACLC,SAASuB,cAAc/B,UAAU2E,SAASpE,UAoBrCqE,CAAOrE,iBACdwC,MAILV,QAAQ9B,SACRwC,IAAIL,UAAUC,IAAI,UAjBFpC,CAAAA,SACTC,SAAS0B,eAAe3B,SAiB/BsE,CAAWtE,SAASmC,UAAUC,IAAI,SAAU,SACrC,IAQLJ,gBAAkB,WACduC,KAAOtE,SAASuE,SAASD,aAC3BA,KAAKE,MAAM,YACJvC,QAAQqC,KAAK1C,QAAQ,MAAO"} \ No newline at end of file +{"version":3,"file":"dynamic_tabs.min.js","sources":["../src/dynamic_tabs.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 * 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 {addIconToContainer} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {getString} 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-bs-toggle=\"tab\"]:not(.disabled)',\n tabContent: '.dynamictabs .tab-pane [data-tab-content]',\n tabToggle: 'a[data-bs-toggle=\"tab\"]',\n tabPane: '.dynamictabs .tab-pane',\n};\n\nSELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content=\"${tabName}\"]`;\nSELECTORS.forTabId = tabName => `.dynamictabs [data-bs-toggle=\"tab\"][href=\"#${tabName}\"]`;\n\nlet watchedFormDirtyNotification = false;\n\n/**\n * Initialises the tabs view on the page (only one tabs view per page is supported)\n */\nexport const init = () => {\n const tabToggles = document.querySelectorAll(SELECTORS.tabToggle);\n tabToggles.forEach(tabToggle => {\n // Listen to click, warn user if they are navigating away with unsaved form changes.\n tabToggle.addEventListener('show.bs.tab', (event) => {\n if (isAnyWatchedFormDirty()) {\n event.preventDefault();\n event.stopPropagation();\n\n // Prevent double execution of event listener.\n if (!watchedFormDirtyNotification) {\n watchedFormDirtyNotification = true;\n\n Notification.saveCancelPromise(\n getString('changesmade'),\n getString('changesmadereallygoaway'),\n getString('confirm'),\n {triggerElement: tabToggle}\n ).then(() => {\n // Reset form dirty state on confirmation, re-trigger the event.\n resetAllFormDirtyStates();\n tabToggle.dispatchEvent(new Event('click', {bubbles: true}));\n return;\n }).catch(() => {\n // User cancelled the dialogue.\n }).finally(() => {\n watchedFormDirtyNotification = false;\n });\n }\n\n return;\n }\n\n // Clean content from previous tab.\n const previousTabName = getActiveTabName();\n if (previousTabName) {\n const previousTab = document.querySelector(SELECTORS.forTabName(previousTabName));\n previousTab.textContent = '';\n }\n });\n\n tabToggle.addEventListener('shown.bs.tab', () => {\n const tabPane = document.getElementById(tabToggle.getAttribute('href').replace(/^#/, ''));\n if (tabPane) {\n loadTab(tabPane.id);\n }\n });\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 * 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 */\nconst loadTab = (tabName) => {\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 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\n addIconToContainer(tab)\n .then(() => {\n let tabArgs = {...tab.dataset};\n delete tabArgs.tabClass;\n delete tabArgs.tabContent;\n return getContent(tab.dataset.tabClass, JSON.stringify(tabArgs));\n })\n .then(response => Promise.all([\n $.parseHTML(response.javascript, null, true).map(node => node.innerHTML).join(\"\\n\"),\n Templates.renderForPromise(response.template, JSON.parse(response.content)),\n ]))\n .then(([responseJs, {html, js}]) => Templates.replaceNodeContents(tab, html, js + responseJs))\n .then(() => pendingPromise.resolve())\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"],"names":["SELECTORS","dynamicTabs","activeTab","allActiveTabs","tabContent","tabToggle","tabPane","tabName","watchedFormDirtyNotification","document","querySelectorAll","forEach","addEventListener","event","preventDefault","stopPropagation","saveCancelPromise","triggerElement","then","dispatchEvent","Event","bubbles","catch","finally","previousTabName","getActiveTabName","querySelector","forTabName","textContent","getElementById","getAttribute","replace","loadTab","id","openTabFromHash","tabs","openTab","classList","add","element","dataset","getFirstTabName","tab","pendingPromise","Pending","tabArgs","tabClass","JSON","stringify","response","Promise","all","$","parseHTML","javascript","map","node","innerHTML","join","Templates","renderForPromise","template","parse","content","_ref2","responseJs","html","js","replaceNodeContents","resolve","Notification","exception","forTabId","getTab","getTabPane","hash","location","match"],"mappings":";;;;;;;4QAgCMA,UAAY,CACdC,YAAa,eACbC,UAAW,gCACXC,cAAe,8DACfC,WAAY,4CACZC,UAAW,0BACXC,QAAS,yBAGbN,WAAuBO,mDAA8CA,cACrEP,SAAqBO,8DAAyDA,mBAE1EC,8BAA+B,gBAKf,QACGC,SAASC,iBAAiBV,UAAUK,WAC5CM,SAAQN,YAEfA,UAAUO,iBAAiB,eAAgBC,YACnC,iDACAA,MAAMC,iBACND,MAAME,uBAGDP,+BACDA,8BAA+B,wBAElBQ,mBACT,kBAAU,gBACV,kBAAU,4BACV,kBAAU,WACV,CAACC,eAAgBZ,YACnBa,MAAK,kDAGHb,UAAUc,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,QAEtDC,OAAM,SAENC,SAAQ,KACPf,8BAA+B,aAQrCgB,gBAAkBC,sBACpBD,gBAAiB,CACGf,SAASiB,cAAc1B,UAAU2B,WAAWH,kBACpDI,YAAc,OAIlCvB,UAAUO,iBAAiB,gBAAgB,WACjCN,QAAUG,SAASoB,eAAexB,UAAUyB,aAAa,QAAQC,QAAQ,KAAM,KACjFzB,SACA0B,QAAQ1B,QAAQ2B,WAKvBC,kBAAmB,OACdC,KAAO1B,SAASiB,cAAc1B,UAAUG,kBAC1CgC,KACAC,QAAQD,KAAKL,aAAa,sBACvB,OAEGxB,QAAUG,SAASiB,cAAc1B,UAAUM,SAC7CA,UACAA,QAAQ+B,UAAUC,IAAI,SAAU,QAChCN,QAAQ1B,QAAQwB,aAAa,iBAWvCL,iBAAmB,WACfc,QAAU9B,SAASiB,cAAc1B,UAAUE,kBAC1CqC,MAAAA,eAAAA,QAAST,aAAa,mBAAoB,MAkB/CE,QAAWzB,4BAEbA,sCAAUA,qCAAWkB,wCAZD,YACdc,QAAU9B,SAASiB,cAAc1B,UAAUI,mBAC1CmC,MAAAA,eAAAA,QAASC,QAAQpC,aAAc,MAUKqC,SACrCC,IAAMjC,SAASiB,cAAc1B,UAAU2B,WAAWpB,cACnDmC,iBAICC,eAAiB,IAAIC,iBAAQ,6BAA+BrC,6CAE/CmC,KAClBxB,MAAK,SACE2B,QAAU,IAAIH,IAAIF,uBACfK,QAAQC,gBACRD,QAAQzC,YACR,4BAAWsC,IAAIF,QAAQM,SAAUC,KAAKC,UAAUH,aAE1D3B,MAAK+B,UAAYC,QAAQC,IAAI,CAC1BC,gBAAEC,UAAUJ,SAASK,WAAY,MAAM,GAAMC,KAAIC,MAAQA,KAAKC,YAAWC,KAAK,MAC9EC,mBAAUC,iBAAiBX,SAASY,SAAUd,KAAKe,MAAMb,SAASc,cAErE7C,MAAK8C,YAAEC,YAAYC,KAACA,KAADC,GAAOA,kBAASR,mBAAUS,oBAAoB1B,IAAKwB,KAAMC,GAAKF,eACjF/C,MAAK,IAAMyB,eAAe0B,YAC1B/C,MAAMgD,sBAAaC,YA6BlBnC,QAAW7B,gBACPmC,IArBMnC,CAAAA,SACLE,SAASiB,cAAc1B,UAAUwE,SAASjE,UAoBrCkE,CAAOlE,iBACdmC,MAILV,QAAQzB,SACRmC,IAAIL,UAAUC,IAAI,UAjBF/B,CAAAA,SACTE,SAASoB,eAAetB,SAiB/BmE,CAAWnE,SAAS8B,UAAUC,IAAI,SAAU,SACrC,IAQLJ,gBAAkB,WACdyC,KAAOlE,SAASmE,SAASD,aAC3BA,KAAKE,MAAM,YACJzC,QAAQuC,KAAK5C,QAAQ,MAAO"} \ No newline at end of file diff --git a/lib/amd/src/dynamic_tabs.js b/lib/amd/src/dynamic_tabs.js index 7f1140995dd..6abbc516692 100644 --- a/lib/amd/src/dynamic_tabs.js +++ b/lib/amd/src/dynamic_tabs.js @@ -26,7 +26,7 @@ import Templates from 'core/templates'; import {addIconToContainer} from 'core/loadingicon'; import Notification from 'core/notification'; import Pending from 'core/pending'; -import {getStrings} from 'core/str'; +import {getString} from 'core/str'; import {getContent} from 'core/local/repository/dynamic_tabs'; import {isAnyWatchedFormDirty, resetAllFormDirtyStates} from 'core_form/changechecker'; @@ -42,36 +42,44 @@ const SELECTORS = { SELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content="${tabName}"]`; SELECTORS.forTabId = tabName => `.dynamictabs [data-bs-toggle="tab"][href="#${tabName}"]`; +let watchedFormDirtyNotification = false; + /** * Initialises the tabs view on the page (only one tabs view per page is supported) */ export const init = () => { const tabToggles = document.querySelectorAll(SELECTORS.tabToggle); - tabToggles.forEach(tabToggle => { // Listen to click, warn user if they are navigating away with unsaved form changes. - tabToggle.addEventListener('click', (event) => { - if (!isAnyWatchedFormDirty()) { + tabToggle.addEventListener('show.bs.tab', (event) => { + if (isAnyWatchedFormDirty()) { + event.preventDefault(); + event.stopPropagation(); + + // Prevent double execution of event listener. + if (!watchedFormDirtyNotification) { + watchedFormDirtyNotification = true; + + Notification.saveCancelPromise( + getString('changesmade'), + getString('changesmadereallygoaway'), + getString('confirm'), + {triggerElement: tabToggle} + ).then(() => { + // Reset form dirty state on confirmation, re-trigger the event. + resetAllFormDirtyStates(); + tabToggle.dispatchEvent(new Event('click', {bubbles: true})); + return; + }).catch(() => { + // User cancelled the dialogue. + }).finally(() => { + watchedFormDirtyNotification = false; + }); + } + 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); - }); - - tabToggle.addEventListener('show.bs.tab', () => { // Clean content from previous tab. const previousTabName = getActiveTabName(); if (previousTabName) {