diff --git a/.upgradenotes/MDL-84804-2025060412115860.yml b/.upgradenotes/MDL-84804-2025060412115860.yml new file mode 100644 index 00000000000..4bc9e66e5ed --- /dev/null +++ b/.upgradenotes/MDL-84804-2025060412115860.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-84804 +notes: + core: + - message: >- + There is a new `core/page_title` Javascript module for manipulating the + current page title + type: improved diff --git a/lib/amd/build/dynamic_tabs.min.js b/lib/amd/build/dynamic_tabs.min.js index 4441d99fbfa..1abfd3f5182 100644 --- a/lib/amd/build/dynamic_tabs.min.js +++ b/lib/amd/build/dynamic_tabs.min.js @@ -1,10 +1,10 @@ -define("core/dynamic_tabs",["exports","jquery","core/templates","core/loadingicon","core/notification","core/pending","core/str","core/local/repository/dynamic_tabs","core_form/changechecker"],(function(_exports,_jquery,_templates,_loadingicon,_notification,_pending,_str,_dynamic_tabs,_changechecker){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core/dynamic_tabs",["exports","jquery","core/templates","core/loadingicon","core/notification","core/page_title","core/pending","core/str","core/local/repository/dynamic_tabs","core_form/changechecker"],(function(_exports,_jquery,_templates,_loadingicon,_notification,_page_title,_pending,_str,_dynamic_tabs,_changechecker){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Dynamic Tabs UI element with AJAX loading of tabs content * * @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-toggle="tab"]:not(.disabled)',tabContent:".dynamictabs .tab-pane [data-tab-content]",tabToggle:'a[data-toggle="tab"]',tabPane:".dynamictabs .tab-pane",forTabName:tabName=>'.dynamictabs [data-tab-content="'.concat(tabName,'"]'),forTabId:tabName=>'.dynamictabs [data-toggle="tab"][href="#'.concat(tabName,'"]')};_exports.init=()=>{const tabToggle=(0,_jquery.default)(SELECTORS.tabToggle);if(tabToggle.on("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.on("show.bs.tab",(function(){const previousTabName=getActiveTabName();if(previousTabName){document.querySelector(SELECTORS.forTabName(previousTabName)).textContent=""}})).on("shown.bs.tab",(function(){const tab=(0,_jquery.default)((0,_jquery.default)(this).attr("href"));1===tab.length&&loadTab(tab.attr("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-toggle="tab"]:not(.disabled)',tabContent:".dynamictabs .tab-pane [data-tab-content]",tabToggle:'a[data-toggle="tab"]',tabPane:".dynamictabs .tab-pane",forTabName:tabName=>'.dynamictabs [data-tab-content="'.concat(tabName,'"]'),forTabId:tabName=>'.dynamictabs [data-toggle="tab"][href="#'.concat(tabName,'"]')};_exports.init=()=>{const tabToggle=(0,_jquery.default)(SELECTORS.tabToggle);if(tabToggle.on("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.on("show.bs.tab",(function(){const previousTabName=getActiveTabName();if(previousTabName){document.querySelector(SELECTORS.forTabName(previousTabName)).textContent=""}})).on("shown.bs.tab",(function(){const tab=(0,_jquery.default)((0,_jquery.default)(this).attr("href"));1===tab.length&&loadTab(tab.attr("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),tabLabelledBy=document.getElementById(tab.getAttribute("aria-labelledby"));(0,_page_title.prependPageTitle)(tabLabelledBy.innerText),(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,""))}})); //# 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 26be38f4404..3975d1c73d5 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-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 events 'show.bs.tab' and 'shown.bs.tab' which is triggered using JQuery and\n // can not be converted yet to native events.\n tabToggle\n .on('show.bs.tab', function() {\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 .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 * 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","on","event","preventDefault","stopPropagation","key","component","then","_ref","strChangesMade","strChangesMadeReally","strConfirm","Notification","confirm","target","trigger","type","catch","exception","previousTabName","getActiveTabName","document","querySelector","forTabName","textContent","tab","this","attr","length","loadTab","openTabFromHash","tabs","openTab","getAttribute","classList","add","element","dataset","getFirstTabName","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","getElementById","getTabPane","hash","location","match","replace"],"mappings":";;;;;;;4QAgCMA,UAAY,CACdC,YAAa,eACbC,UAAW,gCACXC,cAAe,2DACfC,WAAY,4CACZC,UAAW,uBACXC,QAAS,yBAGbN,WAAuBO,mDAA8CA,cACrEP,SAAqBO,2DAAsDA,6BAKvD,WACVF,WAAY,mBAAEL,UAAUK,cAG9BA,UAAUG,GAAG,SAAUC,SACd,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,eAKzBpB,UACKG,GAAG,eAAe,iBAETkB,gBAAkBC,sBACpBD,gBAAiB,CACGE,SAASC,cAAc7B,UAAU8B,WAAWJ,kBACpDK,YAAc,OAGjCvB,GAAG,gBAAgB,iBACVwB,KAAM,oBAAE,mBAAEC,MAAMC,KAAK,SACR,IAAfF,IAAIG,QAGRC,QAAQJ,IAAIE,KAAK,WAGpBG,kBAAmB,OACdC,KAAOV,SAASC,cAAc7B,UAAUG,kBAC1CmC,KACAC,QAAQD,KAAKE,aAAa,sBACvB,OAEGlC,QAAUsB,SAASC,cAAc7B,UAAUM,SAC7CA,UACAA,QAAQmC,UAAUC,IAAI,SAAU,QAChCN,QAAQ9B,QAAQkC,aAAa,iBAWvCb,iBAAmB,WACfgB,QAAUf,SAASC,cAAc7B,UAAUE,kBAC1CyC,MAAAA,eAAAA,QAASH,aAAa,mBAAoB,MAkB/CJ,QAAW7B,6BAEbA,uCAAUA,qCAAWoB,0CAZD,YACdgB,QAAUf,SAASC,cAAc7B,UAAUI,mBAC1CuC,MAAAA,eAAAA,QAASC,QAAQxC,aAAc,MAUKyC,SACrCb,IAAMJ,SAASC,cAAc7B,UAAU8B,WAAWvB,cACnDyB,iBAICc,eAAiB,IAAIC,iBAAQ,6BAA+BxC,6CAE/CyB,KAClBlB,MAAK,SACEkC,QAAU,IAAIhB,IAAIY,uBACfI,QAAQC,gBACRD,QAAQ5C,YACR,4BAAW4B,IAAIY,QAAQK,SAAUC,KAAKC,UAAUH,aAE1DlC,MAAKsC,UAAYC,QAAQC,IAAI,CAC1BC,gBAAEC,UAAUJ,SAASK,WAAY,MAAM,GAAMC,KAAIC,MAAQA,KAAKC,YAAWC,KAAK,MAC9EC,mBAAUC,iBAAiBX,SAASY,SAAUd,KAAKe,MAAMb,SAASc,cAErEpD,MAAKqD,YAAEC,YAAYC,KAACA,KAADC,GAAOA,kBAASR,mBAAUS,oBAAoBvC,IAAKqC,KAAMC,GAAKF,eACjFtD,MAAK,IAAMgC,eAAe0B,YAC1BhD,MAAML,sBAAaM,YA6BlBc,QAAWhC,gBACPyB,IArBMzB,CAAAA,SACLqB,SAASC,cAAc7B,UAAUyE,SAASlE,UAoBrCmE,CAAOnE,iBACdyB,MAILI,QAAQ7B,SACRyB,IAAIS,UAAUC,IAAI,UAjBFnC,CAAAA,SACTqB,SAAS+C,eAAepE,SAiB/BqE,CAAWrE,SAASkC,UAAUC,IAAI,SAAU,SACrC,IAQLL,gBAAkB,WACdwC,KAAOjD,SAASkD,SAASD,aAC3BA,KAAKE,MAAM,YACJxC,QAAQsC,KAAKG,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 {prependPageTitle} from 'core/page_title';\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-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 events 'show.bs.tab' and 'shown.bs.tab' which is triggered using JQuery and\n // can not be converted yet to native events.\n tabToggle\n .on('show.bs.tab', function() {\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 .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 * 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 const tabLabelledBy = document.getElementById(tab.getAttribute('aria-labelledby'));\n prependPageTitle(tabLabelledBy.innerText);\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","on","event","preventDefault","stopPropagation","key","component","then","_ref","strChangesMade","strChangesMadeReally","strConfirm","Notification","confirm","target","trigger","type","catch","exception","previousTabName","getActiveTabName","document","querySelector","forTabName","textContent","tab","this","attr","length","loadTab","openTabFromHash","tabs","openTab","getAttribute","classList","add","element","dataset","getFirstTabName","pendingPromise","Pending","tabLabelledBy","getElementById","innerText","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","replace"],"mappings":";;;;;;;4QAiCMA,UAAY,CACdC,YAAa,eACbC,UAAW,gCACXC,cAAe,2DACfC,WAAY,4CACZC,UAAW,uBACXC,QAAS,yBAGbN,WAAuBO,mDAA8CA,cACrEP,SAAqBO,2DAAsDA,6BAKvD,WACVF,WAAY,mBAAEL,UAAUK,cAG9BA,UAAUG,GAAG,SAAUC,SACd,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,eAKzBpB,UACKG,GAAG,eAAe,iBAETkB,gBAAkBC,sBACpBD,gBAAiB,CACGE,SAASC,cAAc7B,UAAU8B,WAAWJ,kBACpDK,YAAc,OAGjCvB,GAAG,gBAAgB,iBACVwB,KAAM,oBAAE,mBAAEC,MAAMC,KAAK,SACR,IAAfF,IAAIG,QAGRC,QAAQJ,IAAIE,KAAK,WAGpBG,kBAAmB,OACdC,KAAOV,SAASC,cAAc7B,UAAUG,kBAC1CmC,KACAC,QAAQD,KAAKE,aAAa,sBACvB,OAEGlC,QAAUsB,SAASC,cAAc7B,UAAUM,SAC7CA,UACAA,QAAQmC,UAAUC,IAAI,SAAU,QAChCN,QAAQ9B,QAAQkC,aAAa,iBAWvCb,iBAAmB,WACfgB,QAAUf,SAASC,cAAc7B,UAAUE,kBAC1CyC,MAAAA,eAAAA,QAASH,aAAa,mBAAoB,MAkB/CJ,QAAW7B,6BAEbA,uCAAUA,qCAAWoB,0CAZD,YACdgB,QAAUf,SAASC,cAAc7B,UAAUI,mBAC1CuC,MAAAA,eAAAA,QAASC,QAAQxC,aAAc,MAUKyC,SACrCb,IAAMJ,SAASC,cAAc7B,UAAU8B,WAAWvB,cACnDyB,iBAICc,eAAiB,IAAIC,iBAAQ,6BAA+BxC,SAE5DyC,cAAgBpB,SAASqB,eAAejB,IAAIQ,aAAa,qDAC9CQ,cAAcE,+CAEZlB,KAClBlB,MAAK,SACEqC,QAAU,IAAInB,IAAIY,uBACfO,QAAQC,gBACRD,QAAQ/C,YACR,4BAAW4B,IAAIY,QAAQQ,SAAUC,KAAKC,UAAUH,aAE1DrC,MAAKyC,UAAYC,QAAQC,IAAI,CAC1BC,gBAAEC,UAAUJ,SAASK,WAAY,MAAM,GAAMC,KAAIC,MAAQA,KAAKC,YAAWC,KAAK,MAC9EC,mBAAUC,iBAAiBX,SAASY,SAAUd,KAAKe,MAAMb,SAASc,cAErEvD,MAAKwD,YAAEC,YAAYC,KAACA,KAADC,GAAOA,kBAASR,mBAAUS,oBAAoB1C,IAAKwC,KAAMC,GAAKF,eACjFzD,MAAK,IAAMgC,eAAe6B,YAC1BnD,MAAML,sBAAaM,YA6BlBc,QAAWhC,gBACPyB,IArBMzB,CAAAA,SACLqB,SAASC,cAAc7B,UAAU4E,SAASrE,UAoBrCsE,CAAOtE,iBACdyB,MAILI,QAAQ7B,SACRyB,IAAIS,UAAUC,IAAI,UAjBFnC,CAAAA,SACTqB,SAASqB,eAAe1C,SAiB/BuE,CAAWvE,SAASkC,UAAUC,IAAI,SAAU,SACrC,IAQLL,gBAAkB,WACd0C,KAAOnD,SAASoD,SAASD,aAC3BA,KAAKE,MAAM,YACJ1C,QAAQwC,KAAKG,QAAQ,MAAO"} \ No newline at end of file diff --git a/lib/amd/build/page_title.min.js b/lib/amd/build/page_title.min.js new file mode 100644 index 00000000000..f6c94928cf7 --- /dev/null +++ b/lib/amd/build/page_title.min.js @@ -0,0 +1,11 @@ +define("core/page_title",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setPageTitleSeparator=_exports.setPageTitleRoot=_exports.setPageTitle=_exports.prependPageTitle=void 0; +/** + * Helper module for manipulating the current page title + * + * @module core/page_title + * @copyright 2025 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +let pageTitleRoot=document.title,pageTitleSeparator="|";const setPageTitle=pageTitle=>{document.title=pageTitle};_exports.setPageTitle=setPageTitle;_exports.prependPageTitle=pageTitlePrepend=>setPageTitle("".concat(pageTitlePrepend," ").concat(pageTitleSeparator," ").concat(pageTitleRoot));_exports.setPageTitleRoot=titleRoot=>{pageTitleRoot=titleRoot};_exports.setPageTitleSeparator=titleSeparator=>{pageTitleSeparator=titleSeparator}})); + +//# sourceMappingURL=page_title.min.js.map \ No newline at end of file diff --git a/lib/amd/build/page_title.min.js.map b/lib/amd/build/page_title.min.js.map new file mode 100644 index 00000000000..f7635d5debf --- /dev/null +++ b/lib/amd/build/page_title.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"page_title.min.js","sources":["../src/page_title.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 * Helper module for manipulating the current page title\n *\n * @module core/page_title\n * @copyright 2025 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nlet pageTitleRoot = document.title;\nlet pageTitleSeparator = '|';\n\n/**\n * Set the current page title\n *\n * @param {String} pageTitle\n */\nexport const setPageTitle = (pageTitle) => {\n document.title = pageTitle;\n};\n\n/**\n * Prepend value to the page title root, and set as the current page title\n *\n * @param {String} pageTitlePrepend\n */\nexport const prependPageTitle = (pageTitlePrepend) =>\n setPageTitle(`${pageTitlePrepend} ${pageTitleSeparator} ${pageTitleRoot}`);\n\n/**\n * Set the page title root, to be used later when prepending to the title\n *\n * By default, the page title root is the document title\n *\n * @param {String} titleRoot\n */\nexport const setPageTitleRoot = (titleRoot) => {\n pageTitleRoot = titleRoot;\n};\n\n/**\n * Set the page title separator\n *\n * By default, the page title separator is the pipe character\n *\n * @param {String} titleSeparator\n */\nexport const setPageTitleSeparator = (titleSeparator) => {\n pageTitleSeparator = titleSeparator;\n};\n"],"names":["pageTitleRoot","document","title","pageTitleSeparator","setPageTitle","pageTitle","pageTitlePrepend","titleRoot","titleSeparator"],"mappings":";;;;;;;;IAuBIA,cAAgBC,SAASC,MACzBC,mBAAqB,UAOZC,aAAgBC,YACzBJ,SAASC,MAAQG,wEAQYC,kBAC7BF,uBAAgBE,6BAAoBH,+BAAsBH,0CAS7BO,YAC7BP,cAAgBO,0CAUkBC,iBAClCL,mBAAqBK"} \ No newline at end of file diff --git a/lib/amd/src/dynamic_tabs.js b/lib/amd/src/dynamic_tabs.js index 7f3a2108e91..1b5df86f088 100644 --- a/lib/amd/src/dynamic_tabs.js +++ b/lib/amd/src/dynamic_tabs.js @@ -25,6 +25,7 @@ import $ from 'jquery'; import Templates from 'core/templates'; import {addIconToContainer} from 'core/loadingicon'; import Notification from 'core/notification'; +import {prependPageTitle} from 'core/page_title'; import Pending from 'core/pending'; import {getStrings} from 'core/str'; import {getContent} from 'core/local/repository/dynamic_tabs'; @@ -139,6 +140,9 @@ const loadTab = (tabName) => { const pendingPromise = new Pending('core/dynamic_tabs:loadTab:' + tabName); + const tabLabelledBy = document.getElementById(tab.getAttribute('aria-labelledby')); + prependPageTitle(tabLabelledBy.innerText); + addIconToContainer(tab) .then(() => { let tabArgs = {...tab.dataset}; diff --git a/lib/amd/src/page_title.js b/lib/amd/src/page_title.js new file mode 100644 index 00000000000..d602b580e02 --- /dev/null +++ b/lib/amd/src/page_title.js @@ -0,0 +1,64 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Helper module for manipulating the current page title + * + * @module core/page_title + * @copyright 2025 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +let pageTitleRoot = document.title; +let pageTitleSeparator = '|'; + +/** + * Set the current page title + * + * @param {String} pageTitle + */ +export const setPageTitle = (pageTitle) => { + document.title = pageTitle; +}; + +/** + * Prepend value to the page title root, and set as the current page title + * + * @param {String} pageTitlePrepend + */ +export const prependPageTitle = (pageTitlePrepend) => + setPageTitle(`${pageTitlePrepend} ${pageTitleSeparator} ${pageTitleRoot}`); + +/** + * Set the page title root, to be used later when prepending to the title + * + * By default, the page title root is the document title + * + * @param {String} titleRoot + */ +export const setPageTitleRoot = (titleRoot) => { + pageTitleRoot = titleRoot; +}; + +/** + * Set the page title separator + * + * By default, the page title separator is the pipe character + * + * @param {String} titleSeparator + */ +export const setPageTitleSeparator = (titleSeparator) => { + pageTitleSeparator = titleSeparator; +};