diff --git a/.upgradenotes/MDL-85533-2025052201591951.yml b/.upgradenotes/MDL-85533-2025052201591951.yml new file mode 100644 index 00000000000..5b47ed55443 --- /dev/null +++ b/.upgradenotes/MDL-85533-2025052201591951.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-85533 +notes: + core_course: + - message: >- + The activitychoosertabmode setting has been removed. Consider + implementing your own setting in your theme if needed. + type: removed diff --git a/admin/presets/classes/helper.php b/admin/presets/classes/helper.php index 7c8cbdc2bc8..02adb189d20 100644 --- a/admin/presets/classes/helper.php +++ b/admin/presets/classes/helper.php @@ -190,8 +190,6 @@ class helper { static::add_item($presetid, 'showdataretentionsummary', '0', 'tool_dataprivacy'); static::add_item($presetid, 'forum_maxattachments', '3'); static::add_item($presetid, 'guestloginbutton', '0'); - // Set Activity chooser tabs to "Starred, Recommended, All". - static::add_item($presetid, 'activitychoosertabmode', '4'); // Modules: Hide database, external tool (lti), IMS content package (imscp), lesson, SCORM, wiki, workshop. static::add_plugin($presetid, 'mod', 'data', false); @@ -295,8 +293,6 @@ class helper { static::add_item($presetid, 'showdataretentionsummary', '1', 'tool_dataprivacy'); static::add_item($presetid, 'forum_maxattachments', '9'); static::add_item($presetid, 'guestloginbutton', '1'); - // Set Activity chooser tabs to the default value ("Starred, Recommended, All, Activities, Resources"). - static::add_item($presetid, 'activitychoosertabmode', '3'); // Modules: Enable database, external tool (lti), IMS content package (imscp), lesson, SCORM, wiki, workshop. static::add_plugin($presetid, 'mod', 'data', true); diff --git a/admin/settings/courses.php b/admin/settings/courses.php index 2c62ce83be7..18ca097cad7 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -294,23 +294,6 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { ); $temp = new admin_settingpage('activitychoosersettings', new lang_string('activitychoosersettings', 'course')); - // Tab mode for the activity chooser. - $temp->add( - new admin_setting_configselect( - 'activitychoosertabmode', - new lang_string('activitychoosertabmode', 'course'), - new lang_string('activitychoosertabmode_desc', 'course'), - 3, - [ - 3 => new lang_string('activitychoosertabmodefour', 'course'), - 4 => new lang_string('activitychoosertabmodefive', 'course'), - 5 => new lang_string('activitychoosertabmodesix', 'course'), - 0 => new lang_string('activitychoosertabmodeone', 'course'), - 1 => new lang_string('activitychoosertabmodetwo', 'course'), - 2 => new lang_string('activitychoosertabmodethree', 'course'), - ] - ) - ); // Add to the 'Groups' category. $ADMIN->add( @@ -334,23 +317,6 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // Add to the 'Activity Chooser' category. $temp = new admin_settingpage('activitychoosersettings', new lang_string('activitychoosersettings', 'course')); - // Tab mode for the activity chooser. - $temp->add( - new admin_setting_configselect( - 'activitychoosertabmode', - new lang_string('activitychoosertabmode', 'course'), - new lang_string('activitychoosertabmode_desc', 'course'), - 3, - [ - 3 => new lang_string('activitychoosertabmodefour', 'course'), - 4 => new lang_string('activitychoosertabmodefive', 'course'), - 5 => new lang_string('activitychoosertabmodesix', 'course'), - 0 => new lang_string('activitychoosertabmodeone', 'course'), - 1 => new lang_string('activitychoosertabmodetwo', 'course'), - 2 => new lang_string('activitychoosertabmodethree', 'course'), - ] - ) - ); // Build a list of plugins that use the footer callback. $pluginswithfunction = get_plugins_with_function('custom_chooser_footer', 'lib.php'); diff --git a/admin/tool/admin_presets/tests/behat/apply_presets.feature b/admin/tool/admin_presets/tests/behat/apply_presets.feature index cb32bb74d2a..0ac3384cff2 100644 --- a/admin/tool/admin_presets/tests/behat/apply_presets.feature +++ b/admin/tool/admin_presets/tests/behat/apply_presets.feature @@ -28,7 +28,6 @@ Feature: I can apply presets And I should see "Setting changes" # Checking all the settings to be applied for the Starter (if will help to identify possible regressions). And I should see "Activities" in the "Setting changes" "table" - And I should see "Activity chooser tabs" in the "Setting changes" "table" And I should see "Adaptive mode (no penalties)" in the "Setting changes" "table" And I should see "Blog menu" in the "Setting changes" "table" And I should see "Blog tags" in the "Setting changes" "table" diff --git a/course/amd/build/activitychooser.min.js b/course/amd/build/activitychooser.min.js index c341ef88217..57fcf71f448 100644 --- a/course/amd/build/activitychooser.min.js +++ b/course/amd/build/activitychooser.min.js @@ -1,10 +1,10 @@ -define("core_course/activitychooser",["exports","core_course/local/activitychooser/dialogue","core_course/local/activitychooser/repository","core_course/local/activitychooser/selectors","core/custom_interaction_events","core/templates","core/str","core/modal","core/pending"],(function(_exports,ChooserDialogue,Repository,_selectors,_custom_interaction_events,Templates,_str,_modal,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +define("core_course/activitychooser",["exports","core_course/local/activitychooser/dialogue","core_course/local/activitychooser/repository","core_course/local/activitychooser/selectors","core/custom_interaction_events","core/pending"],(function(_exports,ChooserDialogue,Repository,_selectors,_custom_interaction_events,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * A type of dialogue used as for choosing modules in a course. * * @module core_course/activitychooser * @copyright 2020 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,ChooserDialogue=_interopRequireWildcard(ChooserDialogue),Repository=_interopRequireWildcard(Repository),_selectors=_interopRequireDefault(_selectors),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Templates=_interopRequireWildcard(Templates),_modal=_interopRequireDefault(_modal),_pending=_interopRequireDefault(_pending);let initialized=!1;_exports.init=(courseId,chooserConfig)=>{const pendingPromise=new _pending.default;registerListenerEvents(courseId,chooserConfig),pendingPromise.resolve()};const registerListenerEvents=(courseId,chooserConfig)=>{if(initialized)return;const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate],fetchModuleData=(()=>{let innerPromises=new Map;return sectionNum=>(innerPromises.has(sectionNum)||innerPromises.set(sectionNum,new Promise((resolve=>{resolve(Repository.activityModules(courseId,sectionNum))}))),innerPromises.get(sectionNum))})(),fetchFooterData=(()=>{let footerInnerPromise=null;return sectionNum=>(footerInnerPromise||(footerInnerPromise=new Promise((resolve=>{resolve(Repository.fetchFooterData(courseId,sectionNum))}))),footerInnerPromise)})();_custom_interaction_events.default.define(document,events),events.forEach((event=>{document.addEventListener(event,(async e=>{if(e.target.closest(_selectors.default.elements.sectionmodchooser)){let caller,sectionnum;const sectionDiv=e.target.closest(_selectors.default.elements.section),button=e.target.closest(_selectors.default.elements.sectionmodchooser);let bodyPromiseResolver;null!==sectionDiv&§ionDiv.hasAttribute("data-number")?(caller=sectionDiv,sectionnum=sectionDiv.getAttribute("data-number")):(caller=button,caller.hasAttribute("data-sectionid")&&(window.console.warn("The data-sectionid attribute has been deprecated. Please update your code to use data-sectionnum instead."),caller.setAttribute("data-sectionnum",caller.dataset.sectionid)),sectionnum=caller.dataset.sectionnum);const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve})),footerData=await fetchFooterData(sectionnum),sectionModal=buildModal(bodyPromise,footerData),data=await fetchModuleData(sectionnum).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_course/local/activitychooser/error",errorTemplateData))}));if(!data)return;const builtModuleData=sectionMapper(data,sectionnum,caller.dataset.sectionreturnnum,caller.dataset.beforemod);ChooserDialogue.displayChooser(sectionModal,builtModuleData,partiallyAppliedFavouriteManager(data,sectionnum),footerData),bodyPromiseResolver(await Templates.render("core_course/activitychooser",templateDataBuilder(builtModuleData,chooserConfig)))}}))})),initialized=!0},sectionMapper=(webServiceData,num,sectionreturnnum,beforemod)=>{const newData=JSON.parse(JSON.stringify(webServiceData));return newData.content_items.forEach((module=>{module.link+="§ion="+num+"&beforemod="+(null!=beforemod?beforemod:0),sectionreturnnum&&(module.link+="&sr="+sectionreturnnum)})),newData.content_items},templateDataBuilder=(data,chooserConfig)=>{let activities=[],resources=[],showAll=!0,showActivities=!1,showResources=!1;const tabMode=parseInt(chooserConfig.tabmode),favourites=data.filter((mod=>!0===mod.favourite)),recommended=data.filter((mod=>!0===mod.recommended));(tabMode=>-1!==[0,3,2,5].indexOf(tabMode))(tabMode)&&(activities=data.filter((mod=>0===mod.archetype)),resources=data.filter((mod=>1===mod.archetype)),showActivities=!0,showResources=!0,2!==tabMode&&5!==tabMode||(showAll=!1));const recommendedBeginning=-1!==[3,4,5].indexOf(tabMode),favouritesFirst=!!favourites.length,recommendedFirst=!1===favouritesFirst&&!0===recommendedBeginning&&!!recommended.length;return{default:data,showAll:showAll,activities:activities,showActivities:showActivities,activitiesFirst:!1===showAll&&!1===favouritesFirst&&!1===recommendedFirst,resources:resources,showResources:showResources,favourites:favourites,recommended:recommended,recommendedFirst:recommendedFirst,recommendedBeginning:recommendedBeginning,favouritesFirst:favouritesFirst,fallback:!0===showAll&&!1===favouritesFirst&&!1===recommendedFirst}},buildModal=(body,footer)=>_modal.default.create({body:body,title:(0,_str.getString)("addresourceoractivity"),footer:footer.customfootertemplate,large:!0,scrollable:!1,templateContext:{classes:"modchooser"},show:!0}),partiallyAppliedFavouriteManager=(moduleData,sectionnum)=>async(internal,favourite,modalBody)=>{const favouriteArea=modalBody.querySelector(_selectors.default.render.favourites),favouriteButtons=modalBody.querySelectorAll('[data-internal="'.concat(internal,'"] ').concat(_selectors.default.actions.optionActions.manageFavourite)),favouriteTabNav=modalBody.querySelector(_selectors.default.regions.favouriteTabNav),result=moduleData.content_items.find((_ref=>{let{name:name}=_ref;return name===internal})),newFaves={};if(result)if(favourite){result.favourite=!0,newFaves.content_items=moduleData.content_items.filter((mod=>!0===mod.favourite));const builtFaves=sectionMapper(newFaves,sectionnum),{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/favourites",{favourites:builtFaves});await Templates.replaceNodeContents(favouriteArea,html,js),Array.from(favouriteButtons).forEach((element=>{element.classList.remove("text-muted"),element.classList.add("text-primary"),element.dataset.favourited="true",element.setAttribute("aria-pressed",!0),element.firstElementChild.classList.remove("fa-star-o"),element.firstElementChild.classList.add("fa-star")})),favouriteTabNav.classList.remove("d-none")}else{result.favourite=!1;const nodeToRemove=favouriteArea.querySelector('[data-internal="'.concat(internal,'"]'));nodeToRemove.parentNode.removeChild(nodeToRemove),Array.from(favouriteButtons).forEach((element=>{element.classList.add("text-muted"),element.classList.remove("text-primary"),element.dataset.favourited="false",element.setAttribute("aria-pressed",!1),element.firstElementChild.classList.remove("fa-star"),element.firstElementChild.classList.add("fa-star-o")}));0===moduleData.content_items.filter((mod=>!0===mod.favourite)).length&&((favouriteTabNav,modalBody)=>{if(favouriteTabNav.tabIndex=-1,favouriteTabNav.classList.add("d-none"),favouriteTabNav.classList.contains("active")){favouriteTabNav.classList.remove("active"),favouriteTabNav.setAttribute("aria-selected","false"),modalBody.querySelector(_selectors.default.regions.favouriteTab).classList.remove("active");const defaultTabNav=modalBody.querySelector(_selectors.default.regions.defaultTabNav),activitiesTabNav=modalBody.querySelector(_selectors.default.regions.activityTabNav);!1===defaultTabNav.classList.contains("d-none")?(defaultTabNav.classList.add("active"),defaultTabNav.setAttribute("aria-selected","true"),defaultTabNav.tabIndex=0,defaultTabNav.focus(),modalBody.querySelector(_selectors.default.regions.defaultTab).classList.add("active")):(activitiesTabNav.classList.add("active"),activitiesTabNav.setAttribute("aria-selected","true"),activitiesTabNav.tabIndex=0,activitiesTabNav.focus(),modalBody.querySelector(_selectors.default.regions.activityTab).classList.add("active"))}})(favouriteTabNav,modalBody)}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,ChooserDialogue=_interopRequireWildcard(ChooserDialogue),Repository=_interopRequireWildcard(Repository),_selectors=_interopRequireDefault(_selectors),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_pending=_interopRequireDefault(_pending);let initialized=!1;_exports.init=(courseId,chooserConfig)=>{const pendingPromise=new _pending.default;void 0!==chooserConfig.tabmode&&window.console.warn("The tabmode config option has been deprecated and will be ignored."),registerListenerEvents(courseId),pendingPromise.resolve()};const registerListenerEvents=courseId=>{if(initialized)return;initialized=!0;const eventsToHandle=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,eventsToHandle),eventsToHandle.forEach((eventToHandle=>{document.addEventListener(eventToHandle,(async event=>{if(!event.target.closest(_selectors.default.elements.sectionmodchooser))return;const position=function(target){var _caller$dataset$secti,_caller$dataset,_caller$dataset$befor,_caller$dataset2;let caller,sectionNum;const sectionDiv=target.closest(_selectors.default.elements.section),button=target.closest(_selectors.default.elements.sectionmodchooser);null!==sectionDiv&§ionDiv.hasAttribute("data-number")?(caller=sectionDiv,sectionNum=sectionDiv.getAttribute("data-number")):(caller=button,caller.hasAttribute("data-sectionid")&&(window.console.warn("The data-sectionid attribute has been deprecated. Please update your code to use data-sectionnum instead."),caller.setAttribute("data-sectionnum",caller.dataset.sectionid)),sectionNum=caller.dataset.sectionnum);return{sectionNum:sectionNum,sectionReturnNum:null!==(_caller$dataset$secti=null===(_caller$dataset=caller.dataset)||void 0===_caller$dataset?void 0:_caller$dataset.sectionreturnnum)&&void 0!==_caller$dataset$secti?_caller$dataset$secti:null,beforeMod:null!==(_caller$dataset$befor=null===(_caller$dataset2=caller.dataset)||void 0===_caller$dataset2?void 0:_caller$dataset2.beforemod)&&void 0!==_caller$dataset$befor?_caller$dataset$befor:null}}(event.target),footerDataPromise=Repository.getModalFooterData(courseId,position.sectionNum),modulesDataPromise=Repository.getModulesData(courseId,position.sectionNum,position.sectionReturnNum,position.beforeMod);ChooserDialogue.displayActivityChooserModal(footerDataPromise,modulesDataPromise)}))}))}})); //# sourceMappingURL=activitychooser.min.js.map \ No newline at end of file diff --git a/course/amd/build/activitychooser.min.js.map b/course/amd/build/activitychooser.min.js.map index 8bafb5e0463..554c4ec8b02 100644 --- a/course/amd/build/activitychooser.min.js.map +++ b/course/amd/build/activitychooser.min.js.map @@ -1 +1 @@ -{"version":3,"file":"activitychooser.min.js","sources":["../src/activitychooser.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 * A type of dialogue used as for choosing modules in a course.\n *\n * @module core_course/activitychooser\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport CustomEvents from 'core/custom_interaction_events';\nimport * as Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport Pending from 'core/pending';\n\n// Set up some JS module wide constants that can be added to in the future.\n\n// Tab config options.\nconst ALLACTIVITIESRESOURCES = 0;\nconst ACTIVITIESRESOURCES = 2;\nconst ALLACTIVITIESRESOURCESREC = 3;\nconst ONLYALLREC = 4;\nconst ACTIVITIESRESOURCESREC = 5;\n\n\n// Module types.\nconst ACTIVITY = 0;\nconst RESOURCE = 1;\n\nlet initialized = false;\n\n/**\n * Set up the activity chooser.\n *\n * @method init\n * @param {Number} courseId Course ID to use later on in fetchModules()\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n */\nexport const init = (courseId, chooserConfig) => {\n const pendingPromise = new Pending();\n\n registerListenerEvents(courseId, chooserConfig);\n\n pendingPromise.resolve();\n};\n\n/**\n * Once a selection has been made make the modal & module information and pass it along\n *\n * @method registerListenerEvents\n * @param {Number} courseId\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n */\nconst registerListenerEvents = (courseId, chooserConfig) => {\n\n // Ensure we only add our listeners once.\n if (initialized) {\n return;\n }\n\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n\n const fetchModuleData = (() => {\n let innerPromises = new Map();\n\n return (sectionNum) => {\n if (innerPromises.has(sectionNum)) {\n return innerPromises.get(sectionNum);\n }\n\n innerPromises.set(\n sectionNum,\n new Promise((resolve) => {\n resolve(Repository.activityModules(courseId, sectionNum));\n })\n );\n return innerPromises.get(sectionNum);\n };\n })();\n\n const fetchFooterData = (() => {\n let footerInnerPromise = null;\n\n return (sectionNum) => {\n if (!footerInnerPromise) {\n footerInnerPromise = new Promise((resolve) => {\n resolve(Repository.fetchFooterData(courseId, sectionNum));\n });\n }\n\n return footerInnerPromise;\n };\n })();\n\n CustomEvents.define(document, events);\n\n // Display module chooser event listeners.\n events.forEach((event) => {\n document.addEventListener(event, async(e) => {\n if (e.target.closest(selectors.elements.sectionmodchooser)) {\n let caller;\n let sectionnum;\n // We need to know who called this.\n // Standard courses use the ID in the main section info.\n const sectionDiv = e.target.closest(selectors.elements.section);\n // Front page courses need some special handling.\n const button = e.target.closest(selectors.elements.sectionmodchooser);\n\n // If we don't have a section number use the fallback ID.\n // We always want the sectionDiv caller first as it keeps track of section number's after DnD changes.\n // The button attribute is always just a fallback for us as the section div is not always available.\n // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.\n if (sectionDiv !== null && sectionDiv.hasAttribute('data-number')) {\n // We check for attributes just in case of outdated contrib course formats.\n caller = sectionDiv;\n sectionnum = sectionDiv.getAttribute('data-number');\n } else {\n caller = button;\n\n if (caller.hasAttribute('data-sectionid')) {\n window.console.warn(\n 'The data-sectionid attribute has been deprecated. ' +\n 'Please update your code to use data-sectionnum instead.'\n );\n caller.setAttribute('data-sectionnum', caller.dataset.sectionid);\n }\n sectionnum = caller.dataset.sectionnum;\n }\n\n // We want to show the modal instantly but loading whilst waiting for our data.\n let bodyPromiseResolver;\n const bodyPromise = new Promise(resolve => {\n bodyPromiseResolver = resolve;\n });\n\n const footerData = await fetchFooterData(sectionnum);\n const sectionModal = buildModal(bodyPromise, footerData);\n\n // Now we have a modal we should start fetching data.\n // If an error occurs while fetching the data, display the error within the modal.\n const data = await fetchModuleData(sectionnum).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(await Templates.render('core_course/local/activitychooser/error', errorTemplateData));\n });\n\n // Early return if there is no module data.\n if (!data) {\n return;\n }\n\n // Apply the section num to all the module instance links.\n const builtModuleData = sectionMapper(\n data,\n sectionnum,\n caller.dataset.sectionreturnnum,\n caller.dataset.beforemod\n );\n\n ChooserDialogue.displayChooser(\n sectionModal,\n builtModuleData,\n partiallyAppliedFavouriteManager(data, sectionnum),\n footerData,\n );\n\n bodyPromiseResolver(await Templates.render(\n 'core_course/activitychooser',\n templateDataBuilder(builtModuleData, chooserConfig)\n ));\n }\n });\n });\n\n initialized = true;\n};\n\n/**\n * Given the web service data and an ID we want to make a deep copy\n * of the WS data then add on the section num to the addoption URL\n *\n * @method sectionMapper\n * @param {Object} webServiceData Our original data from the Web service call\n * @param {Number} num The number of the section we need to append to the links\n * @param {Number|null} sectionreturnnum The number of the section return we need to append to the links\n * @param {Number|null} beforemod The ID of the cm we need to append to the links\n * @return {Array} [modules] with URL's built\n */\nconst sectionMapper = (webServiceData, num, sectionreturnnum, beforemod) => {\n // We need to take a fresh deep copy of the original data as an object is a reference type.\n const newData = JSON.parse(JSON.stringify(webServiceData));\n newData.content_items.forEach((module) => {\n module.link += '§ion=' + num + '&beforemod=' + (beforemod ?? 0);\n if (sectionreturnnum) {\n module.link += '&sr=' + sectionreturnnum;\n }\n });\n return newData.content_items;\n};\n\n/**\n * Given an array of modules we want to figure out where & how to place them into our template object\n *\n * @method templateDataBuilder\n * @param {Array} data our modules to manipulate into a Templatable object\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n * @return {Object} Our built object ready to render out\n */\nconst templateDataBuilder = (data, chooserConfig) => {\n // Setup of various bits and pieces we need to mutate before throwing it to the wolves.\n let activities = [];\n let resources = [];\n let showAll = true;\n let showActivities = false;\n let showResources = false;\n\n // Tab mode can be the following [All, Resources & Activities, All & Activities & Resources].\n const tabMode = parseInt(chooserConfig.tabmode);\n\n // Filter the incoming data to find favourite & recommended modules.\n const favourites = data.filter(mod => mod.favourite === true);\n const recommended = data.filter(mod => mod.recommended === true);\n\n // Whether the activities and resources tabs should be displayed or not.\n const showActivitiesAndResources = (tabMode) => {\n const acceptableModes = [\n ALLACTIVITIESRESOURCES,\n ALLACTIVITIESRESOURCESREC,\n ACTIVITIESRESOURCES,\n ACTIVITIESRESOURCESREC,\n ];\n\n return acceptableModes.indexOf(tabMode) !== -1;\n };\n\n // These modes need Activity & Resource tabs.\n if (showActivitiesAndResources(tabMode)) {\n // Filter the incoming data to find activities then resources.\n activities = data.filter(mod => mod.archetype === ACTIVITY);\n resources = data.filter(mod => mod.archetype === RESOURCE);\n showActivities = true;\n showResources = true;\n\n // We want all of the previous information but no 'All' tab.\n if (tabMode === ACTIVITIESRESOURCES || tabMode === ACTIVITIESRESOURCESREC) {\n showAll = false;\n }\n }\n\n const recommendedBeforeTabs = [\n ALLACTIVITIESRESOURCESREC,\n ONLYALLREC,\n ACTIVITIESRESOURCESREC,\n ];\n // Whether the recommended tab should be displayed before the All/Activities/Resources tabs.\n const recommendedBeginning = recommendedBeforeTabs.indexOf(tabMode) !== -1;\n\n // Given the results of the above filters lets figure out what tab to set active.\n // We have some favourites.\n const favouritesFirst = !!favourites.length;\n const recommendedFirst = favouritesFirst === false && recommendedBeginning === true && !!recommended.length;\n // We are in tabMode 2 without any favourites.\n const activitiesFirst = showAll === false && favouritesFirst === false && recommendedFirst === false;\n // We have nothing fallback to show all modules.\n const fallback = showAll === true && favouritesFirst === false && recommendedFirst === false;\n\n return {\n 'default': data,\n showAll: showAll,\n activities: activities,\n showActivities: showActivities,\n activitiesFirst: activitiesFirst,\n resources: resources,\n showResources: showResources,\n favourites: favourites,\n recommended: recommended,\n recommendedFirst: recommendedFirst,\n recommendedBeginning: recommendedBeginning,\n favouritesFirst: favouritesFirst,\n fallback: fallback,\n };\n};\n\n/**\n * Given an object we want to build a modal ready to show\n *\n * @method buildModal\n * @param {Promise} body\n * @param {String|Boolean} footer Either a footer to add or nothing\n * @return {Object} The modal ready to display immediately and render body in later.\n */\nconst buildModal = (body, footer) => Modal.create({\n body,\n title: getString('addresourceoractivity'),\n footer: footer.customfootertemplate,\n large: true,\n scrollable: false,\n templateContext: {\n classes: 'modchooser'\n },\n show: true,\n});\n\n/**\n * A small helper function to handle the case where there are no more favourites\n * and we need to mess a bit with the available tabs in the chooser\n *\n * @method nullFavouriteDomManager\n * @param {HTMLElement} favouriteTabNav Dom node of the favourite tab nav\n * @param {HTMLElement} modalBody Our current modals' body\n */\nconst nullFavouriteDomManager = (favouriteTabNav, modalBody) => {\n favouriteTabNav.tabIndex = -1;\n favouriteTabNav.classList.add('d-none');\n // Need to set active to an available tab.\n if (favouriteTabNav.classList.contains('active')) {\n favouriteTabNav.classList.remove('active');\n favouriteTabNav.setAttribute('aria-selected', 'false');\n const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);\n favouriteTab.classList.remove('active');\n const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);\n const activitiesTabNav = modalBody.querySelector(selectors.regions.activityTabNav);\n if (defaultTabNav.classList.contains('d-none') === false) {\n defaultTabNav.classList.add('active');\n defaultTabNav.setAttribute('aria-selected', 'true');\n defaultTabNav.tabIndex = 0;\n defaultTabNav.focus();\n const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);\n defaultTab.classList.add('active');\n } else {\n activitiesTabNav.classList.add('active');\n activitiesTabNav.setAttribute('aria-selected', 'true');\n activitiesTabNav.tabIndex = 0;\n activitiesTabNav.focus();\n const activitiesTab = modalBody.querySelector(selectors.regions.activityTab);\n activitiesTab.classList.add('active');\n }\n\n }\n};\n\n/**\n * Export a curried function where the builtModules has been applied.\n * We have our array of modules so we can rerender the favourites area and have all of the items sorted.\n *\n * @method partiallyAppliedFavouriteManager\n * @param {Array} moduleData This is our raw WS data that we need to manipulate\n * @param {Number} sectionnum We need this to add the sectionnum to the URL's in the faves area after rerender\n * @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array\n */\nconst partiallyAppliedFavouriteManager = (moduleData, sectionnum) => {\n /**\n * Curried function that is being returned.\n *\n * @param {String} internal Internal name of the module to manage\n * @param {Boolean} favourite Is the caller adding a favourite or removing one?\n * @param {HTMLElement} modalBody What we need to update whilst we are here\n */\n return async(internal, favourite, modalBody) => {\n const favouriteArea = modalBody.querySelector(selectors.render.favourites);\n\n // eslint-disable-next-line max-len\n const favouriteButtons = modalBody.querySelectorAll(`[data-internal=\"${internal}\"] ${selectors.actions.optionActions.manageFavourite}`);\n const favouriteTabNav = modalBody.querySelector(selectors.regions.favouriteTabNav);\n const result = moduleData.content_items.find(({name}) => name === internal);\n const newFaves = {};\n if (result) {\n if (favourite) {\n result.favourite = true;\n\n // eslint-disable-next-line camelcase\n newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);\n\n const builtFaves = sectionMapper(newFaves, sectionnum);\n\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/favourites',\n {favourites: builtFaves});\n\n await Templates.replaceNodeContents(favouriteArea, html, js);\n\n Array.from(favouriteButtons).forEach((element) => {\n element.classList.remove('text-muted');\n element.classList.add('text-primary');\n element.dataset.favourited = 'true';\n element.setAttribute('aria-pressed', true);\n element.firstElementChild.classList.remove('fa-star-o');\n element.firstElementChild.classList.add('fa-star');\n });\n\n favouriteTabNav.classList.remove('d-none');\n } else {\n result.favourite = false;\n\n const nodeToRemove = favouriteArea.querySelector(`[data-internal=\"${internal}\"]`);\n\n nodeToRemove.parentNode.removeChild(nodeToRemove);\n\n Array.from(favouriteButtons).forEach((element) => {\n element.classList.add('text-muted');\n element.classList.remove('text-primary');\n element.dataset.favourited = 'false';\n element.setAttribute('aria-pressed', false);\n element.firstElementChild.classList.remove('fa-star');\n element.firstElementChild.classList.add('fa-star-o');\n });\n const newFaves = moduleData.content_items.filter(mod => mod.favourite === true);\n\n if (newFaves.length === 0) {\n nullFavouriteDomManager(favouriteTabNav, modalBody);\n }\n }\n }\n };\n};\n"],"names":["initialized","courseId","chooserConfig","pendingPromise","Pending","registerListenerEvents","resolve","events","CustomEvents","activate","keyboardActivate","fetchModuleData","innerPromises","Map","sectionNum","has","set","Promise","Repository","activityModules","get","fetchFooterData","footerInnerPromise","define","document","forEach","event","addEventListener","async","e","target","closest","selectors","elements","sectionmodchooser","caller","sectionnum","sectionDiv","section","button","bodyPromiseResolver","hasAttribute","getAttribute","window","console","warn","setAttribute","dataset","sectionid","bodyPromise","footerData","sectionModal","buildModal","data","catch","errorTemplateData","message","Templates","render","builtModuleData","sectionMapper","sectionreturnnum","beforemod","ChooserDialogue","displayChooser","partiallyAppliedFavouriteManager","templateDataBuilder","webServiceData","num","newData","JSON","parse","stringify","content_items","module","link","activities","resources","showAll","showActivities","showResources","tabMode","parseInt","tabmode","favourites","filter","mod","favourite","recommended","indexOf","showActivitiesAndResources","archetype","recommendedBeginning","favouritesFirst","length","recommendedFirst","activitiesFirst","fallback","body","footer","Modal","create","title","customfootertemplate","large","scrollable","templateContext","classes","show","moduleData","internal","modalBody","favouriteArea","querySelector","favouriteButtons","querySelectorAll","actions","optionActions","manageFavourite","favouriteTabNav","regions","result","find","_ref","name","newFaves","builtFaves","html","js","renderForPromise","replaceNodeContents","Array","from","element","classList","remove","add","favourited","firstElementChild","nodeToRemove","parentNode","removeChild","tabIndex","contains","favouriteTab","defaultTabNav","activitiesTabNav","activityTabNav","focus","defaultTab","activityTab","nullFavouriteDomManager"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"activitychooser.min.js","sources":["../src/activitychooser.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 * A type of dialogue used as for choosing modules in a course.\n *\n * @module core_course/activitychooser\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport CustomEvents from 'core/custom_interaction_events';\nimport Pending from 'core/pending';\n\nlet initialized = false;\n\n/**\n * Set up the activity chooser.\n *\n * @method init\n * @param {Number} courseId Course ID to use later on in fetchModules()\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n */\nexport const init = (courseId, chooserConfig) => {\n const pendingPromise = new Pending();\n\n // TODO: Remove the chooserConfig in Moodle 6.0 (MDL-85655)\n if (chooserConfig.tabmode !== undefined) {\n window.console.warn('The tabmode config option has been deprecated and will be ignored.');\n }\n\n registerListenerEvents(courseId);\n\n pendingPromise.resolve();\n};\n\n/**\n * Once a selection has been made make the modal & module information and pass it along\n *\n * @method registerListenerEvents\n * @param {Number} courseId\n */\nconst registerListenerEvents = (courseId) => {\n if (initialized) {\n return;\n }\n initialized = true;\n\n const eventsToHandle = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n\n CustomEvents.define(document, eventsToHandle);\n\n // Display module chooser event listeners.\n eventsToHandle.forEach((eventToHandle) => {\n document.addEventListener(eventToHandle, async(event) => {\n if (!event.target.closest(selectors.elements.sectionmodchooser)) {\n return;\n }\n const position = getCoursePositionFromTarget(event.target);\n\n const footerDataPromise = Repository.getModalFooterData(courseId, position.sectionNum);\n\n const modulesDataPromise = Repository.getModulesData(\n courseId,\n position.sectionNum,\n position.sectionReturnNum,\n position.beforeMod,\n );\n\n ChooserDialogue.displayActivityChooserModal(footerDataPromise, modulesDataPromise);\n });\n });\n};\n\n/**\n * Return the course position of a target add activity element.\n *\n * @param {HTMLElement} target The target element.\n * @return {Object} The course position of the target.\n * @property {Number} sectionNum The section number.\n * @property {Number|null} sectionReturnNum The section return number.\n * @property {Number|null} beforeMod The ID of the cm to add the modules before.\n */\nfunction getCoursePositionFromTarget(target) {\n let caller;\n let sectionNum;\n // We need to know who called this.\n // Standard courses use the ID in the main section info.\n const sectionDiv = target.closest(selectors.elements.section);\n // Front page courses need some special handling.\n const button = target.closest(selectors.elements.sectionmodchooser);\n\n // If we don't have a section number use the fallback ID.\n // We always want the sectionDiv caller first as it keeps track of section number's after DnD changes.\n // The button attribute is always just a fallback for us as the section div is not always available.\n // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.\n if (sectionDiv !== null && sectionDiv.hasAttribute('data-number')) {\n // We check for attributes just in case of outdated contrib course formats.\n caller = sectionDiv;\n sectionNum = sectionDiv.getAttribute('data-number');\n } else {\n caller = button;\n\n if (caller.hasAttribute('data-sectionid')) {\n window.console.warn(\n 'The data-sectionid attribute has been deprecated. ' +\n 'Please update your code to use data-sectionnum instead.'\n );\n caller.setAttribute('data-sectionnum', caller.dataset.sectionid);\n }\n sectionNum = caller.dataset.sectionnum;\n }\n return {\n sectionNum,\n sectionReturnNum: caller.dataset?.sectionreturnnum ?? null,\n beforeMod: caller.dataset?.beforemod ?? null,\n };\n}\n"],"names":["initialized","courseId","chooserConfig","pendingPromise","Pending","undefined","tabmode","window","console","warn","registerListenerEvents","resolve","eventsToHandle","CustomEvents","events","activate","keyboardActivate","define","document","forEach","eventToHandle","addEventListener","async","event","target","closest","selectors","elements","sectionmodchooser","position","caller","sectionNum","sectionDiv","section","button","hasAttribute","getAttribute","setAttribute","dataset","sectionid","sectionnum","sectionReturnNum","_caller$dataset","sectionreturnnum","beforeMod","_caller$dataset2","beforemod","getCoursePositionFromTarget","footerDataPromise","Repository","getModalFooterData","modulesDataPromise","getModulesData","ChooserDialogue","displayActivityChooserModal"],"mappings":";;;;;;;oWA6BIA,aAAc,gBASE,CAACC,SAAUC,uBACrBC,eAAiB,IAAIC,sBAGGC,IAA1BH,cAAcI,SACdC,OAAOC,QAAQC,KAAK,sEAGxBC,uBAAuBT,UAEvBE,eAAeQ,iBASbD,uBAA0BT,cACxBD,mBAGJA,aAAc,QAERY,eAAiB,CACnB,QACAC,mCAAaC,OAAOC,SACpBF,mCAAaC,OAAOE,qDAGXC,OAAOC,SAAUN,gBAG9BA,eAAeO,SAASC,gBACpBF,SAASG,iBAAiBD,eAAeE,MAAAA,YAChCC,MAAMC,OAAOC,QAAQC,mBAAUC,SAASC,gCAGvCC,kBAyBmBL,6FAC7BM,OACAC,iBAGEC,WAAaR,OAAOC,QAAQC,mBAAUC,SAASM,SAE/CC,OAASV,OAAOC,QAAQC,mBAAUC,SAASC,mBAM9B,OAAfI,YAAuBA,WAAWG,aAAa,gBAE/CL,OAASE,WACTD,WAAaC,WAAWI,aAAa,iBAErCN,OAASI,OAELJ,OAAOK,aAAa,oBACpB5B,OAAOC,QAAQC,KACX,6GAGJqB,OAAOO,aAAa,kBAAmBP,OAAOQ,QAAQC,YAE1DR,WAAaD,OAAOQ,QAAQE,kBAEzB,CACHT,WAAAA,WACAU,uEAAkBX,OAAOQ,0CAAPI,gBAAgBC,wEAAoB,KACtDC,iEAAWd,OAAOQ,2CAAPO,iBAAgBC,iEAAa,MAzDnBC,CAA4BxB,MAAMC,QAE7CwB,kBAAoBC,WAAWC,mBAAmBjD,SAAU4B,SAASE,YAErEoB,mBAAqBF,WAAWG,eAClCnD,SACA4B,SAASE,WACTF,SAASY,iBACTZ,SAASe,WAGbS,gBAAgBC,4BAA4BN,kBAAmBG"} \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/dialogue.min.js b/course/amd/build/local/activitychooser/dialogue.min.js index ab2293a115b..2e6520cb797 100644 --- a/course/amd/build/local/activitychooser/dialogue.min.js +++ b/course/amd/build/local/activitychooser/dialogue.min.js @@ -1,3 +1,3 @@ -define("core_course/local/activitychooser/dialogue",["exports","theme_boost/bootstrap/carousel","core/modal_events","core_course/local/activitychooser/selectors","core/templates","core/key_codes","core/loadingicon","core_course/local/activitychooser/repository","core/notification","core/utils","core/normalise"],(function(_exports,_carousel,ModalEvents,_selectors,Templates,_key_codes,_loadingicon,Repository,_notification,_utils,_normalise){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayChooser=void 0,_carousel=_interopRequireDefault(_carousel),ModalEvents=_interopRequireWildcard(ModalEvents),_selectors=_interopRequireDefault(_selectors),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),_notification=_interopRequireDefault(_notification);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const showModuleHelp=function(carousel,moduleData){let modal=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;null!==modal&&!0===moduleData.showFooter&&modal.setFooter(Templates.render("core_course/local/activitychooser/footer_partial",moduleData));const help=carousel.querySelector(_selectors.default.regions.help);help.innerHTML="",help.classList.add("m-auto");const spinnerPromise=(0,_loadingicon.addIconToContainer)(help);let transitionPromiseResolver=null;const transitionPromise=new Promise((resolve=>{transitionPromiseResolver=resolve})),contentPromise=Templates.renderForPromise("core_course/local/activitychooser/help",moduleData);Promise.all([contentPromise,spinnerPromise,transitionPromise]).then((_ref=>{let[{html:html,js:js}]=_ref;return Templates.replaceNodeContents(help,html,js)})).then((()=>(help.querySelector(_selectors.default.regions.chooserSummary.header).focus(),help))).catch(_notification.default.exception),carousel.addEventListener("slid.bs.carousel",(()=>{transitionPromiseResolver()}),{once:!0}),_carousel.default.getInstance(carousel).next()},registerListenerEvents=(modal,mappedModules,partialFavourite,footerData)=>{const modalBody=(0,_normalise.getFirst)(modal.getBody()),bodyClickListener=async e=>{if(e.target.closest(_selectors.default.actions.optionActions.showSummary)){const carousel=modalBody.querySelector(_selectors.default.regions.carousel),moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName);moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}if(e.target.closest(_selectors.default.actions.optionActions.manageFavourite)){const caller=e.target.closest(_selectors.default.actions.optionActions.manageFavourite);await(async(modalBody,caller,partialFavourite)=>{const isFavourite=caller.dataset.favourited,id=caller.dataset.id,name=caller.dataset.name,internal=caller.dataset.internal;"true"===isFavourite?(await Repository.unfavouriteModule(name,id),partialFavourite(internal,!1,modalBody)):(await Repository.favouriteModule(name,id),partialFavourite(internal,!0,modalBody))})(modalBody,caller,partialFavourite);const activeSectionId=modalBody.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=modalBody.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(modalBody,mappedModules,sectionChooserOptions,modal)}if(e.target.matches(_selectors.default.actions.closeOption)){const carousel=modalBody.querySelector(_selectors.default.regions.carousel);_carousel.default.getInstance(carousel).prev(),carousel.addEventListener("slid.bs.carousel",(()=>{modalBody.querySelector(_selectors.default.regions.modules).querySelector(_selectors.default.regions.getModuleSelector(e.target.dataset.modname)).focus()}))}if(e.target.closest(_selectors.default.actions.clearSearch)){const searchInput=modalBody.querySelector(_selectors.default.actions.search);searchInput.value="",searchInput.focus(),toggleSearchResultsView(modal,mappedModules,searchInput.value)}},footerClickListener=async e=>{if(!0===footerData.footer){const footerjs=await(pluginName=footerData.customfooterjs,"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginName],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginName)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginName]));await footerjs.footerClickListener(e,footerData,modal)}var pluginName};modal.getBodyPromise().then((body=>body[0])).then((body=>{const carousel=document.querySelector(_selectors.default.regions.carousel);return new _carousel.default(carousel,{interval:!1,pause:!0,keyboard:!1}),body})).then((body=>(body.addEventListener("click",bodyClickListener),body))).then((body=>{const searchInput=body.querySelector(_selectors.default.actions.search);return searchInput.addEventListener("input",(0,_utils.debounce)((()=>{toggleSearchResultsView(modal,mappedModules,searchInput.value)}),300)),body})).then((body=>{const activeSectionId=body.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=body.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);return toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body,mappedModules,sectionChooserOptions,modal),body})).catch(_notification.default.exception),modal.getFooterPromise().then((footer=>footer[0])).then((footer=>(footer.addEventListener("click",footerClickListener),footer))).catch(_notification.default.exception)},initChooserOptionsKeyboardNavigation=function(body,mappedModules,chooserOptionsContainer){let modal=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const chooserOptions=chooserOptionsContainer.querySelectorAll(_selectors.default.regions.chooserOption.container);Array.from(chooserOptions).forEach((element=>element.addEventListener("keydown",(e=>{if((e.keyCode===_key_codes.enter||e.keyCode===_key_codes.space)&&e.target.matches(_selectors.default.actions.optionActions.showSummary)){e.preventDefault();const moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName),carousel=document.querySelector(_selectors.default.regions.carousel);new _carousel.default({interval:!1,pause:!0,keyboard:!1}),moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}if(e.keyCode===_key_codes.arrowRight){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),nextOption=currentOption.nextElementSibling,firstOption=chooserOptionsContainer.firstElementChild,toFocusOption=clickErrorHandler(nextOption,firstOption);focusChooserOption(toFocusOption,currentOption)}if(e.keyCode===_key_codes.arrowLeft){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),previousOption=currentOption.previousElementSibling,lastOption=chooserOptionsContainer.lastElementChild,toFocusOption=clickErrorHandler(previousOption,lastOption);focusChooserOption(toFocusOption,currentOption)}if(e.keyCode===_key_codes.home){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),firstOption=chooserOptionsContainer.firstElementChild;focusChooserOption(firstOption,currentOption)}if(e.keyCode===_key_codes.end){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),lastOption=chooserOptionsContainer.lastElementChild;focusChooserOption(lastOption,currentOption)}}))))},focusChooserOption=function(currentChooserOption){let previousChooserOption=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;null!==previousChooserOption&&toggleFocusableChooserOption(previousChooserOption,!1),toggleFocusableChooserOption(currentChooserOption,!0),currentChooserOption.focus()},toggleFocusableChooserOption=(chooserOption,isFocusable)=>{const chooserOptionLink=chooserOption.querySelector(_selectors.default.actions.addChooser),chooserOptionHelp=chooserOption.querySelector(_selectors.default.actions.optionActions.showSummary),chooserOptionFavourite=chooserOption.querySelector(_selectors.default.actions.optionActions.manageFavourite);isFocusable?(chooserOption.tabIndex=0,chooserOptionLink.tabIndex=0,chooserOptionHelp.tabIndex=0,chooserOptionFavourite.tabIndex=0):(chooserOption.tabIndex=-1,chooserOptionLink.tabIndex=-1,chooserOptionHelp.tabIndex=-1,chooserOptionFavourite.tabIndex=-1)},clickErrorHandler=(item,fallback)=>null!==item?item:fallback,toggleSearchResultsView=async(modal,mappedModules,searchQuery)=>{const modalBody=modal.getBody()[0],searchResultsContainer=modalBody.querySelector(_selectors.default.regions.searchResults),chooserContainer=modalBody.querySelector(_selectors.default.regions.chooser),clearSearchButton=modalBody.querySelector(_selectors.default.actions.clearSearch);if(searchQuery.length>0){const searchResultsData=searchModules(mappedModules,searchQuery);await(async(searchResultsContainer,searchResultsData)=>{const templateData={searchresultsnumber:searchResultsData.length,searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/search_results",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)})(searchResultsContainer,searchResultsData);const searchResultItemsContainer=searchResultsContainer.querySelector(_selectors.default.regions.searchResultItems),firstSearchResultItem=searchResultItemsContainer.querySelector(_selectors.default.regions.chooserOption.container);firstSearchResultItem&&(toggleFocusableChooserOption(firstSearchResultItem,!0),initChooserOptionsKeyboardNavigation(modalBody,mappedModules,searchResultItemsContainer,modal)),clearSearchButton.classList.remove("d-none"),chooserContainer.setAttribute("hidden","hidden"),searchResultsContainer.removeAttribute("hidden")}else clearSearchButton.classList.add("d-none"),searchResultsContainer.setAttribute("hidden","hidden"),chooserContainer.removeAttribute("hidden")},searchModules=(modules,searchTerm)=>{if(""===searchTerm)return modules;searchTerm=searchTerm.toLowerCase();const searchResults=[];return modules.forEach((activity=>{const activityName=activity.title.toLowerCase(),activityDesc=activity.help.toLowerCase();(activityName.includes(searchTerm)||activityDesc.includes(searchTerm))&&searchResults.push(activity)})),searchResults},disableFocusAllChooserOptions=sectionChooserOptions=>{sectionChooserOptions.querySelectorAll(_selectors.default.regions.chooserOption.container).forEach((chooserOption=>{toggleFocusableChooserOption(chooserOption,!1)}))};_exports.displayChooser=(modalPromise,sectionModules,partialFavourite,footerData)=>{const mappedModules=new Map;sectionModules.forEach((module=>{mappedModules.set(module.componentname+"_"+module.link,module)})),modalPromise.then((modal=>(registerListenerEvents(modal,mappedModules,partialFavourite,footerData),((modal,mappedModules)=>{modal.getModal()[0].tabIndex=-1,modal.getBodyPromise().then((body=>{document.querySelectorAll(_selectors.default.elements.tab).forEach((tab=>{tab.addEventListener("shown.bs.tab",(e=>{const activeSectionId=e.target.getAttribute("href"),activeSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=activeSectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container),prevActiveSectionId=e.relatedTarget.getAttribute("href"),prevActiveSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(prevActiveSectionId));disableFocusAllChooserOptions(prevActiveSectionChooserOptions),toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body[0],mappedModules,activeSectionChooserOptions,modal)}))}))})).catch(_notification.default.exception)})(modal,mappedModules),modal.getRoot().on(ModalEvents.hidden,(()=>{modal.destroy()})),modal))).catch(_notification.default.exception)}})); +define("core_course/local/activitychooser/dialogue",["exports","core/utils","core_course/local/activitychooser/dialoguedom","core/key_codes","core_course/local/activitychooser/exporter","core/normalise","core/str","core/modal","core/modal_events","core/notification","core_course/local/activitychooser/repository","core_course/local/activitychooser/selectors","core/templates"],(function(_exports,_utils,_dialoguedom,_key_codes,_exporter,_normalise,_str,_modal,ModalEvents,_notification,Repository,_selectors,Templates){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayActivityChooserModal=async function(footerDataPromise,modulesDataPromise){let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve})),footerData=await footerDataPromise,sectionModal=_modal.default.create({body:bodyPromise,title:(0,_str.getString)("addresourceoractivity"),footer:footerData.customfootertemplate,large:!0,scrollable:!1,templateContext:{classes:"modchooser"},show:!0});try{const modulesData=await modulesDataPromise;if(!modulesData)return;const modal=await sectionModal,dialogue=new ActivityChooserDialogue(modal,modulesData,footerData),templateData=await dialogue.exporter.getModChooserTemplateData(modulesData);bodyPromiseResolver(await Templates.render("core_course/activitychooser",templateData))}catch(error){const errorTemplateData={errormessage:error.message};return void bodyPromiseResolver(await Templates.render("core_course/local/activitychooser/error",errorTemplateData))}},_exports.displayChooser=void 0,_dialoguedom=_interopRequireDefault(_dialoguedom),_exporter=_interopRequireDefault(_exporter),_modal=_interopRequireDefault(_modal),ModalEvents=_interopRequireWildcard(ModalEvents),_notification=_interopRequireDefault(_notification),Repository=_interopRequireWildcard(Repository),_selectors=_interopRequireDefault(_selectors),Templates=_interopRequireWildcard(Templates);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}_exports.displayChooser=(modalPromise,sectionModules,partialFavourite,footerData)=>{window.console.warn("The displayChooser function is deprecated. Please displayActivityChooserModal instead."),modalPromise.then((modal=>(new ActivityChooserDialogue(modal,sectionModules,footerData),modal))).catch(_notification.default.exception)};class ActivityChooserDialogue{constructor(modal,modulesData,footerData){this.modal=modal,this.dialogueDom=null,this.footerData=footerData,this.exporter=new _exporter.default,this.isFavouriteTabDirty=!1,this.mappedModules=new Map,modulesData.forEach((module=>{this.mappedModules.set(module.componentname+"_"+module.link,module)})),this.init()}async init(){const modalBody=(0,_normalise.getFirst)(await this.modal.getBodyPromise());this.dialogueDom=new _dialoguedom.default(this,modalBody,this.exporter),this.registerModalListenerEvents(),this.setupKeyboardAccessibility(),this.modal.getRoot().on(ModalEvents.hidden,(()=>{this.modal.destroy()}))}async registerModalListenerEvents(){const modalRoot=(0,_normalise.getFirst)(this.modal.getRoot());modalRoot.addEventListener("shown.bs.tab",(event=>{if(event.target.closest(_selectors.default.regions.allTabNav))return;const searchInput=this.dialogueDom.getSearchInputElement();searchInput.value.length>0&&(searchInput.value="",this.toggleSearchResultsView(searchInput.value))})),modalRoot.addEventListener("click",this.handleModalClick.bind(this));const searchInput=this.dialogueDom.getSearchInputElement();searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.toggleSearchResultsView(searchInput.value)}),300,{pending:!0})),this.dialogueDom.initBootstrapComponents(),modalRoot.addEventListener("shown.bs.tab",(event=>{event.relatedTarget&&this.dialogueDom.disableFocusAllChooserOptions(event.relatedTarget),this.dialogueDom.initActiveTabNavigation()})),modalRoot.addEventListener("shown.bs.tab",(()=>{this.isFavouriteTabDirty&&!this.dialogueDom.isFavoutiteTabActive()&&this.refreshFavouritesTabContent()})),this.dialogueDom.initActiveTabNavigation();(0,_normalise.getFirst)(await this.modal.getFooterPromise()).addEventListener("click",this.handleFooterClick.bind(this))}async handleFooterClick(event){if(!0===this.footerData.footer){const footerjs=await(pluginName=this.footerData.customfooterjs,"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginName],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginName)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginName]));await footerjs.footerClickListener(event,this.footerData,this.modal)}var pluginName}async handleModalClick(event){const target=event.target;target.closest(_selectors.default.actions.optionActions.showSummary)&&this.handleShowSummary(target),target.closest(_selectors.default.actions.optionActions.manageFavourite)&&await this.handleFavouriteClick(target),target.matches(_selectors.default.actions.closeOption)&&this.dialogueDom.hideModuleHelp(target.dataset.modname),target.closest(_selectors.default.actions.clearSearch)&&this.handleClearSearch()}handleShowSummary(target){const moduleName=this.dialogueDom.getClosestChooserOption(target).dataset.modname,moduleData=this.mappedModules.get(moduleName);moduleData.showFooter=this.modal.hasFooterContent(),this.dialogueDom.showModuleHelp(moduleData,this.modal)}async handleFavouriteClick(target){const caller=target.closest(_selectors.default.actions.optionActions.manageFavourite),id=caller.dataset.id,name=caller.dataset.name,internal=caller.dataset.internal;"true"===caller.dataset.favourited?(await Repository.unfavouriteModule(name,id),this.updateFavouriteItemValue(internal,!1)):(await Repository.favouriteModule(name,id),this.updateFavouriteItemValue(internal,!0))}handleClearSearch(){const searchInput=this.dialogueDom.getSearchInputElement();searchInput.value="",searchInput.focus(),this.toggleSearchResultsView(searchInput.value)}setupKeyboardAccessibility(){const mainElement=(0,_normalise.getFirst)(this.modal.getModal());mainElement.tabIndex=-1,mainElement.addEventListener("keydown",(e=>{const currentOption=this.dialogueDom.getClosestChooserOption(e.target);null!==currentOption&&(e.keyCode!==_key_codes.enter&&e.keyCode!==_key_codes.space||e.target.matches(_selectors.default.actions.optionActions.showSummary)&&(e.preventDefault(),this.handleShowSummary(e.target)),e.keyCode===_key_codes.arrowRight&&(e.preventDefault(),this.dialogueDom.focusNextChooserOption(currentOption)),e.keyCode===_key_codes.arrowLeft&&(e.preventDefault(),this.dialogueDom.focusPreviousChooserOption(currentOption)),e.keyCode===_key_codes.home&&(e.preventDefault(),this.dialogueDom.focusFirstChooserOption(currentOption)),e.keyCode===_key_codes.end&&(e.preventDefault(),this.dialogueDom.focusLastChooserOption(currentOption)))}))}async toggleSearchResultsView(searchQuery){const searchResultsData=this.searchModules(searchQuery);searchQuery.length>0?(await this.dialogueDom.refreshSearchResults(searchResultsData),this.dialogueDom.showAllActivitiesTab(!0)):this.dialogueDom.cleanSearchResults()}searchModules(searchTerm){if(""===searchTerm)return this.mappedModules;searchTerm=searchTerm.toLowerCase();const searchResults=[];return this.mappedModules.forEach((activity=>{const activityName=activity.title.toLowerCase(),activityDesc=activity.help.toLowerCase();(activityName.includes(searchTerm)||activityDesc.includes(searchTerm))&&searchResults.push(activity)})),searchResults}async updateFavouriteItemValue(internal,favourite){const moduleItem=this.mappedModules.find((_ref=>{let{name:name}=_ref;return name===internal}));moduleItem&&(moduleItem.favourite=favourite,this.dialogueDom.updateItemStarredIcons(internal,favourite),this.dialogueDom.isFavoutiteTabActive()?this.isFavouriteTabDirty=!0:this.refreshFavouritesTabContent())}async refreshFavouritesTabContent(){this.isFavouriteTabDirty=!1;const favouriteCount=this.mappedModules.filter((mod=>!0===mod.favourite)).size;this.dialogueDom.toggleFavouriteTabDisplay(favouriteCount>0),await this.dialogueDom.refreshFavouritesTabContent(this.mappedModules)}}})); //# sourceMappingURL=dialogue.min.js.map \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/dialogue.min.js.map b/course/amd/build/local/activitychooser/dialogue.min.js.map index 4898b189a0f..02cd43e2b50 100644 --- a/course/amd/build/local/activitychooser/dialogue.min.js.map +++ b/course/amd/build/local/activitychooser/dialogue.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dialogue.min.js","sources":["../../../src/local/activitychooser/dialogue.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 * A type of dialogue used as for choosing options.\n *\n * @module core_course/local/activitychooser/dialogue\n * @copyright 2019 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Carousel from 'theme_boost/bootstrap/carousel';\nimport * as ModalEvents from 'core/modal_events';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport * as Templates from 'core/templates';\nimport {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';\nimport {addIconToContainer} from 'core/loadingicon';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport Notification from 'core/notification';\nimport {debounce} from 'core/utils';\nimport {getFirst} from 'core/normalise';\nconst getPlugin = pluginName => import(pluginName);\n\n/**\n * Given an event from the main module 'page' navigate to it's help section via a carousel.\n *\n * @method showModuleHelp\n * @param {Element} carousel Our initialized carousel to manipulate\n * @param {Object} moduleData Data of the module to carousel to\n * @param {jQuery} modal We need to figure out if the current modal has a footer.\n */\nconst showModuleHelp = (carousel, moduleData, modal = null) => {\n // If we have a real footer then we need to change temporarily.\n if (modal !== null && moduleData.showFooter === true) {\n modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));\n }\n const help = carousel.querySelector(selectors.regions.help);\n help.innerHTML = '';\n help.classList.add('m-auto');\n\n // Add a spinner.\n const spinnerPromise = addIconToContainer(help);\n\n // Used later...\n let transitionPromiseResolver = null;\n const transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n // Build up the html & js ready to place into the help section.\n const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);\n\n // Wait for the content to be ready, and for the transition to be complet.\n Promise.all([contentPromise, spinnerPromise, transitionPromise])\n .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))\n .then(() => {\n help.querySelector(selectors.regions.chooserSummary.header).focus();\n return help;\n })\n .catch(Notification.exception);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.addEventListener('slid.bs.carousel', () => {\n transitionPromiseResolver();\n }, {once: true});\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).next();\n};\n\n/**\n * Given a user wants to change the favourite state of a module we either add or remove the status.\n * We also propergate this change across our map of modals.\n *\n * @method manageFavouriteState\n * @param {HTMLElement} modalBody The DOM node of the modal to manipulate\n * @param {HTMLElement} caller\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n */\nconst manageFavouriteState = async(modalBody, caller, partialFavourite) => {\n const isFavourite = caller.dataset.favourited;\n const id = caller.dataset.id;\n const name = caller.dataset.name;\n const internal = caller.dataset.internal;\n // Switch on fave or not.\n if (isFavourite === 'true') {\n await Repository.unfavouriteModule(name, id);\n\n partialFavourite(internal, false, modalBody);\n } else {\n await Repository.favouriteModule(name, id);\n\n partialFavourite(internal, true, modalBody);\n }\n\n};\n\n/**\n * Register chooser related event listeners.\n *\n * @method registerListenerEvents\n * @param {Promise} modal Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nconst registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {\n const modalBody = getFirst(modal.getBody());\n const bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = modalBody.querySelector(selectors.regions.carousel);\n\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n\n if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {\n const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);\n await manageFavouriteState(modalBody, caller, partialFavourite);\n const activeSectionId = modalBody.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modalBody\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, sectionChooserOptions, modal);\n }\n\n // From the help screen go back to the module overview.\n if (e.target.matches(selectors.actions.closeOption)) {\n const carousel = modalBody.querySelector(selectors.regions.carousel);\n\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).prev();\n carousel.addEventListener('slid.bs.carousel', () => {\n const allModules = modalBody.querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));\n caller.focus();\n });\n }\n\n // The \"clear search\" button is triggered.\n if (e.target.closest(selectors.actions.clearSearch)) {\n // Clear the entered search query in the search bar and hide the search results container.\n const searchInput = modalBody.querySelector(selectors.actions.search);\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }\n };\n\n // We essentially have two types of footer.\n // A fake one that is handled within the template for chooser_help and then all of the stuff for\n // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we\n // need to manage. The below code handles a real footer going to a mnet carousel item.\n const footerClickListener = async(e) => {\n if (footerData.footer === true) {\n const footerjs = await getPlugin(footerData.customfooterjs);\n await footerjs.footerClickListener(e, footerData, modal);\n }\n };\n\n modal.getBodyPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(body => body[0])\n\n // Set up the carousel.\n .then(body => {\n const carousel = document.querySelector(selectors.regions.carousel);\n new Carousel(carousel, {\n interval: false,\n pause: true,\n keyboard: false\n });\n\n return body;\n })\n\n // Add the listener for clicks on the body.\n .then(body => {\n body.addEventListener('click', bodyClickListener);\n return body;\n })\n\n // Add a listener for an input change in the activity chooser's search bar.\n .then(body => {\n const searchInput = body.querySelector(selectors.actions.search);\n // The search input is triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }, 300));\n return body;\n })\n\n // Register event listeners related to the keyboard navigation controls.\n .then(body => {\n // Get the active chooser options section.\n const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);\n\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);\n\n return body;\n })\n .catch(Notification.exception);\n\n modal.getFooterPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(footer => footer[0])\n // Add the listener for clicks on the footer.\n .then(footer => {\n footer.addEventListener('click', footerClickListener);\n return footer;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Initialise the keyboard navigation controls for the chooser options.\n *\n * @method initChooserOptionsKeyboardNavigation\n * @param {HTMLElement} body Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items\n * @param {Object} modal Our created modal for the section\n */\nconst initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {\n const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);\n\n Array.from(chooserOptions).forEach((element) => {\n return element.addEventListener('keydown', (e) => {\n\n // Check for enter/ space triggers for showing the help.\n if (e.keyCode === enter || e.keyCode === space) {\n if (e.target.matches(selectors.actions.optionActions.showSummary)) {\n e.preventDefault();\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n const carousel = document.querySelector(selectors.regions.carousel);\n new Carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n }\n\n // Next.\n if (e.keyCode === arrowRight) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const nextOption = currentOption.nextElementSibling;\n const firstOption = chooserOptionsContainer.firstElementChild;\n const toFocusOption = clickErrorHandler(nextOption, firstOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n // Previous.\n if (e.keyCode === arrowLeft) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const previousOption = currentOption.previousElementSibling;\n const lastOption = chooserOptionsContainer.lastElementChild;\n const toFocusOption = clickErrorHandler(previousOption, lastOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n if (e.keyCode === home) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const firstOption = chooserOptionsContainer.firstElementChild;\n focusChooserOption(firstOption, currentOption);\n }\n\n if (e.keyCode === end) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const lastOption = chooserOptionsContainer.lastElementChild;\n focusChooserOption(lastOption, currentOption);\n }\n });\n });\n};\n\n/**\n * Focus on a chooser option element and remove the previous chooser element from the focus order\n *\n * @method focusChooserOption\n * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus\n * @param {HTMLElement|null} previousChooserOption The previous focused option element\n */\nconst focusChooserOption = (currentChooserOption, previousChooserOption = null) => {\n if (previousChooserOption !== null) {\n toggleFocusableChooserOption(previousChooserOption, false);\n }\n\n toggleFocusableChooserOption(currentChooserOption, true);\n currentChooserOption.focus();\n};\n\n/**\n * Add or remove a chooser option from the focus order.\n *\n * @method toggleFocusableChooserOption\n * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order\n * @param {Boolean} isFocusable Whether the chooser element is focusable or not\n */\nconst toggleFocusableChooserOption = (chooserOption, isFocusable) => {\n const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);\n const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);\n const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);\n\n if (isFocusable) {\n // Set tabindex to 0 to add current chooser option element to the focus order.\n chooserOption.tabIndex = 0;\n chooserOptionLink.tabIndex = 0;\n chooserOptionHelp.tabIndex = 0;\n chooserOptionFavourite.tabIndex = 0;\n } else {\n // Set tabindex to -1 to remove the previous chooser option element from the focus order.\n chooserOption.tabIndex = -1;\n chooserOptionLink.tabIndex = -1;\n chooserOptionHelp.tabIndex = -1;\n chooserOptionFavourite.tabIndex = -1;\n }\n};\n\n/**\n * Small error handling function to make sure the navigated to object exists\n *\n * @method clickErrorHandler\n * @param {HTMLElement} item What we want to check exists\n * @param {HTMLElement} fallback If we dont match anything fallback the focus\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Render the search results in a defined container\n *\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The container where the data should be rendered\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\nconst renderSearchResults = async(searchResultsContainer, searchResultsData) => {\n const templateData = {\n 'searchresultsnumber': searchResultsData.length,\n 'searchresults': searchResultsData\n };\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query\n *\n * @method toggleSearchResultsView\n * @param {Object} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {String} searchQuery The search query\n */\nconst toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {\n const modalBody = modal.getBody()[0];\n const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);\n const chooserContainer = modalBody.querySelector(selectors.regions.chooser);\n const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);\n\n if (searchQuery.length > 0) { // Search query is present.\n const searchResultsData = searchModules(mappedModules, searchQuery);\n await renderSearchResults(searchResultsContainer, searchResultsData);\n const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);\n const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);\n if (firstSearchResultItem) {\n // Set the first result item to be focusable.\n toggleFocusableChooserOption(firstSearchResultItem, true);\n // Register keyboard events on the created search result items.\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);\n }\n // Display the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.remove('d-none');\n // Hide the default chooser options container.\n chooserContainer.setAttribute('hidden', 'hidden');\n // Display the search results container.\n searchResultsContainer.removeAttribute('hidden');\n } else { // Search query is not present.\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n // Hide the search results container.\n searchResultsContainer.setAttribute('hidden', 'hidden');\n // Display the default chooser options container.\n chooserContainer.removeAttribute('hidden');\n }\n};\n\n/**\n * Return the list of modules which have a name or description that matches the given search term.\n *\n * @method searchModules\n * @param {Array} modules List of available modules\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchModules = (modules, searchTerm) => {\n if (searchTerm === '') {\n return modules;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n modules.forEach((activity) => {\n const activityName = activity.title.toLowerCase();\n const activityDesc = activity.help.toLowerCase();\n if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {\n searchResults.push(activity);\n }\n });\n\n return searchResults;\n};\n\n/**\n * Set up our tabindex information across the chooser.\n *\n * @method setupKeyboardAccessibility\n * @param {Promise} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the built module information\n */\nconst setupKeyboardAccessibility = (modal, mappedModules) => {\n modal.getModal()[0].tabIndex = -1;\n\n modal.getBodyPromise().then(body => {\n document.querySelectorAll(selectors.elements.tab).forEach((tab) => {\n tab.addEventListener('shown.bs.tab', (e) => {\n const activeSectionId = e.target.getAttribute(\"href\");\n const activeSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = activeSectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n const prevActiveSectionId = e.relatedTarget.getAttribute(\"href\");\n const prevActiveSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));\n\n // Disable the focus of every chooser option in the previous active section.\n disableFocusAllChooserOptions(prevActiveSectionChooserOptions);\n // Enable the focus of the first chooser option in the current active section.\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);\n });\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Disable the focus of all chooser options in a specific container (section).\n *\n * @method disableFocusAllChooserOptions\n * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items\n */\nconst disableFocusAllChooserOptions = (sectionChooserOptions) => {\n const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);\n allChooserOptions.forEach((chooserOption) => {\n toggleFocusableChooserOption(chooserOption, false);\n });\n};\n\n/**\n * Display the module chooser.\n *\n * @method displayChooser\n * @param {Promise} modalPromise Our created modal for the section\n * @param {Array} sectionModules An array of all of the built module information\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nexport const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {\n // Make a map so we can quickly fetch a specific module's object for either rendering or searching.\n const mappedModules = new Map();\n sectionModules.forEach((module) => {\n mappedModules.set(module.componentname + '_' + module.link, module);\n });\n\n // Register event listeners.\n modalPromise.then(modal => {\n registerListenerEvents(modal, mappedModules, partialFavourite, footerData);\n\n // We want to focus on the first chooser option element as soon as the modal is opened.\n setupKeyboardAccessibility(modal, mappedModules);\n\n // We want to focus on the action select when the dialog is closed.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n\n return modal;\n }).catch(Notification.exception);\n};\n"],"names":["showModuleHelp","carousel","moduleData","modal","showFooter","setFooter","Templates","render","help","querySelector","selectors","regions","innerHTML","classList","add","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","resolve","contentPromise","renderForPromise","all","then","_ref","html","js","replaceNodeContents","chooserSummary","header","focus","catch","Notification","exception","addEventListener","once","getInstance","next","registerListenerEvents","mappedModules","partialFavourite","footerData","modalBody","getBody","bodyClickListener","async","e","target","closest","actions","optionActions","showSummary","moduleName","chooserOption","container","dataset","modname","get","hasFooterContent","manageFavourite","caller","isFavourite","favourited","id","name","internal","Repository","unfavouriteModule","favouriteModule","manageFavouriteState","activeSectionId","elements","activetab","getAttribute","sectionChooserOptions","getSectionChooserOptions","firstChooserOption","toggleFocusableChooserOption","initChooserOptionsKeyboardNavigation","matches","closeOption","prev","modules","getModuleSelector","clearSearch","searchInput","search","value","toggleSearchResultsView","footerClickListener","footer","footerjs","pluginName","customfooterjs","getBodyPromise","body","document","Carousel","interval","pause","keyboard","getFooterPromise","chooserOptionsContainer","chooserOptions","querySelectorAll","Array","from","forEach","element","keyCode","enter","space","preventDefault","arrowRight","currentOption","nextOption","nextElementSibling","firstOption","firstElementChild","toFocusOption","clickErrorHandler","focusChooserOption","arrowLeft","previousOption","previousElementSibling","lastOption","lastElementChild","home","end","currentChooserOption","previousChooserOption","isFocusable","chooserOptionLink","addChooser","chooserOptionHelp","chooserOptionFavourite","tabIndex","item","fallback","searchQuery","searchResultsContainer","searchResults","chooserContainer","chooser","clearSearchButton","length","searchResultsData","searchModules","templateData","renderSearchResults","searchResultItemsContainer","searchResultItems","firstSearchResultItem","remove","setAttribute","removeAttribute","searchTerm","toLowerCase","activity","activityName","title","activityDesc","includes","push","disableFocusAllChooserOptions","modalPromise","sectionModules","Map","module","set","componentname","link","getModal","tab","activeSectionChooserOptions","prevActiveSectionId","relatedTarget","prevActiveSectionChooserOptions","setupKeyboardAccessibility","getRoot","on","ModalEvents","hidden","destroy"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"dialogue.min.js","sources":["../../../src/local/activitychooser/dialogue.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 * A type of dialogue used as for choosing options.\n *\n * @module core_course/local/activitychooser/dialogue\n * @copyright 2019 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// import {addIconToContainer} from 'core/loadingicon';\nimport {debounce} from 'core/utils';\nimport DialogueDom from 'core_course/local/activitychooser/dialoguedom';\nimport {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';\nimport Exporter from 'core_course/local/activitychooser/exporter';\nimport {getFirst} from 'core/normalise';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport * as ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport * as Templates from 'core/templates';\nconst getPlugin = pluginName => import(pluginName);\n\n/**\n * Display the activity chooser modal.\n *\n * @method displayActivityChooser\n * @param {Promise} footerDataPromise Promise for the footer data.\n * @param {Promise} modulesDataPromise Promise for the modules data.\n */\nexport async function displayActivityChooserModal(\n footerDataPromise,\n modulesDataPromise,\n) {\n // We want to show the modal instantly but loading whilst waiting for our data.\n let bodyPromiseResolver;\n const bodyPromise = new Promise(resolve => {\n bodyPromiseResolver = resolve;\n });\n\n const footerData = await footerDataPromise;\n\n const sectionModal = Modal.create({\n body: bodyPromise,\n title: getString('addresourceoractivity'),\n footer: footerData.customfootertemplate,\n large: true,\n scrollable: false,\n templateContext: {\n classes: 'modchooser'\n },\n show: true,\n });\n\n try {\n const modulesData = await modulesDataPromise;\n\n if (!modulesData) {\n return;\n }\n\n const modal = await sectionModal;\n const dialogue = new ActivityChooserDialogue(modal, modulesData, footerData);\n\n const templateData = await dialogue.exporter.getModChooserTemplateData(modulesData);\n bodyPromiseResolver(await Templates.render('core_course/activitychooser', templateData));\n } catch (error) {\n const errorTemplateData = {\n 'errormessage': error.message\n };\n bodyPromiseResolver(\n await Templates.render('core_course/local/activitychooser/error', errorTemplateData)\n );\n return;\n }\n}\n\n/**\n * Display the module chooser.\n *\n * @deprecated since Moodle 5.1\n * @todo Remove the method in Moodle 6.0 (MDL-85655).\n * @method displayChooser\n * @param {Promise} modalPromise Our created modal for the section\n * @param {Array} sectionModules An array of all of the built module information\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nexport const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {\n window.console.warn(\n 'The displayChooser function is deprecated. ' +\n 'Please displayActivityChooserModal instead.'\n );\n\n // Register event listeners.\n modalPromise.then(modal => {\n new ActivityChooserDialogue(modal, sectionModules, footerData);\n return modal;\n }).catch(Notification.exception);\n};\n\n/**\n * Activity Chooser Dialogue class.\n *\n * @private\n */\nclass ActivityChooserDialogue {\n /**\n * Constructor for the ActivityChooserDialogue class.\n * @param {Modal} modal The modal object.\n * @param {Object} modulesData The data for the modules.\n * @param {Object} footerData The data for the footer.\n */\n constructor(modal, modulesData, footerData) {\n this.modal = modal;\n this.dialogueDom = null; // We cannot init until we have the modal body loaded.\n this.footerData = footerData;\n this.exporter = new Exporter();\n // This attribute marks when the tab content is dirty and needs to be refreshed when the user changes the tab.\n // We don't want the content to be updated while the user is managing their favourites.\n this.isFavouriteTabDirty = false;\n // Make a map so we can quickly fetch a specific module's object for either rendering or searching.\n this.mappedModules = new Map();\n modulesData.forEach((module) => {\n this.mappedModules.set(module.componentname + '_' + module.link, module);\n });\n this.init();\n }\n\n /**\n * Initialise the activity chooser dialogue.\n *\n * @return {Promise} A promise that resolves when the modal is ready.\n */\n async init() {\n const modalBody = getFirst(await this.modal.getBodyPromise());\n this.dialogueDom = new DialogueDom(this, modalBody, this.exporter);\n this.registerModalListenerEvents();\n this.setupKeyboardAccessibility();\n // We want to focus on the action select when the dialog is closed.\n this.modal.getRoot().on(ModalEvents.hidden, () => {\n this.modal.destroy();\n });\n }\n\n /**\n * Register chooser related event listeners.\n *\n * @returns {Promise} A promise that resolves when events are registered\n */\n async registerModalListenerEvents() {\n const modalRoot = getFirst(this.modal.getRoot());\n\n // Changing the tab should cancel any active search.\n modalRoot.addEventListener(\n 'shown.bs.tab',\n (event) => {\n // The all tab has the search result, so we do not want to clear the search input.\n if (event.target.closest(selectors.regions.allTabNav)) {\n return;\n }\n const searchInput = this.dialogueDom.getSearchInputElement();\n if (searchInput.value.length > 0) {\n searchInput.value = \"\";\n this.toggleSearchResultsView(searchInput.value);\n }\n },\n );\n\n // Add the listener for clicks on the full modal.\n modalRoot.addEventListener(\n 'click',\n this.handleModalClick.bind(this),\n );\n\n // Add a listener for an input change in the activity chooser's search bar.\n const searchInput = this.dialogueDom.getSearchInputElement();\n searchInput.addEventListener(\n 'input',\n debounce(\n () => {\n this.toggleSearchResultsView(searchInput.value);\n },\n 300,\n {pending: true},\n ),\n );\n\n this.dialogueDom.initBootstrapComponents();\n\n // Handle focus when a new tab is shown.\n modalRoot.addEventListener('shown.bs.tab', (event) => {\n if (event.relatedTarget) {\n this.dialogueDom.disableFocusAllChooserOptions(event.relatedTarget);\n }\n this.dialogueDom.initActiveTabNavigation();\n });\n\n // Update the favourite tab content when the user changes the tab.\n modalRoot.addEventListener('shown.bs.tab', () => {\n if (this.isFavouriteTabDirty && !this.dialogueDom.isFavoutiteTabActive()) {\n this.refreshFavouritesTabContent();\n }\n });\n\n this.dialogueDom.initActiveTabNavigation();\n\n const modalFooter = getFirst(await this.modal.getFooterPromise());\n\n // Add the listener for clicks on the footer.\n modalFooter.addEventListener(\n 'click',\n this.handleFooterClick.bind(this),\n );\n }\n\n /**\n * Handle the click event on the footer of the modal.\n *\n * @param {Object} event The event object\n * @return {Promise} A promise that resolves when the event is handled\n */\n async handleFooterClick(event) {\n if (this.footerData.footer === true) {\n const footerjs = await getPlugin(this.footerData.customfooterjs);\n await footerjs.footerClickListener(event, this.footerData, this.modal);\n }\n }\n\n /**\n * Modal click handler.\n *\n * @param {Object} event The event object\n * @return {Promise} A promise that resolves when the event is handled\n */\n async handleModalClick(event) {\n const target = event.target;\n\n if (target.closest(selectors.actions.optionActions.showSummary)) {\n this.handleShowSummary(target);\n }\n\n if (target.closest(selectors.actions.optionActions.manageFavourite)) {\n await this.handleFavouriteClick(target);\n }\n\n // From the help screen go back to the module overview.\n if (target.matches(selectors.actions.closeOption)) {\n this.dialogueDom.hideModuleHelp(target.dataset.modname);\n }\n\n // The \"clear search\" button is triggered.\n if (target.closest(selectors.actions.clearSearch)) {\n this.handleClearSearch();\n }\n }\n\n /**\n * Show the summary of a module when the user clicks on the \"show summary\" button.\n *\n * @param {HTMLElement} target The target element that triggered the event\n */\n handleShowSummary(target) {\n const module = this.dialogueDom.getClosestChooserOption(target);\n const moduleName = module.dataset.modname;\n const moduleData = this.mappedModules.get(moduleName);\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = this.modal.hasFooterContent();\n this.dialogueDom.showModuleHelp(moduleData, this.modal);\n }\n\n /**\n * Handle the favourite state of a module when the user clicks on the \"starred\" button.\n *\n * @param {HTMLElement} target The target element that triggered the event\n * @return {Promise} A promise that resolves when the event is handled\n */\n async handleFavouriteClick(target) {\n const caller = target.closest(selectors.actions.optionActions.manageFavourite);\n const id = caller.dataset.id;\n const name = caller.dataset.name;\n const internal = caller.dataset.internal;\n const isFavourite = caller.dataset.favourited;\n\n // Switch on fave or not.\n if (isFavourite === 'true') {\n await Repository.unfavouriteModule(name, id);\n this.updateFavouriteItemValue(internal, false);\n } else {\n await Repository.favouriteModule(name, id);\n this.updateFavouriteItemValue(internal, true);\n }\n }\n\n /**\n * Handle a clear search action.\n */\n handleClearSearch() {\n const searchInput = this.dialogueDom.getSearchInputElement();\n searchInput.value = \"\";\n searchInput.focus();\n this.toggleSearchResultsView(searchInput.value);\n }\n\n /**\n * Set up our tabindex information across the chooser.\n *\n * @method setupKeyboardAccessibility\n */\n setupKeyboardAccessibility() {\n const mainElement = getFirst(this.modal.getModal());\n\n mainElement.tabIndex = -1;\n\n mainElement.addEventListener('keydown', (e) => {\n const currentOption = this.dialogueDom.getClosestChooserOption(e.target);\n if (currentOption === null) {\n return;\n }\n\n // Check for enter/ space triggers for showing the help.\n if (e.keyCode === enter || e.keyCode === space) {\n if (e.target.matches(selectors.actions.optionActions.showSummary)) {\n e.preventDefault();\n this.handleShowSummary(e.target);\n }\n }\n\n if (e.keyCode === arrowRight) {\n e.preventDefault();\n this.dialogueDom.focusNextChooserOption(currentOption);\n }\n if (e.keyCode === arrowLeft) {\n e.preventDefault();\n this.dialogueDom.focusPreviousChooserOption(currentOption);\n }\n if (e.keyCode === home) {\n e.preventDefault();\n this.dialogueDom.focusFirstChooserOption(currentOption);\n }\n if (e.keyCode === end) {\n e.preventDefault();\n this.dialogueDom.focusLastChooserOption(currentOption);\n }\n });\n }\n\n /**\n * Toggle (display/hide) the search results depending on the value of the search query\n *\n * @method toggleSearchResultsView\n * @param {String} searchQuery The search query\n */\n async toggleSearchResultsView(searchQuery) {\n const searchResultsData = this.searchModules(searchQuery);\n\n if (searchQuery.length > 0) {\n await this.dialogueDom.refreshSearchResults(searchResultsData);\n this.dialogueDom.showAllActivitiesTab(true);\n } else {\n this.dialogueDom.cleanSearchResults();\n }\n }\n\n /**\n * Return the list of modules which have a name or description that matches the given search term.\n *\n * @method searchModules\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\n searchModules(searchTerm) {\n if (searchTerm === '') {\n return this.mappedModules;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n this.mappedModules.forEach((activity) => {\n const activityName = activity.title.toLowerCase();\n const activityDesc = activity.help.toLowerCase();\n if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {\n searchResults.push(activity);\n }\n });\n\n return searchResults;\n }\n\n /**\n * Update the favourite item value in the mapped modules.\n *\n * @param {String} internal The internal name of the module.\n * @param {Boolean} favourite Whether the module is a favourite or not.\n * @return {Promise} A promise that resolves when the item is updated.\n */\n async updateFavouriteItemValue(internal, favourite) {\n const moduleItem = this.mappedModules.find(({name}) => name === internal);\n if (!moduleItem) {\n return;\n }\n moduleItem.favourite = favourite;\n\n this.dialogueDom.updateItemStarredIcons(internal, favourite);\n\n if (this.dialogueDom.isFavoutiteTabActive()) {\n this.isFavouriteTabDirty = true;\n } else {\n this.refreshFavouritesTabContent();\n }\n }\n\n /**\n * Refresh the favourites tab content.\n *\n * Note: this method will also hide the favourites tab if there are no favourite modules\n * to keep the modal consistent.\n *\n * @return {Promise} A promise that resolves when the content is refreshed.\n */\n async refreshFavouritesTabContent() {\n this.isFavouriteTabDirty = false;\n const favouriteCount = this.mappedModules.filter(mod => mod.favourite === true).size;\n this.dialogueDom.toggleFavouriteTabDisplay(favouriteCount > 0);\n await this.dialogueDom.refreshFavouritesTabContent(this.mappedModules);\n }\n}\n"],"names":["footerDataPromise","modulesDataPromise","bodyPromiseResolver","bodyPromise","Promise","resolve","footerData","sectionModal","Modal","create","body","title","footer","customfootertemplate","large","scrollable","templateContext","classes","show","modulesData","modal","dialogue","ActivityChooserDialogue","templateData","exporter","getModChooserTemplateData","Templates","render","error","errorTemplateData","message","modalPromise","sectionModules","partialFavourite","window","console","warn","then","catch","Notification","exception","constructor","dialogueDom","Exporter","isFavouriteTabDirty","mappedModules","Map","forEach","module","set","componentname","link","init","modalBody","this","getBodyPromise","DialogueDom","registerModalListenerEvents","setupKeyboardAccessibility","getRoot","on","ModalEvents","hidden","destroy","modalRoot","addEventListener","event","target","closest","selectors","regions","allTabNav","searchInput","getSearchInputElement","value","length","toggleSearchResultsView","handleModalClick","bind","pending","initBootstrapComponents","relatedTarget","disableFocusAllChooserOptions","initActiveTabNavigation","isFavoutiteTabActive","refreshFavouritesTabContent","getFooterPromise","handleFooterClick","footerjs","pluginName","customfooterjs","footerClickListener","actions","optionActions","showSummary","handleShowSummary","manageFavourite","handleFavouriteClick","matches","closeOption","hideModuleHelp","dataset","modname","clearSearch","handleClearSearch","moduleName","getClosestChooserOption","moduleData","get","showFooter","hasFooterContent","showModuleHelp","caller","id","name","internal","favourited","Repository","unfavouriteModule","updateFavouriteItemValue","favouriteModule","focus","mainElement","getModal","tabIndex","e","currentOption","keyCode","enter","space","preventDefault","arrowRight","focusNextChooserOption","arrowLeft","focusPreviousChooserOption","home","focusFirstChooserOption","end","focusLastChooserOption","searchQuery","searchResultsData","searchModules","refreshSearchResults","showAllActivitiesTab","cleanSearchResults","searchTerm","toLowerCase","searchResults","activity","activityName","activityDesc","help","includes","push","favourite","moduleItem","find","_ref","updateItemStarredIcons","favouriteCount","filter","mod","size","toggleFavouriteTabDisplay"],"mappings":"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"} \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/dialoguedom.min.js b/course/amd/build/local/activitychooser/dialoguedom.min.js new file mode 100644 index 00000000000..cae5d721c7d --- /dev/null +++ b/course/amd/build/local/activitychooser/dialoguedom.min.js @@ -0,0 +1,10 @@ +define("core_course/local/activitychooser/dialoguedom",["exports","core/loadingicon","theme_boost/bootstrap/carousel","core/notification","core/pending","core_course/local/activitychooser/selectors","theme_boost/bootstrap/tab","core/templates"],(function(_exports,_loadingicon,_carousel,_notification,_pending,_selectors,_tab,Templates){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * The activity changer dialogue DOM manipulation module. + * + * @module core_course/local/activitychooser/dialoguedom + * @copyright 2025 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_carousel=_interopRequireDefault(_carousel),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_tab=_interopRequireDefault(_tab),Templates=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Templates);return _exports.default=class{constructor(dialogue,modalBody,exporter){this.modalBody=modalBody,this.exporter=exporter,this.dialogue=dialogue}getSearchInputElement(){return this.modalBody.querySelector(_selectors.default.actions.search)}getClosestChooserOption(element){return element.closest(_selectors.default.regions.chooserOption.container)}isFavoutiteTabActive(){const favouriteTab=this.modalBody.querySelector(_selectors.default.regions.favouriteTabNav);return favouriteTab&&favouriteTab.classList.contains("active")}async refreshSearchResults(searchResultsData){const searchResultsContainer=this.modalBody.querySelector(_selectors.default.regions.searchResults),clearSearchButton=this.modalBody.querySelector(_selectors.default.actions.clearSearch);await this.renderSearchResults(searchResultsContainer,searchResultsData);const firstSearchResultItem=searchResultsContainer.querySelector(_selectors.default.regions.chooserOptions).querySelector(_selectors.default.regions.chooserOption.container);firstSearchResultItem&&this.toggleFocusableChooserOption(firstSearchResultItem,!0),clearSearchButton.classList.remove("d-none");searchResultsContainer.closest(_selectors.default.regions.tabContent).querySelector(_selectors.default.regions.categoryContent).classList.add("d-none")}cleanSearchResults(){const searchResultsContainer=this.modalBody.querySelector(_selectors.default.regions.searchResults),clearSearchButton=this.modalBody.querySelector(_selectors.default.actions.clearSearch);searchResultsContainer.innerHTML="",clearSearchButton.classList.add("d-none");searchResultsContainer.closest(_selectors.default.regions.tabContent).querySelector(_selectors.default.regions.categoryContent).classList.remove("d-none")}async renderSearchResults(searchResultsContainer,searchResultsData){const templateData=this.exporter.getSearchResultData(searchResultsData),{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/search_results",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)}showAllActivitiesTab(){const navTab=this.modalBody.querySelector(_selectors.default.regions.allTabNav);if(navTab.classList.contains("active"))return navTab;const pendingPromise=new _pending.default("core_course/activitychooser:alltab");return navTab.addEventListener("shown.bs.tab",pendingPromise.resolve,{once:!0}),_tab.default.getOrCreateInstance(navTab).show(),navTab}updateItemStarredIcons(internal,favourite){const favouriteButtons=this.modalBody.querySelectorAll("".concat(_selectors.default.elements.moduleItem(internal)," ").concat(_selectors.default.actions.optionActions.manageFavourite));Array.from(favouriteButtons).forEach((element=>{var _element$querySelecto,_element$querySelecto2;element.classList.toggle("text-muted",!favourite),element.classList.toggle("text-primary",favourite),element.dataset.favourited=favourite,element.setAttribute("aria-pressed",favourite),null===(_element$querySelecto=element.querySelector(_selectors.default.elements.favouriteIconActive))||void 0===_element$querySelecto||_element$querySelecto.classList.toggle("d-none",!favourite),null===(_element$querySelecto2=element.querySelector(_selectors.default.elements.favouriteIconInactive))||void 0===_element$querySelecto2||_element$querySelecto2.classList.toggle("d-none",favourite);const iconSelectsor=favourite?_selectors.default.elements.favouriteIconActive:_selectors.default.elements.favouriteIconInactive,favouriteIcon=element.querySelector(iconSelectsor);element.setAttribute("aria-label",(null==favouriteIcon?void 0:favouriteIcon.getAttribute("data-action-label"))||"")}))}async refreshFavouritesTabContent(mappedModules){const templateData=await this.exporter.getFavouriteTabData(mappedModules),favouriteArea=this.modalBody.querySelector(_selectors.default.regions.favouriteTab),{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/tabcontent",templateData);await Templates.replaceNodeContents(favouriteArea,html,js)}toggleFavouriteTabDisplay(displayed){const favouriteTabNav=this.modalBody.querySelector(_selectors.default.regions.favouriteTabNav);let moveFocusTo;!displayed&&favouriteTabNav.classList.contains("active")&&(moveFocusTo=this.showAllActivitiesTab()),null==favouriteTabNav||favouriteTabNav.classList.toggle("d-none",!displayed),favouriteTabNav.tabIndex=displayed?0:-1,displayed?favouriteTabNav.classList.remove("disabled"):favouriteTabNav.classList.add("disabled"),moveFocusTo&&moveFocusTo.focus(),this.initActiveTabNavigation()}showModuleHelp(moduleData,modal){const carousel=this.modalBody.querySelector(_selectors.default.regions.carousel);!0===moduleData.showFooter&&modal.setFooter(Templates.render("core_course/local/activitychooser/footer_partial",moduleData));const help=carousel.querySelector(_selectors.default.regions.help);help.innerHTML="",help.classList.add("m-auto");const spinnerPromise=(0,_loadingicon.addIconToContainer)(help);let transitionPromiseResolver=null;const transitionPromise=new Promise((resolve=>{transitionPromiseResolver=resolve})),contentPromise=Templates.renderForPromise("core_course/local/activitychooser/help",moduleData);Promise.all([contentPromise,spinnerPromise,transitionPromise]).then((_ref=>{let[{html:html,js:js}]=_ref;return Templates.replaceNodeContents(help,html,js)})).then((()=>(help.querySelector(_selectors.default.regions.chooserSummary.header).focus(),help))).catch(_notification.default.exception),carousel.addEventListener("slid.bs.carousel",(()=>{transitionPromiseResolver()}),{once:!0}),_carousel.default.getInstance(carousel).next()}hideModuleHelp(){let internal=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;const carousel=this.modalBody.querySelector(_selectors.default.regions.carousel);_carousel.default.getInstance(carousel).prev(),null!==internal&&carousel.addEventListener("slid.bs.carousel",(()=>{this.focusChooserOption(internal)}),{once:!0})}focusChooserOption(internal){const currentTabNav=this.modalBody.querySelector(_selectors.default.elements.activetab),activeSectionId=currentTabNav.getAttribute("href"),newCurrent=this.modalBody.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)).querySelector(_selectors.default.regions.getModuleSelector(internal));if(!newCurrent)throw new Error("Invalid chooser option to focus on: ".concat(internal));this.disableFocusAllChooserOptions(currentTabNav),this.toggleFocusableChooserOption(newCurrent,!0),newCurrent.contentEditable=!0,newCurrent.focus(),newCurrent.contentEditable=!1}initActiveTabNavigation(){const activeSectionId=this.modalBody.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=this.modalBody.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=null==sectionChooserOptions?void 0:sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);firstChooserOption&&this.toggleFocusableChooserOption(firstChooserOption,!0)}initBootstrapComponents(){this.modalBody.querySelectorAll(_selectors.default.elements.tab).forEach((navTab=>{_tab.default.getOrCreateInstance(navTab)}));const carousel=this.modalBody.querySelector(_selectors.default.regions.carousel);new _carousel.default(carousel,{interval:!1,pause:!0,keyboard:!1})}disableFocusAllChooserOptions(tabNav){const tabId=tabNav.getAttribute("href"),chooserOptions=this.modalBody.querySelector(_selectors.default.regions.getSectionChooserOptions(tabId));if(null===chooserOptions)return;chooserOptions.querySelectorAll(_selectors.default.regions.chooserOption.container).forEach((chooserOption=>{this.toggleFocusableChooserOption(chooserOption,!1)}))}toggleFocusableChooserOption(chooserOption,isFocusable){const chooserOptionLink=chooserOption.querySelector(_selectors.default.actions.addChooser),chooserOptionHelp=chooserOption.querySelector(_selectors.default.actions.optionActions.showSummary),chooserOptionFavourite=chooserOption.querySelector(_selectors.default.actions.optionActions.manageFavourite);isFocusable?(chooserOption.tabIndex=0,chooserOptionLink.tabIndex=0,chooserOptionHelp.tabIndex=0,chooserOptionFavourite.tabIndex=0):(chooserOption.tabIndex=-1,chooserOptionLink.tabIndex=-1,chooserOptionHelp.tabIndex=-1,chooserOptionFavourite.tabIndex=-1)}focusNextChooserOption(current){this.moveChooserOptionFocus(current,(currentOption=>{var _currentOption$nextEl;return null!==(_currentOption$nextEl=currentOption.nextElementSibling)&&void 0!==_currentOption$nextEl?_currentOption$nextEl:currentOption}))}focusPreviousChooserOption(current){this.moveChooserOptionFocus(current,(currentOption=>{var _currentOption$previo;return null!==(_currentOption$previo=currentOption.previousElementSibling)&&void 0!==_currentOption$previo?_currentOption$previo:currentOption}))}focusFirstChooserOption(current){this.moveChooserOptionFocus(current,((currentOption,container)=>{var _container$firstEleme;return null!==(_container$firstEleme=container.firstElementChild)&&void 0!==_container$firstEleme?_container$firstEleme:currentOption}))}focusLastChooserOption(current){this.moveChooserOptionFocus(current,((currentOption,container)=>{var _container$lastElemen;return null!==(_container$lastElemen=container.lastElementChild)&&void 0!==_container$lastElemen?_container$lastElemen:currentOption}))}moveChooserOptionFocus(current,getNextFocus){const currentOption=this.getClosestChooserOption(current),container=current.closest(_selectors.default.regions.chooserOptions);if(!container||!currentOption)throw new Error("Invalid chooser options container or current option");const newFocusOption=getNextFocus(currentOption,container);newFocusOption&&(this.toggleFocusableChooserOption(currentOption,!1),this.toggleFocusableChooserOption(newFocusOption,!0),newFocusOption.focus())}},_exports.default})); + +//# sourceMappingURL=dialoguedom.min.js.map \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/dialoguedom.min.js.map b/course/amd/build/local/activitychooser/dialoguedom.min.js.map new file mode 100644 index 00000000000..6342ae8214a --- /dev/null +++ b/course/amd/build/local/activitychooser/dialoguedom.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"dialoguedom.min.js","sources":["../../../src/local/activitychooser/dialoguedom.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\nimport {addIconToContainer} from 'core/loadingicon';\nimport Carousel from 'theme_boost/bootstrap/carousel';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport Tab from 'theme_boost/bootstrap/tab';\nimport * as Templates from 'core/templates';\n\n\n/**\n * The activity changer dialogue DOM manipulation module.\n *\n * @module core_course/local/activitychooser/dialoguedom\n * @copyright 2025 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class ChooserDialogueDOM {\n\n constructor(dialogue, modalBody, exporter) {\n this.modalBody = modalBody;\n this.exporter = exporter;\n // Temporal variable while migrating methods.\n this.dialogue = dialogue;\n }\n\n /**\n * Get the search input element.\n *\n * @return {HTMLElement} The search input element.\n */\n getSearchInputElement() {\n return this.modalBody.querySelector(selectors.actions.search);\n }\n\n /**\n * Get the closest chooser option element.\n *\n * @param {HTMLElement} element\n * @return {HTMLElement|null} element\n */\n getClosestChooserOption(element) {\n return element.closest(selectors.regions.chooserOption.container);\n }\n\n /**\n * Check if the search tab is active.\n *\n * @return {Boolean} True if the search tab is active, false otherwise.\n */\n isFavoutiteTabActive() {\n const favouriteTab = this.modalBody.querySelector(selectors.regions.favouriteTabNav);\n return favouriteTab && favouriteTab.classList.contains('active');\n }\n\n /**\n * Show the search results.\n *\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\n async refreshSearchResults(searchResultsData) {\n const searchResultsContainer = this.modalBody.querySelector(selectors.regions.searchResults);\n const clearSearchButton = this.modalBody.querySelector(selectors.actions.clearSearch);\n\n await this.renderSearchResults(searchResultsContainer, searchResultsData);\n const chooserOptionsContainer = searchResultsContainer.querySelector(selectors.regions.chooserOptions);\n const firstSearchResultItem = chooserOptionsContainer.querySelector(selectors.regions.chooserOption.container);\n if (firstSearchResultItem) {\n // Set the first result item to be focusable.\n this.toggleFocusableChooserOption(firstSearchResultItem, true);\n // Register keyboard events on the created search result items.\n }\n clearSearchButton.classList.remove('d-none');\n\n // Results are rendered in the all activities tab, so we need to hide the category content.\n const tabContent = searchResultsContainer.closest(selectors.regions.tabContent);\n const categoryContent = tabContent.querySelector(selectors.regions.categoryContent);\n categoryContent.classList.add('d-none');\n }\n\n /**\n * Clear the search results.\n */\n cleanSearchResults() {\n const searchResultsContainer = this.modalBody.querySelector(selectors.regions.searchResults);\n const clearSearchButton = this.modalBody.querySelector(selectors.actions.clearSearch);\n searchResultsContainer.innerHTML = '';\n clearSearchButton.classList.add('d-none');\n\n // Results are rendered in the all activities tab, so we need to show the category content again.\n const tabContent = searchResultsContainer.closest(selectors.regions.tabContent);\n const categoryContent = tabContent.querySelector(selectors.regions.categoryContent);\n categoryContent.classList.remove('d-none');\n }\n\n /**\n * Render the search results in a defined container\n *\n * @private\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The container where the data should be rendered\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\n async renderSearchResults(searchResultsContainer, searchResultsData) {\n const templateData = this.exporter.getSearchResultData(searchResultsData);\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise(\n 'core_course/local/activitychooser/search_results',\n templateData\n );\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n }\n\n /**\n * Show the \"All activities\" tab.\n *\n * @method showAllActivitiesTab\n * @return {HTMLElement} The \"All activities\" tab element.\n */\n showAllActivitiesTab() {\n const navTab = this.modalBody.querySelector(selectors.regions.allTabNav);\n\n if (navTab.classList.contains('active')) {\n return navTab;\n }\n\n const pendingPromise = new Pending('core_course/activitychooser:alltab');\n navTab.addEventListener('shown.bs.tab', pendingPromise.resolve, {once: true});\n\n Tab.getOrCreateInstance(navTab).show();\n return navTab;\n }\n\n /**\n * Update the starred icons in the chooser modal.\n *\n * @method updateItemStarredIcons\n * @param {String} internal The internal name of the module.\n * @param {Boolean} favourite Whether the module is a favourite or not.\n */\n updateItemStarredIcons(internal, favourite) {\n const favouriteButtons = this.modalBody.querySelectorAll(\n `${selectors.elements.moduleItem(internal)} ${selectors.actions.optionActions.manageFavourite}`\n );\n Array.from(favouriteButtons).forEach((element) => {\n element.classList.toggle('text-muted', !favourite);\n element.classList.toggle('text-primary', favourite);\n element.dataset.favourited = favourite;\n element.setAttribute('aria-pressed', favourite);\n element.querySelector(selectors.elements.favouriteIconActive)?.classList.toggle('d-none', !favourite);\n element.querySelector(selectors.elements.favouriteIconInactive)?.classList.toggle('d-none', favourite);\n\n const iconSelectsor = favourite ? selectors.elements.favouriteIconActive : selectors.elements.favouriteIconInactive;\n const favouriteIcon = element.querySelector(iconSelectsor);\n element.setAttribute('aria-label', favouriteIcon?.getAttribute('data-action-label') || '');\n });\n }\n\n /**\n * Refresh the favourite content.\n *\n * @param {Array} mappedModules The modules to be displayed in the favourite tab.\n */\n async refreshFavouritesTabContent(mappedModules) {\n const templateData = await this.exporter.getFavouriteTabData(mappedModules);\n const favouriteArea = this.modalBody.querySelector(selectors.regions.favouriteTab);\n const {html, js} = await Templates.renderForPromise(\n 'core_course/local/activitychooser/tabcontent',\n templateData,\n );\n await Templates.replaceNodeContents(favouriteArea, html, js);\n }\n\n /**\n * Toggle the display of the favourite tab.\n *\n * The favourite tab is only displayed when there are favourite modules\n * or when it is the active tab.\n *\n * @param {Boolean} displayed Whether we want to show or hide the favourite tab\n */\n toggleFavouriteTabDisplay(displayed) {\n const favouriteTabNav = this.modalBody.querySelector(selectors.regions.favouriteTabNav);\n\n let moveFocusTo;\n if (!displayed && favouriteTabNav.classList.contains('active')) {\n moveFocusTo = this.showAllActivitiesTab();\n }\n\n favouriteTabNav?.classList.toggle('d-none', !displayed);\n favouriteTabNav.tabIndex = displayed ? 0 : -1;\n // The disabled class is used by Boostrap Tab for keyboard navigation.\n if (displayed) {\n favouriteTabNav.classList.remove('disabled');\n } else {\n favouriteTabNav.classList.add('disabled');\n }\n\n if (moveFocusTo) {\n moveFocusTo.focus();\n }\n this.initActiveTabNavigation();\n }\n\n /**\n * Given an event from the main module 'page' navigate to it's help section via a carousel.\n *\n * @method showModuleHelp\n * @param {Object} moduleData Data of the module to carousel to\n * @param {Modal} modal The modal object\n */\n showModuleHelp(moduleData, modal) {\n const carousel = this.modalBody.querySelector(selectors.regions.carousel);\n // If we have a real footer then we need to change temporarily.\n if (moduleData.showFooter === true) {\n modal.setFooter(Templates.render(\n 'core_course/local/activitychooser/footer_partial',\n moduleData\n ));\n }\n const help = carousel.querySelector(selectors.regions.help);\n help.innerHTML = '';\n help.classList.add('m-auto');\n\n // Add a spinner.\n const spinnerPromise = addIconToContainer(help);\n\n // Used later...\n let transitionPromiseResolver = null;\n const transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n // Build up the html & js ready to place into the help section.\n const contentPromise = Templates.renderForPromise(\n 'core_course/local/activitychooser/help',\n moduleData\n );\n\n // Wait for the content to be ready, and for the transition to be complet.\n Promise.all([contentPromise, spinnerPromise, transitionPromise])\n .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))\n .then(() => {\n help.querySelector(selectors.regions.chooserSummary.header).focus();\n return help;\n })\n .catch(Notification.exception);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.addEventListener(\n 'slid.bs.carousel',\n () => {\n transitionPromiseResolver();\n },\n {once: true}\n );\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).next();\n }\n\n /**\n * Hide the help section of the chooser.\n *\n * @param {String|null} internal The internal name of the module to return to, if any.\n */\n hideModuleHelp(internal = null) {\n const carousel = this.modalBody.querySelector(selectors.regions.carousel);\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).prev();\n if (internal === null) {\n return;\n }\n carousel.addEventListener(\n 'slid.bs.carousel',\n () => {\n this.focusChooserOption(internal);\n },\n {once: true}\n );\n }\n\n /**\n * Focus on a specific activity inside the active tab (if present).\n *\n * @private\n * @method focusChooserOption\n * @param {String} internal The internal name of the module.\n */\n focusChooserOption(internal) {\n const currentTabNav = this.modalBody.querySelector(selectors.elements.activetab);\n const activeSectionId = currentTabNav.getAttribute(\"href\");\n const sectionChooserOptions = this.modalBody.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const newCurrent = sectionChooserOptions.querySelector(selectors.regions.getModuleSelector(internal));\n\n if (!newCurrent) {\n throw new Error(`Invalid chooser option to focus on: ${internal}`);\n }\n\n // Chooser can only have one element focusable at a time, so we disable them all first.\n this.disableFocusAllChooserOptions(currentTabNav);\n this.toggleFocusableChooserOption(newCurrent, true);\n\n // Little hack: we want the element considered a focus-visible element.\n // But the focus method does not trigger the focus-visible class. There's an\n // experimental \"{focusVisible: true}\" option in the focus method, but it's not\n // supported in all browsers yet so we need to fake an editable element.\n newCurrent.contentEditable = true;\n newCurrent.focus();\n newCurrent.contentEditable = false;\n }\n\n /**\n * Initialise the active tab navigation.\n */\n initActiveTabNavigation() {\n const activeSectionId = this.modalBody.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = this.modalBody.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions?.querySelector(selectors.regions.chooserOption.container);\n if (!firstChooserOption) {\n return;\n }\n this.toggleFocusableChooserOption(firstChooserOption, true);\n }\n\n /**\n * Initialise all Boostrap components.\n */\n initBootstrapComponents() {\n this.modalBody.querySelectorAll(selectors.elements.tab).forEach((navTab) => {\n // Init the Bootstrap Tab navigation.\n Tab.getOrCreateInstance(navTab);\n });\n\n // Set up the carousel.\n const carousel = this.modalBody.querySelector(selectors.regions.carousel);\n new Carousel(carousel, {\n interval: false,\n pause: true,\n keyboard: false\n });\n }\n\n /**\n * Disable the focus of all chooser options in a specific container (section).\n *\n * @method disableFocusAllChooserOptions\n * @param {HTMLElement} tabNav The tab navigation element (from the shown.bs.ta event).\n */\n disableFocusAllChooserOptions(tabNav) {\n const tabId = tabNav.getAttribute(\"href\");\n const chooserOptions = this.modalBody.querySelector(\n selectors.regions.getSectionChooserOptions(tabId)\n );\n\n if (chooserOptions === null) {\n return;\n }\n\n const allChooserOptions = chooserOptions.querySelectorAll(selectors.regions.chooserOption.container);\n allChooserOptions.forEach((chooserOption) => {\n this.toggleFocusableChooserOption(chooserOption, false);\n });\n }\n\n /**\n * Add or remove a chooser option from the focus order.\n *\n * @private\n * @method toggleFocusableChooserOption\n * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order\n * @param {Boolean} isFocusable Whether the chooser element is focusable or not\n */\n toggleFocusableChooserOption(chooserOption, isFocusable) {\n const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);\n const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);\n const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);\n\n if (isFocusable) {\n // Set tabindex to 0 to add current chooser option element to the focus order.\n chooserOption.tabIndex = 0;\n chooserOptionLink.tabIndex = 0;\n chooserOptionHelp.tabIndex = 0;\n chooserOptionFavourite.tabIndex = 0;\n } else {\n // Set tabindex to -1 to remove the previous chooser option element from the focus order.\n chooserOption.tabIndex = -1;\n chooserOptionLink.tabIndex = -1;\n chooserOptionHelp.tabIndex = -1;\n chooserOptionFavourite.tabIndex = -1;\n }\n }\n\n /**\n * Move the focus to the previous chooser option element.\n *\n * @param {HTMLElement} current The current chooser option element\n */\n focusNextChooserOption(current) {\n this.moveChooserOptionFocus(\n current,\n (currentOption) => currentOption.nextElementSibling ?? currentOption,\n );\n }\n\n /**\n * Move the focus to the previous chooser option element.\n *\n * @param {HTMLElement} current The current chooser option element\n */\n focusPreviousChooserOption(current) {\n this.moveChooserOptionFocus(\n current,\n (currentOption) => currentOption.previousElementSibling ?? currentOption,\n );\n }\n\n /**\n * Move the focus to the first chooser option element.\n *\n * @param {HTMLElement} current The current chooser option element\n */\n focusFirstChooserOption(current) {\n this.moveChooserOptionFocus(\n current,\n (currentOption, container) => container.firstElementChild ?? currentOption,\n );\n }\n\n /**\n * Move the focus to the last chooser option element.\n *\n * @param {HTMLElement} current The current chooser option element\n */\n focusLastChooserOption(current) {\n this.moveChooserOptionFocus(\n current,\n (currentOption, container) => container.lastElementChild ?? currentOption,\n );\n }\n\n /**\n * Move the focus to the next chooser option element.\n *\n * @private\n * @param {HTMLElement} current The current chooser option element\n * @param {Function} getNextFocus Function to get the next focusable element\n */\n moveChooserOptionFocus(current, getNextFocus) {\n const currentOption = this.getClosestChooserOption(current);\n const container = current.closest(selectors.regions.chooserOptions);\n\n if (!container || !currentOption) {\n throw new Error('Invalid chooser options container or current option');\n }\n\n const newFocusOption = getNextFocus(currentOption, container);\n if (!newFocusOption) {\n return;\n }\n\n this.toggleFocusableChooserOption(currentOption, false);\n this.toggleFocusableChooserOption(newFocusOption, true);\n newFocusOption.focus();\n }\n}\n"],"names":["constructor","dialogue","modalBody","exporter","getSearchInputElement","this","querySelector","selectors","actions","search","getClosestChooserOption","element","closest","regions","chooserOption","container","isFavoutiteTabActive","favouriteTab","favouriteTabNav","classList","contains","searchResultsData","searchResultsContainer","searchResults","clearSearchButton","clearSearch","renderSearchResults","firstSearchResultItem","chooserOptions","toggleFocusableChooserOption","remove","tabContent","categoryContent","add","cleanSearchResults","innerHTML","templateData","getSearchResultData","html","js","Templates","renderForPromise","replaceNodeContents","showAllActivitiesTab","navTab","allTabNav","pendingPromise","Pending","addEventListener","resolve","once","getOrCreateInstance","show","updateItemStarredIcons","internal","favourite","favouriteButtons","querySelectorAll","elements","moduleItem","optionActions","manageFavourite","Array","from","forEach","toggle","dataset","favourited","setAttribute","favouriteIconActive","favouriteIconInactive","iconSelectsor","favouriteIcon","getAttribute","mappedModules","getFavouriteTabData","favouriteArea","toggleFavouriteTabDisplay","displayed","moveFocusTo","tabIndex","focus","initActiveTabNavigation","showModuleHelp","moduleData","modal","carousel","showFooter","setFooter","render","help","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","contentPromise","all","then","_ref","chooserSummary","header","catch","Notification","exception","getInstance","next","hideModuleHelp","prev","focusChooserOption","currentTabNav","activetab","activeSectionId","newCurrent","getSectionChooserOptions","getModuleSelector","Error","disableFocusAllChooserOptions","contentEditable","sectionChooserOptions","firstChooserOption","initBootstrapComponents","tab","Carousel","interval","pause","keyboard","tabNav","tabId","isFocusable","chooserOptionLink","addChooser","chooserOptionHelp","showSummary","chooserOptionFavourite","focusNextChooserOption","current","moveChooserOptionFocus","currentOption","nextElementSibling","focusPreviousChooserOption","previousElementSibling","focusFirstChooserOption","firstElementChild","focusLastChooserOption","lastElementChild","getNextFocus","newFocusOption"],"mappings":";;;;;;;++BAiCIA,YAAYC,SAAUC,UAAWC,eACxBD,UAAYA,eACZC,SAAWA,cAEXF,SAAWA,SAQpBG,+BACWC,KAAKH,UAAUI,cAAcC,mBAAUC,QAAQC,QAS1DC,wBAAwBC,gBACbA,QAAQC,QAAQL,mBAAUM,QAAQC,cAAcC,WAQ3DC,6BACUC,aAAeZ,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQK,wBAC7DD,cAAgBA,aAAaE,UAAUC,SAAS,qCAQhCC,yBACjBC,uBAAyBjB,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQU,eACxEC,kBAAoBnB,KAAKH,UAAUI,cAAcC,mBAAUC,QAAQiB,mBAEnEpB,KAAKqB,oBAAoBJ,uBAAwBD,yBAEjDM,sBAD0BL,uBAAuBhB,cAAcC,mBAAUM,QAAQe,gBACjCtB,cAAcC,mBAAUM,QAAQC,cAAcC,WAChGY,4BAEKE,6BAA6BF,uBAAuB,GAG7DH,kBAAkBL,UAAUW,OAAO,UAGhBR,uBAAuBV,QAAQL,mBAAUM,QAAQkB,YACjCzB,cAAcC,mBAAUM,QAAQmB,iBACnDb,UAAUc,IAAI,UAMlCC,2BACUZ,uBAAyBjB,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQU,eACxEC,kBAAoBnB,KAAKH,UAAUI,cAAcC,mBAAUC,QAAQiB,aACzEH,uBAAuBa,UAAY,GACnCX,kBAAkBL,UAAUc,IAAI,UAGbX,uBAAuBV,QAAQL,mBAAUM,QAAQkB,YACjCzB,cAAcC,mBAAUM,QAAQmB,iBACnDb,UAAUW,OAAO,oCAWXR,uBAAwBD,yBACxCe,aAAe/B,KAAKF,SAASkC,oBAAoBhB,oBAEjDiB,KAACA,KAADC,GAAOA,UAAYC,UAAUC,iBAC/B,mDACAL,oBAEEI,UAAUE,oBAAoBpB,uBAAwBgB,KAAMC,IAStEI,6BACUC,OAASvC,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQgC,cAE1DD,OAAOzB,UAAUC,SAAS,iBACnBwB,aAGLE,eAAiB,IAAIC,iBAAQ,6CACnCH,OAAOI,iBAAiB,eAAgBF,eAAeG,QAAS,CAACC,MAAM,iBAEnEC,oBAAoBP,QAAQQ,OACzBR,OAUXS,uBAAuBC,SAAUC,iBACvBC,iBAAmBnD,KAAKH,UAAUuD,2BACjClD,mBAAUmD,SAASC,WAAWL,sBAAa/C,mBAAUC,QAAQoD,cAAcC,kBAElFC,MAAMC,KAAKP,kBAAkBQ,SAASrD,2DAClCA,QAAQQ,UAAU8C,OAAO,cAAeV,WACxC5C,QAAQQ,UAAU8C,OAAO,eAAgBV,WACzC5C,QAAQuD,QAAQC,WAAaZ,UAC7B5C,QAAQyD,aAAa,eAAgBb,yCACrC5C,QAAQL,cAAcC,mBAAUmD,SAASW,6EAAsBlD,UAAU8C,OAAO,UAAWV,0CAC3F5C,QAAQL,cAAcC,mBAAUmD,SAASY,iFAAwBnD,UAAU8C,OAAO,SAAUV,iBAEtFgB,cAAgBhB,UAAYhD,mBAAUmD,SAASW,oBAAsB9D,mBAAUmD,SAASY,sBACxFE,cAAgB7D,QAAQL,cAAciE,eAC5C5D,QAAQyD,aAAa,cAAcI,MAAAA,qBAAAA,cAAeC,aAAa,uBAAwB,yCAS7DC,qBACxBtC,mBAAqB/B,KAAKF,SAASwE,oBAAoBD,eACvDE,cAAgBvE,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQI,eAC/DqB,KAACA,KAADC,GAAOA,UAAYC,UAAUC,iBAC/B,+CACAL,oBAEEI,UAAUE,oBAAoBkC,cAAetC,KAAMC,IAW7DsC,0BAA0BC,iBAChB5D,gBAAkBb,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQK,qBAEnE6D,aACCD,WAAa5D,gBAAgBC,UAAUC,SAAS,YACjD2D,YAAc1E,KAAKsC,wBAGvBzB,MAAAA,iBAAAA,gBAAiBC,UAAU8C,OAAO,UAAWa,WAC7C5D,gBAAgB8D,SAAWF,UAAY,GAAK,EAExCA,UACA5D,gBAAgBC,UAAUW,OAAO,YAEjCZ,gBAAgBC,UAAUc,IAAI,YAG9B8C,aACAA,YAAYE,aAEXC,0BAUTC,eAAeC,WAAYC,aACjBC,SAAWjF,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQyE,WAElC,IAA1BF,WAAWG,YACXF,MAAMG,UAAUhD,UAAUiD,OACtB,mDACAL,mBAGFM,KAAOJ,SAAShF,cAAcC,mBAAUM,QAAQ6E,MACtDA,KAAKvD,UAAY,GACjBuD,KAAKvE,UAAUc,IAAI,gBAGb0D,gBAAiB,mCAAmBD,UAGtCE,0BAA4B,WAC1BC,kBAAoB,IAAIC,SAAQ7C,UAClC2C,0BAA4B3C,WAI1B8C,eAAiBvD,UAAUC,iBAC7B,yCACA2C,YAIJU,QAAQE,IAAI,CAACD,eAAgBJ,eAAgBE,oBACxCI,MAAKC,YAAE5D,KAACA,KAADC,GAAOA,iBAASC,UAAUE,oBAAoBgD,KAAMpD,KAAMC,OACjE0D,MAAK,KACFP,KAAKpF,cAAcC,mBAAUM,QAAQsF,eAAeC,QAAQnB,QACrDS,QAEVW,MAAMC,sBAAaC,WAGxBjB,SAAStC,iBACL,oBACA,KACI4C,8BAEJ,CAAC1C,MAAM,sBAGFsD,YAAYlB,UAAUmB,OAQnCC,qBAAepD,gEAAW,WAChBgC,SAAWjF,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQyE,4BAEvDkB,YAAYlB,UAAUqB,OACd,OAAbrD,UAGJgC,SAAStC,iBACL,oBACA,UACS4D,mBAAmBtD,YAE5B,CAACJ,MAAM,IAWf0D,mBAAmBtD,gBACTuD,cAAgBxG,KAAKH,UAAUI,cAAcC,mBAAUmD,SAASoD,WAChEC,gBAAkBF,cAAcpC,aAAa,QAE7CuC,WADwB3G,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQoG,yBAAyBF,kBAC7DzG,cAAcC,mBAAUM,QAAQqG,kBAAkB5D,eAEtF0D,iBACK,IAAIG,oDAA6C7D,gBAItD8D,8BAA8BP,oBAC9BhF,6BAA6BmF,YAAY,GAM9CA,WAAWK,iBAAkB,EAC7BL,WAAW/B,QACX+B,WAAWK,iBAAkB,EAMjCnC,gCACU6B,gBAAkB1G,KAAKH,UAAUI,cAAcC,mBAAUmD,SAASoD,WAAWrC,aAAa,QAC1F6C,sBAAwBjH,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQoG,yBAAyBF,kBAChGQ,mBAAqBD,MAAAA,6BAAAA,sBAAuBhH,cAAcC,mBAAUM,QAAQC,cAAcC,WAC3FwG,yBAGA1F,6BAA6B0F,oBAAoB,GAM1DC,+BACStH,UAAUuD,iBAAiBlD,mBAAUmD,SAAS+D,KAAKzD,SAASpB,sBAEzDO,oBAAoBP,iBAItB0C,SAAWjF,KAAKH,UAAUI,cAAcC,mBAAUM,QAAQyE,cAC5DoC,kBAASpC,SAAU,CACnBqC,UAAU,EACVC,OAAO,EACPC,UAAU,IAUlBT,8BAA8BU,cACpBC,MAAQD,OAAOrD,aAAa,QAC5B7C,eAAiBvB,KAAKH,UAAUI,cAClCC,mBAAUM,QAAQoG,yBAAyBc,WAGxB,OAAnBnG,sBAIsBA,eAAe6B,iBAAiBlD,mBAAUM,QAAQC,cAAcC,WACxEiD,SAASlD,qBAClBe,6BAA6Bf,eAAe,MAYzDe,6BAA6Bf,cAAekH,mBAClCC,kBAAoBnH,cAAcR,cAAcC,mBAAUC,QAAQ0H,YAClEC,kBAAoBrH,cAAcR,cAAcC,mBAAUC,QAAQoD,cAAcwE,aAChFC,uBAAyBvH,cAAcR,cAAcC,mBAAUC,QAAQoD,cAAcC,iBAEvFmE,aAEAlH,cAAckE,SAAW,EACzBiD,kBAAkBjD,SAAW,EAC7BmD,kBAAkBnD,SAAW,EAC7BqD,uBAAuBrD,SAAW,IAGlClE,cAAckE,UAAY,EAC1BiD,kBAAkBjD,UAAY,EAC9BmD,kBAAkBnD,UAAY,EAC9BqD,uBAAuBrD,UAAY,GAS3CsD,uBAAuBC,cACdC,uBACDD,SACCE,+EAAkBA,cAAcC,0EAAsBD,iBAS/DE,2BAA2BJ,cAClBC,uBACDD,SACCE,+EAAkBA,cAAcG,8EAA0BH,iBASnEI,wBAAwBN,cACfC,uBACDD,SACA,CAACE,cAAe1H,4EAAcA,UAAU+H,yEAAqBL,iBASrEM,uBAAuBR,cACdC,uBACDD,SACA,CAACE,cAAe1H,4EAAcA,UAAUiI,wEAAoBP,iBAWpED,uBAAuBD,QAASU,oBACtBR,cAAgBpI,KAAKK,wBAAwB6H,SAC7CxH,UAAYwH,QAAQ3H,QAAQL,mBAAUM,QAAQe,oBAE/Cb,YAAc0H,oBACT,IAAItB,MAAM,6DAGd+B,eAAiBD,aAAaR,cAAe1H,WAC9CmI,sBAIArH,6BAA6B4G,eAAe,QAC5C5G,6BAA6BqH,gBAAgB,GAClDA,eAAejE"} \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/exporter.min.js b/course/amd/build/local/activitychooser/exporter.min.js new file mode 100644 index 00000000000..2059c4b8382 --- /dev/null +++ b/course/amd/build/local/activitychooser/exporter.min.js @@ -0,0 +1,11 @@ +define("core_course/local/activitychooser/exporter",["exports","core/str"],(function(_exports,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * Module to generate template data for the activity chooser. + * + * @module core_course/local/activitychooser/exporter + * @copyright 2025 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const activityCategories=["activities","resources"];let allStrings=null;loadNecessaryStrings();async function loadNecessaryStrings(){if(null!==allStrings)return allStrings;allStrings={};const stringToLoad=[{key:"all",component:"core"},{key:"favourites",component:"core"},{key:"recommended",component:"core"},{key:"recommended_help",component:"core_course"},...activityCategories.map((key=>({key:key,component:"core"}))),...activityCategories.map((key=>({key:key+"_help",component:"core"})))],loadedStrings=await(0,_str.getStrings)(stringToLoad);return stringToLoad.forEach(((_ref,index)=>{let{key:key}=_ref;allStrings[key]=loadedStrings[index]})),allStrings}return _exports.default=class{getTabData(tabId,filteredModules,tabLabel){let tabHelp=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const result={tabId:tabId,active:arguments.length>4&&void 0!==arguments[4]&&arguments[4],items:filteredModules,displayed:filteredModules.length>0,tabLabel:tabLabel};return tabHelp&&(result.tabHelp=tabHelp),result}normaliseModulesData(modulesData){if(modulesData instanceof Map)modulesData=Array.from(modulesData.values());else if(!Array.isArray(modulesData))throw new Error("Invalid modules data format. Expected an array or a Map.");return modulesData}async getModChooserTemplateData(modulesData){modulesData=this.normaliseModulesData(modulesData);const allStrings=await loadNecessaryStrings(),favouriteTab=await this.getFavouriteTabData(modulesData),tabs=[{...this.getTabData("all",modulesData,allStrings.all,null,!favouriteTab.displayed),hasSearchResults:!0},favouriteTab,{...this.getTabData("recommended",modulesData.filter((mod=>!0===mod.recommended)),allStrings.recommended,allStrings.recommended_help),separator:!0}];return activityCategories.forEach(((category,index)=>{const categoryModules=modulesData.filter((mod=>mod.archetype==index));0!==categoryModules.length&&tabs.push(this.getTabData(category,categoryModules,allStrings[category],allStrings[category+"_help"]))})),{modules:modulesData,tabs:tabs}}async getFavouriteTabData(modulesData){modulesData=this.normaliseModulesData(modulesData);const allStrings=await loadNecessaryStrings(),favouriteModules=modulesData.filter((mod=>!0===mod.favourite));return this.getTabData("favourites",favouriteModules,allStrings.favourites,null,favouriteModules.length>0)}getSearchResultData(resultsModulesData){return{searchresultsnumber:(resultsModulesData=this.normaliseModulesData(resultsModulesData)).length,searchresults:resultsModulesData}}countTabItems(tabData){var _tabData$items$length,_tabData$items;return null!==(_tabData$items$length=null===(_tabData$items=tabData.items)||void 0===_tabData$items?void 0:_tabData$items.length)&&void 0!==_tabData$items$length?_tabData$items$length:0}},_exports.default})); + +//# sourceMappingURL=exporter.min.js.map \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/exporter.min.js.map b/course/amd/build/local/activitychooser/exporter.min.js.map new file mode 100644 index 00000000000..bea713a366c --- /dev/null +++ b/course/amd/build/local/activitychooser/exporter.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"exporter.min.js","sources":["../../../src/local/activitychooser/exporter.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 * Module to generate template data for the activity chooser.\n *\n * @module core_course/local/activitychooser/exporter\n * @copyright 2025 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getStrings} from 'core/str';\n\nconst activityCategories = ['activities', 'resources'];\n\nlet allStrings = null;\n\nloadNecessaryStrings();\n\nexport default class {\n /**\n * A tab data structure.\n *\n * @typedef {object} TabData\n * @property {String} tabId the tab ID\n * @property {Boolean} active whether the tab is active or not\n * @property {Array} items the filtered modules to be displayed in the tab\n * @property {Boolean} displayed whether the tab is displayed or not\n * @property {String} tabLabel the tab label\n * @property {String|null} tabHelp the help text for the tab (optional)\n */\n\n /**\n * Generate a tab data object for the activity chooser.\n *\n * @private\n * @param {String} tabId Tab ID.\n * @param {Array} filteredModules Filtered modules to be displayed in the tab.\n * @param {String} tabLabel Tab label.\n * @param {String|null} tabHelp Help text for the tab (optional).\n * @param {Boolean} active Whether the tab is active or not.\n * @return {TabData} Tab data object.\n */\n getTabData(tabId, filteredModules, tabLabel, tabHelp = null, active = false) {\n const result = {\n tabId: tabId,\n active: active,\n items: filteredModules,\n displayed: filteredModules.length > 0,\n tabLabel,\n };\n if (tabHelp) {\n result.tabHelp = tabHelp;\n }\n return result;\n }\n\n /**\n * Normalise the modules data to be used in the chooser.\n *\n * The modulesData can be a plain array or a Map. This method will convert it to a\n * plain array of objects.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Array} Normalised modules data.\n */\n normaliseModulesData(modulesData) {\n if (modulesData instanceof Map) {\n modulesData = Array.from(modulesData.values());\n } else if (!Array.isArray(modulesData)) {\n throw new Error('Invalid modules data format. Expected an array or a Map.');\n }\n return modulesData;\n }\n\n /**\n * Fetch the chooser template data for a specific section.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Promise} Promise resolved with the template data.\n */\n async getModChooserTemplateData(modulesData) {\n modulesData = this.normaliseModulesData(modulesData);\n const allStrings = await loadNecessaryStrings();\n const favouriteTab = await this.getFavouriteTabData(modulesData);\n\n const tabs = [\n {\n ...this.getTabData(\n 'all',\n modulesData,\n allStrings.all,\n null,\n !favouriteTab.displayed,\n ),\n hasSearchResults: true, // The all tab will also show search results.\n },\n favouriteTab,\n {\n ...this.getTabData(\n 'recommended',\n modulesData.filter(mod => mod.recommended === true),\n allStrings.recommended,\n allStrings.recommended_help\n ),\n separator: true, // Add a separator before the purpose categories.\n },\n ];\n\n activityCategories.forEach((category, index) => {\n const categoryModules = modulesData.filter(mod => mod.archetype == index);\n if (categoryModules.length === 0) {\n return;\n }\n tabs.push(\n this.getTabData(\n category,\n categoryModules,\n allStrings[category],\n allStrings[category + '_help']\n )\n );\n });\n\n return {\n modules: modulesData,\n tabs,\n };\n }\n\n /**\n * Get the favourite tab data.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Promise} Promise resolved with the template data.\n */\n async getFavouriteTabData(modulesData) {\n modulesData = this.normaliseModulesData(modulesData);\n const allStrings = await loadNecessaryStrings();\n\n // We need to deconstruct the modules data to ensure it is an array.\n const favouriteModules = modulesData.filter(\n mod => {\n return mod.favourite === true;\n }\n );\n\n return this.getTabData(\n 'favourites',\n favouriteModules,\n allStrings.favourites,\n null,\n favouriteModules.length > 0,\n );\n }\n\n /**\n * Get the search result template data.\n *\n * @param {Array|Map} resultsModulesData Modules data to be used in the chooser.\n * @return {Object} The template data.\n */\n getSearchResultData(resultsModulesData) {\n resultsModulesData = this.normaliseModulesData(resultsModulesData);\n return {\n 'searchresultsnumber': resultsModulesData.length,\n 'searchresults': resultsModulesData,\n };\n }\n\n /**\n * Get the number of items in a tab.\n *\n * @param {TabData} tabData The tab data.\n * @return {Number} The number of items in the tab.\n */\n countTabItems(tabData) {\n return tabData.items?.length ?? 0;\n }\n\n}\n\n/**\n * Load the necessary strings for the activity chooser.\n *\n * @return {Promise} Promise resolved with the loaded strings.\n */\nasync function loadNecessaryStrings() {\n if (allStrings !== null) {\n return allStrings;\n }\n allStrings = {};\n\n const stringToLoad = [\n {key: 'all', component: 'core'},\n {key: 'favourites', component: 'core'},\n {key: 'recommended', component: 'core'},\n {key: 'recommended_help', component: 'core_course'},\n ...activityCategories.map(\n (key) => ({\n key: key,\n component: 'core',\n })\n ),\n ...activityCategories.map(\n (key) => ({\n key: key + '_help',\n component: 'core',\n })\n ),\n ];\n\n const loadedStrings = await getStrings(stringToLoad);\n stringToLoad.forEach(({key}, index) => {\n allStrings[key] = loadedStrings[index];\n });\n return allStrings;\n}\n"],"names":["activityCategories","allStrings","loadNecessaryStrings","stringToLoad","key","component","map","loadedStrings","forEach","index","getTabData","tabId","filteredModules","tabLabel","tabHelp","result","active","items","displayed","length","normaliseModulesData","modulesData","Map","Array","from","values","isArray","Error","this","favouriteTab","getFavouriteTabData","tabs","all","hasSearchResults","filter","mod","recommended","recommended_help","separator","category","categoryModules","archetype","push","modules","favouriteModules","favourite","favourites","getSearchResultData","resultsModulesData","countTabItems","tabData","_tabData$items"],"mappings":";;;;;;;;MAyBMA,mBAAqB,CAAC,aAAc,iBAEtCC,WAAa,KAEjBC,sCA0KeA,0BACQ,OAAfD,kBACOA,WAEXA,WAAa,SAEPE,aAAe,CACjB,CAACC,IAAK,MAAOC,UAAW,QACxB,CAACD,IAAK,aAAcC,UAAW,QAC/B,CAACD,IAAK,cAAeC,UAAW,QAChC,CAACD,IAAK,mBAAoBC,UAAW,kBAClCL,mBAAmBM,KACjBF,OACGA,IAAKA,IACLC,UAAW,cAGhBL,mBAAmBM,KACjBF,OACGA,IAAKA,IAAM,QACXC,UAAW,YAKjBE,oBAAsB,mBAAWJ,qBACvCA,aAAaK,SAAQ,MAAQC,aAAPL,IAACA,UACnBH,WAAWG,KAAOG,cAAcE,UAE7BR,yCA7KPS,WAAWC,MAAOC,gBAAiBC,cAAUC,+DAAU,WAC7CC,OAAS,CACXJ,MAAOA,MACPK,+DACAC,MAAOL,gBACPM,UAAWN,gBAAgBO,OAAS,EACpCN,SAAAA,iBAEAC,UACAC,OAAOD,QAAUA,SAEdC,OAYXK,qBAAqBC,gBACbA,uBAAuBC,IACvBD,YAAcE,MAAMC,KAAKH,YAAYI,eAClC,IAAKF,MAAMG,QAAQL,mBAChB,IAAIM,MAAM,mEAEbN,4CASqBA,aAC5BA,YAAcO,KAAKR,qBAAqBC,mBAClCpB,iBAAmBC,uBACnB2B,mBAAqBD,KAAKE,oBAAoBT,aAE9CU,KAAO,CACT,IACOH,KAAKlB,WACJ,MACAW,YACApB,WAAW+B,IACX,MACCH,aAAaX,WAElBe,kBAAkB,GAEtBJ,aACA,IACOD,KAAKlB,WACJ,cACAW,YAAYa,QAAOC,MAA2B,IAApBA,IAAIC,cAC9BnC,WAAWmC,YACXnC,WAAWoC,kBAEfC,WAAW,WAInBtC,mBAAmBQ,SAAQ,CAAC+B,SAAU9B,eAC5B+B,gBAAkBnB,YAAYa,QAAOC,KAAOA,IAAIM,WAAahC,QACpC,IAA3B+B,gBAAgBrB,QAGpBY,KAAKW,KACDd,KAAKlB,WACD6B,SACAC,gBACAvC,WAAWsC,UACXtC,WAAWsC,SAAW,cAK3B,CACHI,QAAStB,YACTU,KAAAA,gCAUkBV,aACtBA,YAAcO,KAAKR,qBAAqBC,mBAClCpB,iBAAmBC,uBAGnB0C,iBAAmBvB,YAAYa,QACjCC,MAC6B,IAAlBA,IAAIU,mBAIZjB,KAAKlB,WACR,aACAkC,iBACA3C,WAAW6C,WACX,KACAF,iBAAiBzB,OAAS,GAUlC4B,oBAAoBC,0BAET,sBADPA,mBAAqBpB,KAAKR,qBAAqB4B,qBAED7B,qBACzB6B,oBAUzBC,cAAcC,8GACHA,QAAQjC,uCAARkC,eAAehC,8DAAU"} \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/repository.min.js b/course/amd/build/local/activitychooser/repository.min.js index 5f044e6cc83..8968a317d56 100644 --- a/course/amd/build/local/activitychooser/repository.min.js +++ b/course/amd/build/local/activitychooser/repository.min.js @@ -5,6 +5,6 @@ define("core_course/local/activitychooser/repository",["exports","core/ajax"],(f * @module core_course/local/activitychooser/repository * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unfavouriteModule=_exports.fetchFooterData=_exports.favouriteModule=_exports.activityModules=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.activityModules=(courseid,sectionnum)=>{const request={methodname:"core_course_get_course_content_items",args:{courseid:courseid,sectionnum:sectionnum}};return _ajax.default.call([request])[0]};_exports.favouriteModule=(modName,modID)=>{const request={methodname:"core_course_add_content_item_to_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.unfavouriteModule=(modName,modID)=>{const request={methodname:"core_course_remove_content_item_from_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.fetchFooterData=(courseid,sectionid)=>{const request={methodname:"core_course_get_activity_chooser_footer",args:{courseid:courseid,sectionid:sectionid}};return _ajax.default.call([request])[0]}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.fetchFooterData=_exports.favouriteModule=_exports.activityModules=void 0,_exports.getModalFooterData=async function(courseId,sectionNum){const cacheKey="".concat(courseId,"-").concat(sectionNum);if(sectionsFooterPromises.has(cacheKey))return sectionsFooterPromises.get(cacheKey);return sectionsFooterPromises.set(cacheKey,new Promise((resolve=>{resolve(fetchFooterData(courseId,sectionNum))}))),sectionsFooterPromises.get(cacheKey)},_exports.getModulesData=async function(courseId,sectionNum,sectionReturnNum,beforeMod){const cacheKey="".concat(courseId,"-").concat(sectionNum);sectionsModulesPromises.has(cacheKey)||sectionsModulesPromises.set(cacheKey,new Promise((resolve=>{resolve(activityModules(courseId,sectionNum))})));const moduleData=await sectionsModulesPromises.get(cacheKey);if(!moduleData)throw new Error("Cannot fetch module data");return function(webServiceData,sectionNum,sectionReturnNum,beforeMod){const newData=JSON.parse(JSON.stringify(webServiceData));return newData.content_items.forEach((module=>{module.link+="§ion="+sectionNum+"&beforemod="+(null!=beforeMod?beforeMod:0),sectionReturnNum&&(module.link+="&sr="+sectionReturnNum)})),newData.content_items}(moduleData,sectionNum,sectionReturnNum,beforeMod)},_exports.unfavouriteModule=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const sectionsModulesPromises=new Map,sectionsFooterPromises=new Map,activityModules=(courseid,sectionnum)=>{const request={methodname:"core_course_get_course_content_items",args:{courseid:courseid,sectionnum:sectionnum}};return _ajax.default.call([request])[0]};_exports.activityModules=activityModules;_exports.favouriteModule=(modName,modID)=>{const request={methodname:"core_course_add_content_item_to_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0].then((()=>(sectionsModulesPromises.clear(),!0)))};_exports.unfavouriteModule=(modName,modID)=>{const request={methodname:"core_course_remove_content_item_from_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0].then((()=>(sectionsModulesPromises.clear(),!0)))};const fetchFooterData=(courseid,sectionid)=>{const request={methodname:"core_course_get_activity_chooser_footer",args:{courseid:courseid,sectionid:sectionid}};return _ajax.default.call([request])[0]};_exports.fetchFooterData=fetchFooterData})); //# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/repository.min.js.map b/course/amd/build/local/activitychooser/repository.min.js.map index f3f181dc756..71353e1dafd 100644 --- a/course/amd/build/local/activitychooser/repository.min.js.map +++ b/course/amd/build/local/activitychooser/repository.min.js.map @@ -1 +1 @@ -{"version":3,"file":"repository.min.js","sources":["../../../src/local/activitychooser/repository.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 * A javascript module to handle user AJAX actions.\n *\n * @module core_course/local/activitychooser/repository\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ajax from 'core/ajax';\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method activityModules\n * @param {Number} courseid What course to fetch the modules for\n * @param {Number} sectionnum What course section to fetch the modules for\n * @return {object} jQuery promise\n */\nexport const activityModules = (courseid, sectionnum) => {\n const request = {\n methodname: 'core_course_get_course_content_items',\n args: {\n courseid: courseid,\n sectionnum: sectionnum,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is a users' favourite.\n *\n * @method favouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const favouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_add_content_item_to_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is no longer a users' favourite.\n *\n * @method unfavouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const unfavouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_remove_content_item_from_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method fetchFooterData\n * @param {Number} courseid What course to fetch the data for\n * @param {Number} sectionid What section to fetch the data for\n * @return {object} jQuery promise\n */\nexport const fetchFooterData = (courseid, sectionid) => {\n const request = {\n methodname: 'core_course_get_activity_chooser_footer',\n args: {\n courseid: courseid,\n sectionid: sectionid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","sectionnum","request","methodname","args","ajax","call","modName","modID","componentname","contentitemid","sectionid"],"mappings":";;;;;;;uPAgC+B,CAACA,SAAUC,oBAChCC,QAAU,CACZC,WAAY,uCACZC,KAAM,CACFJ,SAAUA,SACVC,WAAYA,oBAGbI,cAAKC,KAAK,CAACJ,UAAU,6BAYD,CAACK,QAASC,eAC/BN,QAAU,CACZC,WAAY,kDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,+BAYC,CAACK,QAASC,eACjCN,QAAU,CACZC,WAAY,uDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,6BAWD,CAACF,SAAUW,mBAChCT,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFJ,SAAUA,SACVW,UAAWA,mBAGZN,cAAKC,KAAK,CAACJ,UAAU"} \ No newline at end of file +{"version":3,"file":"repository.min.js","sources":["../../../src/local/activitychooser/repository.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 * A javascript module to handle user AJAX actions.\n *\n * @module core_course/local/activitychooser/repository\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ajax from 'core/ajax';\n\n// A promises map for caching specific section modules data.\nconst sectionsModulesPromises = new Map();\n\n// A promises map for caching a specific section footer data.\nconst sectionsFooterPromises = new Map();\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method activityModules\n * @param {Number} courseid What course to fetch the modules for\n * @param {Number} sectionnum What course section to fetch the modules for\n * @return {object} jQuery promise\n */\nexport const activityModules = (courseid, sectionnum) => {\n const request = {\n methodname: 'core_course_get_course_content_items',\n args: {\n courseid: courseid,\n sectionnum: sectionnum,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is a users' favourite.\n *\n * @method favouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const favouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_add_content_item_to_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n const promise = ajax.call([request])[0];\n // After the promise is resolved, we need to invalidate the cache for the section.\n return promise.then(() => {\n sectionsModulesPromises.clear();\n return true;\n });\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is no longer a users' favourite.\n *\n * @method unfavouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const unfavouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_remove_content_item_from_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n\n const promise = ajax.call([request])[0];\n // After the promise is resolved, we need to invalidate the cache for the section.\n return promise.then(() => {\n sectionsModulesPromises.clear();\n return true;\n });\n};\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method fetchFooterData\n * @param {Number} courseid What course to fetch the data for\n * @param {Number} sectionid What section to fetch the data for\n * @return {object} jQuery promise\n */\nexport const fetchFooterData = (courseid, sectionid) => {\n const request = {\n methodname: 'core_course_get_activity_chooser_footer',\n args: {\n courseid: courseid,\n sectionid: sectionid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method fetchSectionModules\n *\n * @private\n * @param {Number} courseId Course ID.\n * @param {Number} sectionNum Section number.\n * @param {Number} sectionReturnNum Section return.\n * @param {Number} beforeMod Before module number to be used in the module.\n * @return {Object} Tab data.\n */\nexport async function getModulesData(courseId, sectionNum, sectionReturnNum, beforeMod) {\n const cacheKey = `${courseId}-${sectionNum}`;\n if (!sectionsModulesPromises.has(cacheKey)) {\n sectionsModulesPromises.set(\n cacheKey,\n new Promise((resolve) => {\n resolve(activityModules(courseId, sectionNum));\n })\n );\n }\n\n const moduleData = await sectionsModulesPromises.get(cacheKey);\n\n // Early return if there is no module data.\n if (!moduleData) {\n throw new Error('Cannot fetch module data');\n }\n\n // Apply the section num to all the module instance links.\n return sectionMapper(\n moduleData,\n sectionNum,\n sectionReturnNum,\n beforeMod\n );\n}\n\n/**\n * Given the web service data and an ID we want to make a deep copy\n * of the WS data then add on the section num to the addoption URL\n *\n * @method sectionMapper\n * @param {Object} webServiceData Our original data from the Web service call\n * @param {Number} sectionNum The number of the section we need to append to the links\n * @param {Number|null} sectionReturnNum The number of the section return we need to append to the links\n * @param {Number|null} beforeMod The ID of the cm we need to append to the links\n * @return {Array} [modules] with URL's built\n */\nfunction sectionMapper(webServiceData, sectionNum, sectionReturnNum, beforeMod) {\n // We need to take a fresh deep copy of the original data as an object is a reference type.\n const newData = JSON.parse(JSON.stringify(webServiceData));\n newData.content_items.forEach((module) => {\n module.link += '§ion=' + sectionNum + '&beforemod=' + (beforeMod ?? 0);\n if (sectionReturnNum) {\n module.link += '&sr=' + sectionReturnNum;\n }\n });\n return newData.content_items;\n}\n\n/**\n * Fetch the footer data for a specific section.\n *\n * @param {Number} courseId Course ID.\n * @param {Number} sectionNum Section number.\n * @return {Promise} Promise resolved with the footer data.\n */\nexport async function getModalFooterData(courseId, sectionNum) {\n const cacheKey = `${courseId}-${sectionNum}`;\n if (sectionsFooterPromises.has(cacheKey)) {\n return sectionsFooterPromises.get(cacheKey);\n }\n\n sectionsFooterPromises.set(\n cacheKey,\n new Promise((resolve) => {\n resolve(fetchFooterData(courseId, sectionNum));\n })\n );\n return sectionsFooterPromises.get(cacheKey);\n}\n"],"names":["courseId","sectionNum","cacheKey","sectionsFooterPromises","has","get","set","Promise","resolve","fetchFooterData","sectionReturnNum","beforeMod","sectionsModulesPromises","activityModules","moduleData","Error","webServiceData","newData","JSON","parse","stringify","content_items","forEach","module","link","sectionMapper","Map","courseid","sectionnum","request","methodname","args","ajax","call","modName","modID","componentname","contentitemid","then","clear","sectionid"],"mappings":";;;;;;;0LA4LyCA,SAAUC,kBACzCC,mBAAcF,qBAAYC,eAC5BE,uBAAuBC,IAAIF,iBACpBC,uBAAuBE,IAAIH,iBAGtCC,uBAAuBG,IACnBJ,SACA,IAAIK,SAASC,UACTA,QAAQC,gBAAgBT,SAAUC,iBAGnCE,uBAAuBE,IAAIH,kDArEDF,SAAUC,WAAYS,iBAAkBC,iBACnET,mBAAcF,qBAAYC,YAC3BW,wBAAwBR,IAAIF,WAC7BU,wBAAwBN,IACpBJ,SACA,IAAIK,SAASC,UACTA,QAAQK,gBAAgBb,SAAUC,uBAKxCa,iBAAmBF,wBAAwBP,IAAIH,cAGhDY,iBACK,IAAIC,MAAM,4CAuBDC,eAAgBf,WAAYS,iBAAkBC,iBAE3DM,QAAUC,KAAKC,MAAMD,KAAKE,UAAUJ,wBAC1CC,QAAQI,cAAcC,SAASC,SAC3BA,OAAOC,MAAQ,YAAcvB,WAAa,eAAiBU,MAAAA,UAAAA,UAAa,GACpED,mBACAa,OAAOC,MAAQ,OAASd,qBAGzBO,QAAQI,cA5BRI,CACHX,WACAb,WACAS,iBACAC,wGAjIFC,wBAA0B,IAAIc,IAG9BvB,uBAAyB,IAAIuB,IAUtBb,gBAAkB,CAACc,SAAUC,oBAChCC,QAAU,CACZC,WAAY,uCACZC,KAAM,CACFJ,SAAUA,SACVC,WAAYA,oBAGbI,cAAKC,KAAK,CAACJ,UAAU,sEAYD,CAACK,QAASC,eAC/BN,QAAU,CACZC,WAAY,kDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGPH,cAAKC,KAAK,CAACJ,UAAU,GAEtBS,MAAK,KAChB1B,wBAAwB2B,SACjB,iCAakB,CAACL,QAASC,eACjCN,QAAU,CACZC,WAAY,uDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAIPH,cAAKC,KAAK,CAACJ,UAAU,GAEtBS,MAAK,KAChB1B,wBAAwB2B,SACjB,YAYF9B,gBAAkB,CAACkB,SAAUa,mBAChCX,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFJ,SAAUA,SACVa,UAAWA,mBAGZR,cAAKC,KAAK,CAACJ,UAAU"} \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/selectors.min.js b/course/amd/build/local/activitychooser/selectors.min.js index ea12a997acb..a9386273804 100644 --- a/course/amd/build/local/activitychooser/selectors.min.js +++ b/course/amd/build/local/activitychooser/selectors.min.js @@ -6,6 +6,6 @@ define("core_course/local/activitychooser/selectors",["exports"],(function(_expo * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{chooser:getDataSelector("region","chooser-container"),getSectionChooserOptions:containerid=>"".concat(containerid," ").concat(getDataSelector("region","chooser-options-container")),chooserOption:{container:getDataSelector("region","chooser-option-container"),actions:getDataSelector("region","chooser-option-actions-container"),info:getDataSelector("region","chooser-option-info-container")},chooserSummary:{container:getDataSelector("region","chooser-option-summary-container"),content:getDataSelector("region","chooser-option-summary-content-container"),header:getDataSelector("region","summary-header"),actions:getDataSelector("region","chooser-option-summary-actions-container")},carousel:getDataSelector("region","carousel"),help:getDataSelector("region","help"),modules:getDataSelector("region","modules"),favouriteTabNav:getDataSelector("region","favourite-tab-nav"),defaultTabNav:getDataSelector("region","default-tab-nav"),activityTabNav:getDataSelector("region","activity-tab-nav"),favouriteTab:getDataSelector("region","favourites"),recommendedTab:getDataSelector("region","recommended"),defaultTab:getDataSelector("region","default"),activityTab:getDataSelector("region","activity"),resourceTab:getDataSelector("region","resources"),getModuleSelector:modname=>'[role="menuitem"][data-modname="'.concat(modname,'"]'),searchResults:getDataSelector("region","search-results-container"),searchResultItems:getDataSelector("region","search-result-items-container")},actions:{optionActions:{showSummary:getDataSelector("action","show-option-summary"),manageFavourite:getDataSelector("action","manage-module-favourite")},addChooser:getDataSelector("action","add-chooser-option"),closeOption:getDataSelector("action","close-chooser-option-summary"),hide:getDataSelector("action","hide"),search:getDataSelector("action","search"),clearSearch:getDataSelector("action","clearsearch")},render:{favourites:getDataSelector("render","favourites-area")},elements:{section:".section",sectionmodchooser:"button.section-modchooser-link",sitemenu:".block_site_main_menu",sitetopic:"div.sitetopic",tab:'a[data-bs-toggle="tab"]',activetab:'a[data-bs-toggle="tab"][aria-selected="true"]',visibletabs:'a[data-bs-toggle="tab"]:not(.d-none)'}};return _exports.default=_default,_exports.default})); +const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{categoryContent:getDataSelector("region","category-content"),getSectionChooserOptions:containerid=>"".concat(containerid," ").concat(getDataSelector("region","chooser-options-container")),chooserOptions:getDataSelector("region","chooser-options-container"),chooserOption:{container:getDataSelector("region","chooser-option-container"),actions:getDataSelector("region","chooser-option-actions-container"),info:getDataSelector("region","chooser-option-info-container")},chooserSummary:{container:getDataSelector("region","chooser-option-summary-container"),content:getDataSelector("region","chooser-option-summary-content-container"),header:getDataSelector("region","summary-header"),actions:getDataSelector("region","chooser-option-summary-actions-container")},carousel:getDataSelector("region","carousel"),help:getDataSelector("region","help"),tabContent:getDataSelector("region","tabcontent"),favouriteTabNav:getDataSelector("region","favourites-tab-nav"),allTabNav:getDataSelector("region","all-tab-nav"),favouriteTab:getDataSelector("region","favourites"),getModuleSelector:modname=>'[role="menuitem"][data-modname="'.concat(modname,'"]'),searchResults:getDataSelector("region","search-results-container")},actions:{optionActions:{showSummary:getDataSelector("action","show-option-summary"),manageFavourite:getDataSelector("action","manage-module-favourite")},addChooser:getDataSelector("action","add-chooser-option"),closeOption:getDataSelector("action","close-chooser-option-summary"),hide:getDataSelector("action","hide"),search:getDataSelector("action","search"),clearSearch:getDataSelector("action","clearsearch")},elements:{section:".section",sectionmodchooser:"button.section-modchooser-link",tab:'a[data-bs-toggle="tab"]',activetab:'a[data-bs-toggle="tab"][aria-selected="true"]',moduleItem:item=>'[data-internal="'.concat(item,'"]'),favouriteIconActive:'[data-icon="favourite-active"]',favouriteIconInactive:'[data-icon="favourite-inactive"]'}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=selectors.min.js.map \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/selectors.min.js.map b/course/amd/build/local/activitychooser/selectors.min.js.map index bac816e3d84..d00e753e57a 100644 --- a/course/amd/build/local/activitychooser/selectors.min.js.map +++ b/course/amd/build/local/activitychooser/selectors.min.js.map @@ -1 +1 @@ -{"version":3,"file":"selectors.min.js","sources":["../../../src/local/activitychooser/selectors.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 * Define all of the selectors we will be using on the grading interface.\n *\n * @module core_course/local/activitychooser/selectors\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n chooser: getDataSelector('region', 'chooser-container'),\n getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,\n chooserOption: {\n container: getDataSelector('region', 'chooser-option-container'),\n actions: getDataSelector('region', 'chooser-option-actions-container'),\n info: getDataSelector('region', 'chooser-option-info-container'),\n },\n chooserSummary: {\n container: getDataSelector('region', 'chooser-option-summary-container'),\n content: getDataSelector('region', 'chooser-option-summary-content-container'),\n header: getDataSelector('region', 'summary-header'),\n actions: getDataSelector('region', 'chooser-option-summary-actions-container'),\n },\n carousel: getDataSelector('region', 'carousel'),\n help: getDataSelector('region', 'help'),\n modules: getDataSelector('region', 'modules'),\n favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),\n defaultTabNav: getDataSelector('region', 'default-tab-nav'),\n activityTabNav: getDataSelector('region', 'activity-tab-nav'),\n favouriteTab: getDataSelector('region', 'favourites'),\n recommendedTab: getDataSelector('region', 'recommended'),\n defaultTab: getDataSelector('region', 'default'),\n activityTab: getDataSelector('region', 'activity'),\n resourceTab: getDataSelector('region', 'resources'),\n getModuleSelector: modname => `[role=\"menuitem\"][data-modname=\"${modname}\"]`,\n searchResults: getDataSelector('region', 'search-results-container'),\n searchResultItems: getDataSelector('region', 'search-result-items-container'),\n },\n actions: {\n optionActions: {\n showSummary: getDataSelector('action', 'show-option-summary'),\n manageFavourite: getDataSelector('action', 'manage-module-favourite'),\n },\n addChooser: getDataSelector('action', 'add-chooser-option'),\n closeOption: getDataSelector('action', 'close-chooser-option-summary'),\n hide: getDataSelector('action', 'hide'),\n search: getDataSelector('action', 'search'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n },\n render: {\n favourites: getDataSelector('render', 'favourites-area'),\n },\n elements: {\n section: '.section',\n sectionmodchooser: 'button.section-modchooser-link',\n sitemenu: '.block_site_main_menu',\n sitetopic: 'div.sitetopic',\n tab: 'a[data-bs-toggle=\"tab\"]',\n activetab: 'a[data-bs-toggle=\"tab\"][aria-selected=\"true\"]',\n visibletabs: 'a[data-bs-toggle=\"tab\"]:not(.d-none)'\n },\n};\n"],"names":["getDataSelector","name","value","regions","chooser","getSectionChooserOptions","containerid","chooserOption","container","actions","info","chooserSummary","content","header","carousel","help","modules","favouriteTabNav","defaultTabNav","activityTabNav","favouriteTab","recommendedTab","defaultTab","activityTab","resourceTab","getModuleSelector","modname","searchResults","searchResultItems","optionActions","showSummary","manageFavourite","addChooser","closeOption","hide","search","clearSearch","render","favourites","elements","section","sectionmodchooser","sitemenu","sitetopic","tab","activetab","visibletabs"],"mappings":";;;;;;;;MA8BMA,gBAAkB,CAACC,KAAMC,wBACXD,kBAASC,yBAGd,CACXC,QAAS,CACLC,QAASJ,gBAAgB,SAAU,qBACnCK,yBAA0BC,uBAAkBA,wBAAeN,gBAAgB,SAAU,8BACrFO,cAAe,CACXC,UAAWR,gBAAgB,SAAU,4BACrCS,QAAST,gBAAgB,SAAU,oCACnCU,KAAMV,gBAAgB,SAAU,kCAEpCW,eAAgB,CACZH,UAAWR,gBAAgB,SAAU,oCACrCY,QAASZ,gBAAgB,SAAU,4CACnCa,OAAQb,gBAAgB,SAAU,kBAClCS,QAAST,gBAAgB,SAAU,6CAEvCc,SAAUd,gBAAgB,SAAU,YACpCe,KAAMf,gBAAgB,SAAU,QAChCgB,QAAShB,gBAAgB,SAAU,WACnCiB,gBAAiBjB,gBAAgB,SAAU,qBAC3CkB,cAAelB,gBAAgB,SAAU,mBACzCmB,eAAgBnB,gBAAgB,SAAU,oBAC1CoB,aAAcpB,gBAAgB,SAAU,cACxCqB,eAAgBrB,gBAAgB,SAAU,eAC1CsB,WAAYtB,gBAAgB,SAAU,WACtCuB,YAAavB,gBAAgB,SAAU,YACvCwB,YAAaxB,gBAAgB,SAAU,aACvCyB,kBAAmBC,mDAA8CA,cACjEC,cAAe3B,gBAAgB,SAAU,4BACzC4B,kBAAmB5B,gBAAgB,SAAU,kCAEjDS,QAAS,CACLoB,cAAe,CACXC,YAAa9B,gBAAgB,SAAU,uBACvC+B,gBAAiB/B,gBAAgB,SAAU,4BAE/CgC,WAAYhC,gBAAgB,SAAU,sBACtCiC,YAAajC,gBAAgB,SAAU,gCACvCkC,KAAMlC,gBAAgB,SAAU,QAChCmC,OAAQnC,gBAAgB,SAAU,UAClCoC,YAAapC,gBAAgB,SAAU,gBAE3CqC,OAAQ,CACJC,WAAYtC,gBAAgB,SAAU,oBAE1CuC,SAAU,CACNC,QAAS,WACTC,kBAAmB,iCACnBC,SAAU,wBACVC,UAAW,gBACXC,IAAK,0BACLC,UAAW,gDACXC,YAAa"} \ No newline at end of file +{"version":3,"file":"selectors.min.js","sources":["../../../src/local/activitychooser/selectors.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 * Define all of the selectors we will be using on the grading interface.\n *\n * @module core_course/local/activitychooser/selectors\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n categoryContent: getDataSelector('region', 'category-content'),\n getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,\n chooserOptions: getDataSelector('region', 'chooser-options-container'),\n chooserOption: {\n container: getDataSelector('region', 'chooser-option-container'),\n actions: getDataSelector('region', 'chooser-option-actions-container'),\n info: getDataSelector('region', 'chooser-option-info-container'),\n },\n chooserSummary: {\n container: getDataSelector('region', 'chooser-option-summary-container'),\n content: getDataSelector('region', 'chooser-option-summary-content-container'),\n header: getDataSelector('region', 'summary-header'),\n actions: getDataSelector('region', 'chooser-option-summary-actions-container'),\n },\n carousel: getDataSelector('region', 'carousel'),\n help: getDataSelector('region', 'help'),\n tabContent: getDataSelector('region', 'tabcontent'),\n favouriteTabNav: getDataSelector('region', 'favourites-tab-nav'),\n allTabNav: getDataSelector('region', 'all-tab-nav'),\n favouriteTab: getDataSelector('region', 'favourites'),\n getModuleSelector: modname => `[role=\"menuitem\"][data-modname=\"${modname}\"]`,\n searchResults: getDataSelector('region', 'search-results-container'),\n },\n actions: {\n optionActions: {\n showSummary: getDataSelector('action', 'show-option-summary'),\n manageFavourite: getDataSelector('action', 'manage-module-favourite'),\n },\n addChooser: getDataSelector('action', 'add-chooser-option'),\n closeOption: getDataSelector('action', 'close-chooser-option-summary'),\n hide: getDataSelector('action', 'hide'),\n search: getDataSelector('action', 'search'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n },\n elements: {\n section: '.section',\n sectionmodchooser: 'button.section-modchooser-link',\n tab: 'a[data-bs-toggle=\"tab\"]',\n activetab: 'a[data-bs-toggle=\"tab\"][aria-selected=\"true\"]',\n moduleItem: item => `[data-internal=\"${item}\"]`,\n favouriteIconActive: '[data-icon=\"favourite-active\"]',\n favouriteIconInactive: '[data-icon=\"favourite-inactive\"]',\n },\n};\n"],"names":["getDataSelector","name","value","regions","categoryContent","getSectionChooserOptions","containerid","chooserOptions","chooserOption","container","actions","info","chooserSummary","content","header","carousel","help","tabContent","favouriteTabNav","allTabNav","favouriteTab","getModuleSelector","modname","searchResults","optionActions","showSummary","manageFavourite","addChooser","closeOption","hide","search","clearSearch","elements","section","sectionmodchooser","tab","activetab","moduleItem","item","favouriteIconActive","favouriteIconInactive"],"mappings":";;;;;;;;MA8BMA,gBAAkB,CAACC,KAAMC,wBACXD,kBAASC,yBAGd,CACXC,QAAS,CACLC,gBAAiBJ,gBAAgB,SAAU,oBAC3CK,yBAA0BC,uBAAkBA,wBAAeN,gBAAgB,SAAU,8BACrFO,eAAgBP,gBAAgB,SAAU,6BAC1CQ,cAAe,CACXC,UAAWT,gBAAgB,SAAU,4BACrCU,QAASV,gBAAgB,SAAU,oCACnCW,KAAMX,gBAAgB,SAAU,kCAEpCY,eAAgB,CACZH,UAAWT,gBAAgB,SAAU,oCACrCa,QAASb,gBAAgB,SAAU,4CACnCc,OAAQd,gBAAgB,SAAU,kBAClCU,QAASV,gBAAgB,SAAU,6CAEvCe,SAAUf,gBAAgB,SAAU,YACpCgB,KAAMhB,gBAAgB,SAAU,QAChCiB,WAAYjB,gBAAgB,SAAU,cACtCkB,gBAAiBlB,gBAAgB,SAAU,sBAC3CmB,UAAWnB,gBAAgB,SAAU,eACrCoB,aAAcpB,gBAAgB,SAAU,cACxCqB,kBAAmBC,mDAA8CA,cACjEC,cAAevB,gBAAgB,SAAU,6BAE7CU,QAAS,CACLc,cAAe,CACXC,YAAazB,gBAAgB,SAAU,uBACvC0B,gBAAiB1B,gBAAgB,SAAU,4BAE/C2B,WAAY3B,gBAAgB,SAAU,sBACtC4B,YAAa5B,gBAAgB,SAAU,gCACvC6B,KAAM7B,gBAAgB,SAAU,QAChC8B,OAAQ9B,gBAAgB,SAAU,UAClC+B,YAAa/B,gBAAgB,SAAU,gBAE3CgC,SAAU,CACNC,QAAS,WACTC,kBAAmB,iCACnBC,IAAK,0BACLC,UAAW,gDACXC,WAAYC,gCAA2BA,WACvCC,oBAAqB,iCACrBC,sBAAuB"} \ No newline at end of file diff --git a/course/amd/src/activitychooser.js b/course/amd/src/activitychooser.js index 1bc047ef9f0..515cd1d1efe 100644 --- a/course/amd/src/activitychooser.js +++ b/course/amd/src/activitychooser.js @@ -25,25 +25,8 @@ import * as ChooserDialogue from 'core_course/local/activitychooser/dialogue'; import * as Repository from 'core_course/local/activitychooser/repository'; import selectors from 'core_course/local/activitychooser/selectors'; import CustomEvents from 'core/custom_interaction_events'; -import * as Templates from 'core/templates'; -import {getString} from 'core/str'; -import Modal from 'core/modal'; import Pending from 'core/pending'; -// Set up some JS module wide constants that can be added to in the future. - -// Tab config options. -const ALLACTIVITIESRESOURCES = 0; -const ACTIVITIESRESOURCES = 2; -const ALLACTIVITIESRESOURCESREC = 3; -const ONLYALLREC = 4; -const ACTIVITIESRESOURCESREC = 5; - - -// Module types. -const ACTIVITY = 0; -const RESOURCE = 1; - let initialized = false; /** @@ -56,7 +39,12 @@ let initialized = false; export const init = (courseId, chooserConfig) => { const pendingPromise = new Pending(); - registerListenerEvents(courseId, chooserConfig); + // TODO: Remove the chooserConfig in Moodle 6.0 (MDL-85655) + if (chooserConfig.tabmode !== undefined) { + window.console.warn('The tabmode config option has been deprecated and will be ignored.'); + } + + registerListenerEvents(courseId); pendingPromise.resolve(); }; @@ -66,371 +54,84 @@ export const init = (courseId, chooserConfig) => { * * @method registerListenerEvents * @param {Number} courseId - * @param {Object} chooserConfig Any PHP config settings that we may need to reference */ -const registerListenerEvents = (courseId, chooserConfig) => { - - // Ensure we only add our listeners once. +const registerListenerEvents = (courseId) => { if (initialized) { return; } + initialized = true; - const events = [ + const eventsToHandle = [ 'click', CustomEvents.events.activate, CustomEvents.events.keyboardActivate ]; - const fetchModuleData = (() => { - let innerPromises = new Map(); - - return (sectionNum) => { - if (innerPromises.has(sectionNum)) { - return innerPromises.get(sectionNum); - } - - innerPromises.set( - sectionNum, - new Promise((resolve) => { - resolve(Repository.activityModules(courseId, sectionNum)); - }) - ); - return innerPromises.get(sectionNum); - }; - })(); - - const fetchFooterData = (() => { - let footerInnerPromise = null; - - return (sectionNum) => { - if (!footerInnerPromise) { - footerInnerPromise = new Promise((resolve) => { - resolve(Repository.fetchFooterData(courseId, sectionNum)); - }); - } - - return footerInnerPromise; - }; - })(); - - CustomEvents.define(document, events); + CustomEvents.define(document, eventsToHandle); // Display module chooser event listeners. - events.forEach((event) => { - document.addEventListener(event, async(e) => { - if (e.target.closest(selectors.elements.sectionmodchooser)) { - let caller; - let sectionnum; - // We need to know who called this. - // Standard courses use the ID in the main section info. - const sectionDiv = e.target.closest(selectors.elements.section); - // Front page courses need some special handling. - const button = e.target.closest(selectors.elements.sectionmodchooser); - - // If we don't have a section number use the fallback ID. - // We always want the sectionDiv caller first as it keeps track of section number's after DnD changes. - // The button attribute is always just a fallback for us as the section div is not always available. - // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here. - if (sectionDiv !== null && sectionDiv.hasAttribute('data-number')) { - // We check for attributes just in case of outdated contrib course formats. - caller = sectionDiv; - sectionnum = sectionDiv.getAttribute('data-number'); - } else { - caller = button; - - if (caller.hasAttribute('data-sectionid')) { - window.console.warn( - 'The data-sectionid attribute has been deprecated. ' + - 'Please update your code to use data-sectionnum instead.' - ); - caller.setAttribute('data-sectionnum', caller.dataset.sectionid); - } - sectionnum = caller.dataset.sectionnum; - } - - // We want to show the modal instantly but loading whilst waiting for our data. - let bodyPromiseResolver; - const bodyPromise = new Promise(resolve => { - bodyPromiseResolver = resolve; - }); - - const footerData = await fetchFooterData(sectionnum); - const sectionModal = buildModal(bodyPromise, footerData); - - // Now we have a modal we should start fetching data. - // If an error occurs while fetching the data, display the error within the modal. - const data = await fetchModuleData(sectionnum).catch(async(e) => { - const errorTemplateData = { - 'errormessage': e.message - }; - bodyPromiseResolver(await Templates.render('core_course/local/activitychooser/error', errorTemplateData)); - }); - - // Early return if there is no module data. - if (!data) { - return; - } - - // Apply the section num to all the module instance links. - const builtModuleData = sectionMapper( - data, - sectionnum, - caller.dataset.sectionreturnnum, - caller.dataset.beforemod - ); - - ChooserDialogue.displayChooser( - sectionModal, - builtModuleData, - partiallyAppliedFavouriteManager(data, sectionnum), - footerData, - ); - - bodyPromiseResolver(await Templates.render( - 'core_course/activitychooser', - templateDataBuilder(builtModuleData, chooserConfig) - )); + eventsToHandle.forEach((eventToHandle) => { + document.addEventListener(eventToHandle, async(event) => { + if (!event.target.closest(selectors.elements.sectionmodchooser)) { + return; } + const position = getCoursePositionFromTarget(event.target); + + const footerDataPromise = Repository.getModalFooterData(courseId, position.sectionNum); + + const modulesDataPromise = Repository.getModulesData( + courseId, + position.sectionNum, + position.sectionReturnNum, + position.beforeMod, + ); + + ChooserDialogue.displayActivityChooserModal(footerDataPromise, modulesDataPromise); }); }); - - initialized = true; }; /** - * Given the web service data and an ID we want to make a deep copy - * of the WS data then add on the section num to the addoption URL + * Return the course position of a target add activity element. * - * @method sectionMapper - * @param {Object} webServiceData Our original data from the Web service call - * @param {Number} num The number of the section we need to append to the links - * @param {Number|null} sectionreturnnum The number of the section return we need to append to the links - * @param {Number|null} beforemod The ID of the cm we need to append to the links - * @return {Array} [modules] with URL's built + * @param {HTMLElement} target The target element. + * @return {Object} The course position of the target. + * @property {Number} sectionNum The section number. + * @property {Number|null} sectionReturnNum The section return number. + * @property {Number|null} beforeMod The ID of the cm to add the modules before. */ -const sectionMapper = (webServiceData, num, sectionreturnnum, beforemod) => { - // We need to take a fresh deep copy of the original data as an object is a reference type. - const newData = JSON.parse(JSON.stringify(webServiceData)); - newData.content_items.forEach((module) => { - module.link += '§ion=' + num + '&beforemod=' + (beforemod ?? 0); - if (sectionreturnnum) { - module.link += '&sr=' + sectionreturnnum; - } - }); - return newData.content_items; -}; +function getCoursePositionFromTarget(target) { + let caller; + let sectionNum; + // We need to know who called this. + // Standard courses use the ID in the main section info. + const sectionDiv = target.closest(selectors.elements.section); + // Front page courses need some special handling. + const button = target.closest(selectors.elements.sectionmodchooser); -/** - * Given an array of modules we want to figure out where & how to place them into our template object - * - * @method templateDataBuilder - * @param {Array} data our modules to manipulate into a Templatable object - * @param {Object} chooserConfig Any PHP config settings that we may need to reference - * @return {Object} Our built object ready to render out - */ -const templateDataBuilder = (data, chooserConfig) => { - // Setup of various bits and pieces we need to mutate before throwing it to the wolves. - let activities = []; - let resources = []; - let showAll = true; - let showActivities = false; - let showResources = false; + // If we don't have a section number use the fallback ID. + // We always want the sectionDiv caller first as it keeps track of section number's after DnD changes. + // The button attribute is always just a fallback for us as the section div is not always available. + // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here. + if (sectionDiv !== null && sectionDiv.hasAttribute('data-number')) { + // We check for attributes just in case of outdated contrib course formats. + caller = sectionDiv; + sectionNum = sectionDiv.getAttribute('data-number'); + } else { + caller = button; - // Tab mode can be the following [All, Resources & Activities, All & Activities & Resources]. - const tabMode = parseInt(chooserConfig.tabmode); - - // Filter the incoming data to find favourite & recommended modules. - const favourites = data.filter(mod => mod.favourite === true); - const recommended = data.filter(mod => mod.recommended === true); - - // Whether the activities and resources tabs should be displayed or not. - const showActivitiesAndResources = (tabMode) => { - const acceptableModes = [ - ALLACTIVITIESRESOURCES, - ALLACTIVITIESRESOURCESREC, - ACTIVITIESRESOURCES, - ACTIVITIESRESOURCESREC, - ]; - - return acceptableModes.indexOf(tabMode) !== -1; - }; - - // These modes need Activity & Resource tabs. - if (showActivitiesAndResources(tabMode)) { - // Filter the incoming data to find activities then resources. - activities = data.filter(mod => mod.archetype === ACTIVITY); - resources = data.filter(mod => mod.archetype === RESOURCE); - showActivities = true; - showResources = true; - - // We want all of the previous information but no 'All' tab. - if (tabMode === ACTIVITIESRESOURCES || tabMode === ACTIVITIESRESOURCESREC) { - showAll = false; + if (caller.hasAttribute('data-sectionid')) { + window.console.warn( + 'The data-sectionid attribute has been deprecated. ' + + 'Please update your code to use data-sectionnum instead.' + ); + caller.setAttribute('data-sectionnum', caller.dataset.sectionid); } + sectionNum = caller.dataset.sectionnum; } - - const recommendedBeforeTabs = [ - ALLACTIVITIESRESOURCESREC, - ONLYALLREC, - ACTIVITIESRESOURCESREC, - ]; - // Whether the recommended tab should be displayed before the All/Activities/Resources tabs. - const recommendedBeginning = recommendedBeforeTabs.indexOf(tabMode) !== -1; - - // Given the results of the above filters lets figure out what tab to set active. - // We have some favourites. - const favouritesFirst = !!favourites.length; - const recommendedFirst = favouritesFirst === false && recommendedBeginning === true && !!recommended.length; - // We are in tabMode 2 without any favourites. - const activitiesFirst = showAll === false && favouritesFirst === false && recommendedFirst === false; - // We have nothing fallback to show all modules. - const fallback = showAll === true && favouritesFirst === false && recommendedFirst === false; - return { - 'default': data, - showAll: showAll, - activities: activities, - showActivities: showActivities, - activitiesFirst: activitiesFirst, - resources: resources, - showResources: showResources, - favourites: favourites, - recommended: recommended, - recommendedFirst: recommendedFirst, - recommendedBeginning: recommendedBeginning, - favouritesFirst: favouritesFirst, - fallback: fallback, + sectionNum, + sectionReturnNum: caller.dataset?.sectionreturnnum ?? null, + beforeMod: caller.dataset?.beforemod ?? null, }; -}; - -/** - * Given an object we want to build a modal ready to show - * - * @method buildModal - * @param {Promise} body - * @param {String|Boolean} footer Either a footer to add or nothing - * @return {Object} The modal ready to display immediately and render body in later. - */ -const buildModal = (body, footer) => Modal.create({ - body, - title: getString('addresourceoractivity'), - footer: footer.customfootertemplate, - large: true, - scrollable: false, - templateContext: { - classes: 'modchooser' - }, - show: true, -}); - -/** - * A small helper function to handle the case where there are no more favourites - * and we need to mess a bit with the available tabs in the chooser - * - * @method nullFavouriteDomManager - * @param {HTMLElement} favouriteTabNav Dom node of the favourite tab nav - * @param {HTMLElement} modalBody Our current modals' body - */ -const nullFavouriteDomManager = (favouriteTabNav, modalBody) => { - favouriteTabNav.tabIndex = -1; - favouriteTabNav.classList.add('d-none'); - // Need to set active to an available tab. - if (favouriteTabNav.classList.contains('active')) { - favouriteTabNav.classList.remove('active'); - favouriteTabNav.setAttribute('aria-selected', 'false'); - const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab); - favouriteTab.classList.remove('active'); - const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav); - const activitiesTabNav = modalBody.querySelector(selectors.regions.activityTabNav); - if (defaultTabNav.classList.contains('d-none') === false) { - defaultTabNav.classList.add('active'); - defaultTabNav.setAttribute('aria-selected', 'true'); - defaultTabNav.tabIndex = 0; - defaultTabNav.focus(); - const defaultTab = modalBody.querySelector(selectors.regions.defaultTab); - defaultTab.classList.add('active'); - } else { - activitiesTabNav.classList.add('active'); - activitiesTabNav.setAttribute('aria-selected', 'true'); - activitiesTabNav.tabIndex = 0; - activitiesTabNav.focus(); - const activitiesTab = modalBody.querySelector(selectors.regions.activityTab); - activitiesTab.classList.add('active'); - } - - } -}; - -/** - * Export a curried function where the builtModules has been applied. - * We have our array of modules so we can rerender the favourites area and have all of the items sorted. - * - * @method partiallyAppliedFavouriteManager - * @param {Array} moduleData This is our raw WS data that we need to manipulate - * @param {Number} sectionnum We need this to add the sectionnum to the URL's in the faves area after rerender - * @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array - */ -const partiallyAppliedFavouriteManager = (moduleData, sectionnum) => { - /** - * Curried function that is being returned. - * - * @param {String} internal Internal name of the module to manage - * @param {Boolean} favourite Is the caller adding a favourite or removing one? - * @param {HTMLElement} modalBody What we need to update whilst we are here - */ - return async(internal, favourite, modalBody) => { - const favouriteArea = modalBody.querySelector(selectors.render.favourites); - - // eslint-disable-next-line max-len - const favouriteButtons = modalBody.querySelectorAll(`[data-internal="${internal}"] ${selectors.actions.optionActions.manageFavourite}`); - const favouriteTabNav = modalBody.querySelector(selectors.regions.favouriteTabNav); - const result = moduleData.content_items.find(({name}) => name === internal); - const newFaves = {}; - if (result) { - if (favourite) { - result.favourite = true; - - // eslint-disable-next-line camelcase - newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true); - - const builtFaves = sectionMapper(newFaves, sectionnum); - - const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/favourites', - {favourites: builtFaves}); - - await Templates.replaceNodeContents(favouriteArea, html, js); - - Array.from(favouriteButtons).forEach((element) => { - element.classList.remove('text-muted'); - element.classList.add('text-primary'); - element.dataset.favourited = 'true'; - element.setAttribute('aria-pressed', true); - element.firstElementChild.classList.remove('fa-star-o'); - element.firstElementChild.classList.add('fa-star'); - }); - - favouriteTabNav.classList.remove('d-none'); - } else { - result.favourite = false; - - const nodeToRemove = favouriteArea.querySelector(`[data-internal="${internal}"]`); - - nodeToRemove.parentNode.removeChild(nodeToRemove); - - Array.from(favouriteButtons).forEach((element) => { - element.classList.add('text-muted'); - element.classList.remove('text-primary'); - element.dataset.favourited = 'false'; - element.setAttribute('aria-pressed', false); - element.firstElementChild.classList.remove('fa-star'); - element.firstElementChild.classList.add('fa-star-o'); - }); - const newFaves = moduleData.content_items.filter(mod => mod.favourite === true); - - if (newFaves.length === 0) { - nullFavouriteDomManager(favouriteTabNav, modalBody); - } - } - } - }; -}; +} diff --git a/course/amd/src/local/activitychooser/dialogue.js b/course/amd/src/local/activitychooser/dialogue.js index 618077db350..6b02ddef65b 100644 --- a/course/amd/src/local/activitychooser/dialogue.js +++ b/course/amd/src/local/activitychooser/dialogue.js @@ -21,483 +21,80 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import Carousel from 'theme_boost/bootstrap/carousel'; +// import {addIconToContainer} from 'core/loadingicon'; +import {debounce} from 'core/utils'; +import DialogueDom from 'core_course/local/activitychooser/dialoguedom'; +import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes'; +import Exporter from 'core_course/local/activitychooser/exporter'; +import {getFirst} from 'core/normalise'; +import {getString} from 'core/str'; +import Modal from 'core/modal'; import * as ModalEvents from 'core/modal_events'; +import Notification from 'core/notification'; +import * as Repository from 'core_course/local/activitychooser/repository'; import selectors from 'core_course/local/activitychooser/selectors'; import * as Templates from 'core/templates'; -import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes'; -import {addIconToContainer} from 'core/loadingicon'; -import * as Repository from 'core_course/local/activitychooser/repository'; -import Notification from 'core/notification'; -import {debounce} from 'core/utils'; -import {getFirst} from 'core/normalise'; const getPlugin = pluginName => import(pluginName); /** - * Given an event from the main module 'page' navigate to it's help section via a carousel. + * Display the activity chooser modal. * - * @method showModuleHelp - * @param {Element} carousel Our initialized carousel to manipulate - * @param {Object} moduleData Data of the module to carousel to - * @param {jQuery} modal We need to figure out if the current modal has a footer. + * @method displayActivityChooser + * @param {Promise} footerDataPromise Promise for the footer data. + * @param {Promise} modulesDataPromise Promise for the modules data. */ -const showModuleHelp = (carousel, moduleData, modal = null) => { - // If we have a real footer then we need to change temporarily. - if (modal !== null && moduleData.showFooter === true) { - modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData)); - } - const help = carousel.querySelector(selectors.regions.help); - help.innerHTML = ''; - help.classList.add('m-auto'); - - // Add a spinner. - const spinnerPromise = addIconToContainer(help); - - // Used later... - let transitionPromiseResolver = null; - const transitionPromise = new Promise(resolve => { - transitionPromiseResolver = resolve; +export async function displayActivityChooserModal( + footerDataPromise, + modulesDataPromise, +) { + // We want to show the modal instantly but loading whilst waiting for our data. + let bodyPromiseResolver; + const bodyPromise = new Promise(resolve => { + bodyPromiseResolver = resolve; }); - // Build up the html & js ready to place into the help section. - const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData); + const footerData = await footerDataPromise; - // Wait for the content to be ready, and for the transition to be complet. - Promise.all([contentPromise, spinnerPromise, transitionPromise]) - .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js)) - .then(() => { - help.querySelector(selectors.regions.chooserSummary.header).focus(); - return help; - }) - .catch(Notification.exception); - - // Move to the next slide, and resolve the transition promise when it's done. - carousel.addEventListener('slid.bs.carousel', () => { - transitionPromiseResolver(); - }, {once: true}); - // Trigger the transition between 'pages'. - Carousel.getInstance(carousel).next(); -}; - -/** - * Given a user wants to change the favourite state of a module we either add or remove the status. - * We also propergate this change across our map of modals. - * - * @method manageFavouriteState - * @param {HTMLElement} modalBody The DOM node of the modal to manipulate - * @param {HTMLElement} caller - * @param {Function} partialFavourite Partially applied function we need to manage favourite status - */ -const manageFavouriteState = async(modalBody, caller, partialFavourite) => { - const isFavourite = caller.dataset.favourited; - const id = caller.dataset.id; - const name = caller.dataset.name; - const internal = caller.dataset.internal; - // Switch on fave or not. - if (isFavourite === 'true') { - await Repository.unfavouriteModule(name, id); - - partialFavourite(internal, false, modalBody); - } else { - await Repository.favouriteModule(name, id); - - partialFavourite(internal, true, modalBody); - } - -}; - -/** - * Register chooser related event listeners. - * - * @method registerListenerEvents - * @param {Promise} modal Our modal that we are working with - * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object} - * @param {Function} partialFavourite Partially applied function we need to manage favourite status - * @param {Object} footerData Our base footer object. - */ -const registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => { - const modalBody = getFirst(modal.getBody()); - const bodyClickListener = async(e) => { - if (e.target.closest(selectors.actions.optionActions.showSummary)) { - const carousel = modalBody.querySelector(selectors.regions.carousel); - - const module = e.target.closest(selectors.regions.chooserOption.container); - const moduleName = module.dataset.modname; - const moduleData = mappedModules.get(moduleName); - // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer. - moduleData.showFooter = modal.hasFooterContent(); - showModuleHelp(carousel, moduleData, modal); - } - - if (e.target.closest(selectors.actions.optionActions.manageFavourite)) { - const caller = e.target.closest(selectors.actions.optionActions.manageFavourite); - await manageFavouriteState(modalBody, caller, partialFavourite); - const activeSectionId = modalBody.querySelector(selectors.elements.activetab).getAttribute("href"); - const sectionChooserOptions = modalBody - .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); - const firstChooserOption = sectionChooserOptions - .querySelector(selectors.regions.chooserOption.container); - toggleFocusableChooserOption(firstChooserOption, true); - initChooserOptionsKeyboardNavigation(modalBody, mappedModules, sectionChooserOptions, modal); - } - - // From the help screen go back to the module overview. - if (e.target.matches(selectors.actions.closeOption)) { - const carousel = modalBody.querySelector(selectors.regions.carousel); - - // Trigger the transition between 'pages'. - Carousel.getInstance(carousel).prev(); - carousel.addEventListener('slid.bs.carousel', () => { - const allModules = modalBody.querySelector(selectors.regions.modules); - const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname)); - caller.focus(); - }); - } - - // The "clear search" button is triggered. - if (e.target.closest(selectors.actions.clearSearch)) { - // Clear the entered search query in the search bar and hide the search results container. - const searchInput = modalBody.querySelector(selectors.actions.search); - searchInput.value = ""; - searchInput.focus(); - toggleSearchResultsView(modal, mappedModules, searchInput.value); - } - }; - - // We essentially have two types of footer. - // A fake one that is handled within the template for chooser_help and then all of the stuff for - // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we - // need to manage. The below code handles a real footer going to a mnet carousel item. - const footerClickListener = async(e) => { - if (footerData.footer === true) { - const footerjs = await getPlugin(footerData.customfooterjs); - await footerjs.footerClickListener(e, footerData, modal); - } - }; - - modal.getBodyPromise() - - // The return value of getBodyPromise is a jquery object containing the body NodeElement. - .then(body => body[0]) - - // Set up the carousel. - .then(body => { - const carousel = document.querySelector(selectors.regions.carousel); - new Carousel(carousel, { - interval: false, - pause: true, - keyboard: false - }); - - return body; - }) - - // Add the listener for clicks on the body. - .then(body => { - body.addEventListener('click', bodyClickListener); - return body; - }) - - // Add a listener for an input change in the activity chooser's search bar. - .then(body => { - const searchInput = body.querySelector(selectors.actions.search); - // The search input is triggered. - searchInput.addEventListener('input', debounce(() => { - // Display the search results. - toggleSearchResultsView(modal, mappedModules, searchInput.value); - }, 300)); - return body; - }) - - // Register event listeners related to the keyboard navigation controls. - .then(body => { - // Get the active chooser options section. - const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute("href"); - const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); - const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container); - - toggleFocusableChooserOption(firstChooserOption, true); - initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal); - - return body; - }) - .catch(Notification.exception); - - modal.getFooterPromise() - - // The return value of getBodyPromise is a jquery object containing the body NodeElement. - .then(footer => footer[0]) - // Add the listener for clicks on the footer. - .then(footer => { - footer.addEventListener('click', footerClickListener); - return footer; - }) - .catch(Notification.exception); -}; - -/** - * Initialise the keyboard navigation controls for the chooser options. - * - * @method initChooserOptionsKeyboardNavigation - * @param {HTMLElement} body Our modal that we are working with - * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object} - * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items - * @param {Object} modal Our created modal for the section - */ -const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => { - const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container); - - Array.from(chooserOptions).forEach((element) => { - return element.addEventListener('keydown', (e) => { - - // Check for enter/ space triggers for showing the help. - if (e.keyCode === enter || e.keyCode === space) { - if (e.target.matches(selectors.actions.optionActions.showSummary)) { - e.preventDefault(); - const module = e.target.closest(selectors.regions.chooserOption.container); - const moduleName = module.dataset.modname; - const moduleData = mappedModules.get(moduleName); - const carousel = document.querySelector(selectors.regions.carousel); - new Carousel({ - interval: false, - pause: true, - keyboard: false - }); - - // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer. - moduleData.showFooter = modal.hasFooterContent(); - showModuleHelp(carousel, moduleData, modal); - } - } - - // Next. - if (e.keyCode === arrowRight) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const nextOption = currentOption.nextElementSibling; - const firstOption = chooserOptionsContainer.firstElementChild; - const toFocusOption = clickErrorHandler(nextOption, firstOption); - focusChooserOption(toFocusOption, currentOption); - } - - // Previous. - if (e.keyCode === arrowLeft) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const previousOption = currentOption.previousElementSibling; - const lastOption = chooserOptionsContainer.lastElementChild; - const toFocusOption = clickErrorHandler(previousOption, lastOption); - focusChooserOption(toFocusOption, currentOption); - } - - if (e.keyCode === home) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const firstOption = chooserOptionsContainer.firstElementChild; - focusChooserOption(firstOption, currentOption); - } - - if (e.keyCode === end) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const lastOption = chooserOptionsContainer.lastElementChild; - focusChooserOption(lastOption, currentOption); - } - }); - }); -}; - -/** - * Focus on a chooser option element and remove the previous chooser element from the focus order - * - * @method focusChooserOption - * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus - * @param {HTMLElement|null} previousChooserOption The previous focused option element - */ -const focusChooserOption = (currentChooserOption, previousChooserOption = null) => { - if (previousChooserOption !== null) { - toggleFocusableChooserOption(previousChooserOption, false); - } - - toggleFocusableChooserOption(currentChooserOption, true); - currentChooserOption.focus(); -}; - -/** - * Add or remove a chooser option from the focus order. - * - * @method toggleFocusableChooserOption - * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order - * @param {Boolean} isFocusable Whether the chooser element is focusable or not - */ -const toggleFocusableChooserOption = (chooserOption, isFocusable) => { - const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser); - const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary); - const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite); - - if (isFocusable) { - // Set tabindex to 0 to add current chooser option element to the focus order. - chooserOption.tabIndex = 0; - chooserOptionLink.tabIndex = 0; - chooserOptionHelp.tabIndex = 0; - chooserOptionFavourite.tabIndex = 0; - } else { - // Set tabindex to -1 to remove the previous chooser option element from the focus order. - chooserOption.tabIndex = -1; - chooserOptionLink.tabIndex = -1; - chooserOptionHelp.tabIndex = -1; - chooserOptionFavourite.tabIndex = -1; - } -}; - -/** - * Small error handling function to make sure the navigated to object exists - * - * @method clickErrorHandler - * @param {HTMLElement} item What we want to check exists - * @param {HTMLElement} fallback If we dont match anything fallback the focus - * @return {HTMLElement} - */ -const clickErrorHandler = (item, fallback) => { - if (item !== null) { - return item; - } else { - return fallback; - } -}; - -/** - * Render the search results in a defined container - * - * @method renderSearchResults - * @param {HTMLElement} searchResultsContainer The container where the data should be rendered - * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria - */ -const renderSearchResults = async(searchResultsContainer, searchResultsData) => { - const templateData = { - 'searchresultsnumber': searchResultsData.length, - 'searchresults': searchResultsData - }; - // Build up the html & js ready to place into the help section. - const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData); - await Templates.replaceNodeContents(searchResultsContainer, html, js); -}; - -/** - * Toggle (display/hide) the search results depending on the value of the search query - * - * @method toggleSearchResultsView - * @param {Object} modal Our created modal for the section - * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object} - * @param {String} searchQuery The search query - */ -const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => { - const modalBody = modal.getBody()[0]; - const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults); - const chooserContainer = modalBody.querySelector(selectors.regions.chooser); - const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch); - - if (searchQuery.length > 0) { // Search query is present. - const searchResultsData = searchModules(mappedModules, searchQuery); - await renderSearchResults(searchResultsContainer, searchResultsData); - const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems); - const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container); - if (firstSearchResultItem) { - // Set the first result item to be focusable. - toggleFocusableChooserOption(firstSearchResultItem, true); - // Register keyboard events on the created search result items. - initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal); - } - // Display the "clear" search button in the activity chooser search bar. - clearSearchButton.classList.remove('d-none'); - // Hide the default chooser options container. - chooserContainer.setAttribute('hidden', 'hidden'); - // Display the search results container. - searchResultsContainer.removeAttribute('hidden'); - } else { // Search query is not present. - // Hide the "clear" search button in the activity chooser search bar. - clearSearchButton.classList.add('d-none'); - // Hide the search results container. - searchResultsContainer.setAttribute('hidden', 'hidden'); - // Display the default chooser options container. - chooserContainer.removeAttribute('hidden'); - } -}; - -/** - * Return the list of modules which have a name or description that matches the given search term. - * - * @method searchModules - * @param {Array} modules List of available modules - * @param {String} searchTerm The search term to match - * @return {Array} - */ -const searchModules = (modules, searchTerm) => { - if (searchTerm === '') { - return modules; - } - searchTerm = searchTerm.toLowerCase(); - const searchResults = []; - modules.forEach((activity) => { - const activityName = activity.title.toLowerCase(); - const activityDesc = activity.help.toLowerCase(); - if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) { - searchResults.push(activity); - } + const sectionModal = Modal.create({ + body: bodyPromise, + title: getString('addresourceoractivity'), + footer: footerData.customfootertemplate, + large: true, + scrollable: false, + templateContext: { + classes: 'modchooser' + }, + show: true, }); - return searchResults; -}; + try { + const modulesData = await modulesDataPromise; -/** - * Set up our tabindex information across the chooser. - * - * @method setupKeyboardAccessibility - * @param {Promise} modal Our created modal for the section - * @param {Map} mappedModules A map of all of the built module information - */ -const setupKeyboardAccessibility = (modal, mappedModules) => { - modal.getModal()[0].tabIndex = -1; + if (!modulesData) { + return; + } - modal.getBodyPromise().then(body => { - document.querySelectorAll(selectors.elements.tab).forEach((tab) => { - tab.addEventListener('shown.bs.tab', (e) => { - const activeSectionId = e.target.getAttribute("href"); - const activeSectionChooserOptions = body[0] - .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); - const firstChooserOption = activeSectionChooserOptions - .querySelector(selectors.regions.chooserOption.container); - const prevActiveSectionId = e.relatedTarget.getAttribute("href"); - const prevActiveSectionChooserOptions = body[0] - .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId)); + const modal = await sectionModal; + const dialogue = new ActivityChooserDialogue(modal, modulesData, footerData); - // Disable the focus of every chooser option in the previous active section. - disableFocusAllChooserOptions(prevActiveSectionChooserOptions); - // Enable the focus of the first chooser option in the current active section. - toggleFocusableChooserOption(firstChooserOption, true); - initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal); - }); - }); + const templateData = await dialogue.exporter.getModChooserTemplateData(modulesData); + bodyPromiseResolver(await Templates.render('core_course/activitychooser', templateData)); + } catch (error) { + const errorTemplateData = { + 'errormessage': error.message + }; + bodyPromiseResolver( + await Templates.render('core_course/local/activitychooser/error', errorTemplateData) + ); return; - }).catch(Notification.exception); -}; - -/** - * Disable the focus of all chooser options in a specific container (section). - * - * @method disableFocusAllChooserOptions - * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items - */ -const disableFocusAllChooserOptions = (sectionChooserOptions) => { - const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container); - allChooserOptions.forEach((chooserOption) => { - toggleFocusableChooserOption(chooserOption, false); - }); -}; + } +} /** * Display the module chooser. * + * @deprecated since Moodle 5.1 + * @todo Remove the method in Moodle 6.0 (MDL-85655). * @method displayChooser * @param {Promise} modalPromise Our created modal for the section * @param {Array} sectionModules An array of all of the built module information @@ -505,24 +102,340 @@ const disableFocusAllChooserOptions = (sectionChooserOptions) => { * @param {Object} footerData Our base footer object. */ export const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => { - // Make a map so we can quickly fetch a specific module's object for either rendering or searching. - const mappedModules = new Map(); - sectionModules.forEach((module) => { - mappedModules.set(module.componentname + '_' + module.link, module); - }); + window.console.warn( + 'The displayChooser function is deprecated. ' + + 'Please displayActivityChooserModal instead.' + ); // Register event listeners. modalPromise.then(modal => { - registerListenerEvents(modal, mappedModules, partialFavourite, footerData); - - // We want to focus on the first chooser option element as soon as the modal is opened. - setupKeyboardAccessibility(modal, mappedModules); - - // We want to focus on the action select when the dialog is closed. - modal.getRoot().on(ModalEvents.hidden, () => { - modal.destroy(); - }); - + new ActivityChooserDialogue(modal, sectionModules, footerData); return modal; }).catch(Notification.exception); }; + +/** + * Activity Chooser Dialogue class. + * + * @private + */ +class ActivityChooserDialogue { + /** + * Constructor for the ActivityChooserDialogue class. + * @param {Modal} modal The modal object. + * @param {Object} modulesData The data for the modules. + * @param {Object} footerData The data for the footer. + */ + constructor(modal, modulesData, footerData) { + this.modal = modal; + this.dialogueDom = null; // We cannot init until we have the modal body loaded. + this.footerData = footerData; + this.exporter = new Exporter(); + // This attribute marks when the tab content is dirty and needs to be refreshed when the user changes the tab. + // We don't want the content to be updated while the user is managing their favourites. + this.isFavouriteTabDirty = false; + // Make a map so we can quickly fetch a specific module's object for either rendering or searching. + this.mappedModules = new Map(); + modulesData.forEach((module) => { + this.mappedModules.set(module.componentname + '_' + module.link, module); + }); + this.init(); + } + + /** + * Initialise the activity chooser dialogue. + * + * @return {Promise} A promise that resolves when the modal is ready. + */ + async init() { + const modalBody = getFirst(await this.modal.getBodyPromise()); + this.dialogueDom = new DialogueDom(this, modalBody, this.exporter); + this.registerModalListenerEvents(); + this.setupKeyboardAccessibility(); + // We want to focus on the action select when the dialog is closed. + this.modal.getRoot().on(ModalEvents.hidden, () => { + this.modal.destroy(); + }); + } + + /** + * Register chooser related event listeners. + * + * @returns {Promise} A promise that resolves when events are registered + */ + async registerModalListenerEvents() { + const modalRoot = getFirst(this.modal.getRoot()); + + // Changing the tab should cancel any active search. + modalRoot.addEventListener( + 'shown.bs.tab', + (event) => { + // The all tab has the search result, so we do not want to clear the search input. + if (event.target.closest(selectors.regions.allTabNav)) { + return; + } + const searchInput = this.dialogueDom.getSearchInputElement(); + if (searchInput.value.length > 0) { + searchInput.value = ""; + this.toggleSearchResultsView(searchInput.value); + } + }, + ); + + // Add the listener for clicks on the full modal. + modalRoot.addEventListener( + 'click', + this.handleModalClick.bind(this), + ); + + // Add a listener for an input change in the activity chooser's search bar. + const searchInput = this.dialogueDom.getSearchInputElement(); + searchInput.addEventListener( + 'input', + debounce( + () => { + this.toggleSearchResultsView(searchInput.value); + }, + 300, + {pending: true}, + ), + ); + + this.dialogueDom.initBootstrapComponents(); + + // Handle focus when a new tab is shown. + modalRoot.addEventListener('shown.bs.tab', (event) => { + if (event.relatedTarget) { + this.dialogueDom.disableFocusAllChooserOptions(event.relatedTarget); + } + this.dialogueDom.initActiveTabNavigation(); + }); + + // Update the favourite tab content when the user changes the tab. + modalRoot.addEventListener('shown.bs.tab', () => { + if (this.isFavouriteTabDirty && !this.dialogueDom.isFavoutiteTabActive()) { + this.refreshFavouritesTabContent(); + } + }); + + this.dialogueDom.initActiveTabNavigation(); + + const modalFooter = getFirst(await this.modal.getFooterPromise()); + + // Add the listener for clicks on the footer. + modalFooter.addEventListener( + 'click', + this.handleFooterClick.bind(this), + ); + } + + /** + * Handle the click event on the footer of the modal. + * + * @param {Object} event The event object + * @return {Promise} A promise that resolves when the event is handled + */ + async handleFooterClick(event) { + if (this.footerData.footer === true) { + const footerjs = await getPlugin(this.footerData.customfooterjs); + await footerjs.footerClickListener(event, this.footerData, this.modal); + } + } + + /** + * Modal click handler. + * + * @param {Object} event The event object + * @return {Promise} A promise that resolves when the event is handled + */ + async handleModalClick(event) { + const target = event.target; + + if (target.closest(selectors.actions.optionActions.showSummary)) { + this.handleShowSummary(target); + } + + if (target.closest(selectors.actions.optionActions.manageFavourite)) { + await this.handleFavouriteClick(target); + } + + // From the help screen go back to the module overview. + if (target.matches(selectors.actions.closeOption)) { + this.dialogueDom.hideModuleHelp(target.dataset.modname); + } + + // The "clear search" button is triggered. + if (target.closest(selectors.actions.clearSearch)) { + this.handleClearSearch(); + } + } + + /** + * Show the summary of a module when the user clicks on the "show summary" button. + * + * @param {HTMLElement} target The target element that triggered the event + */ + handleShowSummary(target) { + const module = this.dialogueDom.getClosestChooserOption(target); + const moduleName = module.dataset.modname; + const moduleData = this.mappedModules.get(moduleName); + // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer. + moduleData.showFooter = this.modal.hasFooterContent(); + this.dialogueDom.showModuleHelp(moduleData, this.modal); + } + + /** + * Handle the favourite state of a module when the user clicks on the "starred" button. + * + * @param {HTMLElement} target The target element that triggered the event + * @return {Promise} A promise that resolves when the event is handled + */ + async handleFavouriteClick(target) { + const caller = target.closest(selectors.actions.optionActions.manageFavourite); + const id = caller.dataset.id; + const name = caller.dataset.name; + const internal = caller.dataset.internal; + const isFavourite = caller.dataset.favourited; + + // Switch on fave or not. + if (isFavourite === 'true') { + await Repository.unfavouriteModule(name, id); + this.updateFavouriteItemValue(internal, false); + } else { + await Repository.favouriteModule(name, id); + this.updateFavouriteItemValue(internal, true); + } + } + + /** + * Handle a clear search action. + */ + handleClearSearch() { + const searchInput = this.dialogueDom.getSearchInputElement(); + searchInput.value = ""; + searchInput.focus(); + this.toggleSearchResultsView(searchInput.value); + } + + /** + * Set up our tabindex information across the chooser. + * + * @method setupKeyboardAccessibility + */ + setupKeyboardAccessibility() { + const mainElement = getFirst(this.modal.getModal()); + + mainElement.tabIndex = -1; + + mainElement.addEventListener('keydown', (e) => { + const currentOption = this.dialogueDom.getClosestChooserOption(e.target); + if (currentOption === null) { + return; + } + + // Check for enter/ space triggers for showing the help. + if (e.keyCode === enter || e.keyCode === space) { + if (e.target.matches(selectors.actions.optionActions.showSummary)) { + e.preventDefault(); + this.handleShowSummary(e.target); + } + } + + if (e.keyCode === arrowRight) { + e.preventDefault(); + this.dialogueDom.focusNextChooserOption(currentOption); + } + if (e.keyCode === arrowLeft) { + e.preventDefault(); + this.dialogueDom.focusPreviousChooserOption(currentOption); + } + if (e.keyCode === home) { + e.preventDefault(); + this.dialogueDom.focusFirstChooserOption(currentOption); + } + if (e.keyCode === end) { + e.preventDefault(); + this.dialogueDom.focusLastChooserOption(currentOption); + } + }); + } + + /** + * Toggle (display/hide) the search results depending on the value of the search query + * + * @method toggleSearchResultsView + * @param {String} searchQuery The search query + */ + async toggleSearchResultsView(searchQuery) { + const searchResultsData = this.searchModules(searchQuery); + + if (searchQuery.length > 0) { + await this.dialogueDom.refreshSearchResults(searchResultsData); + this.dialogueDom.showAllActivitiesTab(true); + } else { + this.dialogueDom.cleanSearchResults(); + } + } + + /** + * Return the list of modules which have a name or description that matches the given search term. + * + * @method searchModules + * @param {String} searchTerm The search term to match + * @return {Array} + */ + searchModules(searchTerm) { + if (searchTerm === '') { + return this.mappedModules; + } + searchTerm = searchTerm.toLowerCase(); + const searchResults = []; + this.mappedModules.forEach((activity) => { + const activityName = activity.title.toLowerCase(); + const activityDesc = activity.help.toLowerCase(); + if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) { + searchResults.push(activity); + } + }); + + return searchResults; + } + + /** + * Update the favourite item value in the mapped modules. + * + * @param {String} internal The internal name of the module. + * @param {Boolean} favourite Whether the module is a favourite or not. + * @return {Promise} A promise that resolves when the item is updated. + */ + async updateFavouriteItemValue(internal, favourite) { + const moduleItem = this.mappedModules.find(({name}) => name === internal); + if (!moduleItem) { + return; + } + moduleItem.favourite = favourite; + + this.dialogueDom.updateItemStarredIcons(internal, favourite); + + if (this.dialogueDom.isFavoutiteTabActive()) { + this.isFavouriteTabDirty = true; + } else { + this.refreshFavouritesTabContent(); + } + } + + /** + * Refresh the favourites tab content. + * + * Note: this method will also hide the favourites tab if there are no favourite modules + * to keep the modal consistent. + * + * @return {Promise} A promise that resolves when the content is refreshed. + */ + async refreshFavouritesTabContent() { + this.isFavouriteTabDirty = false; + const favouriteCount = this.mappedModules.filter(mod => mod.favourite === true).size; + this.dialogueDom.toggleFavouriteTabDisplay(favouriteCount > 0); + await this.dialogueDom.refreshFavouritesTabContent(this.mappedModules); + } +} diff --git a/course/amd/src/local/activitychooser/dialoguedom.js b/course/amd/src/local/activitychooser/dialoguedom.js new file mode 100644 index 00000000000..174dcb13f1b --- /dev/null +++ b/course/amd/src/local/activitychooser/dialoguedom.js @@ -0,0 +1,479 @@ +// 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 . + +import {addIconToContainer} from 'core/loadingicon'; +import Carousel from 'theme_boost/bootstrap/carousel'; +import Notification from 'core/notification'; +import Pending from 'core/pending'; +import selectors from 'core_course/local/activitychooser/selectors'; +import Tab from 'theme_boost/bootstrap/tab'; +import * as Templates from 'core/templates'; + + +/** + * The activity changer dialogue DOM manipulation module. + * + * @module core_course/local/activitychooser/dialoguedom + * @copyright 2025 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +export default class ChooserDialogueDOM { + + constructor(dialogue, modalBody, exporter) { + this.modalBody = modalBody; + this.exporter = exporter; + // Temporal variable while migrating methods. + this.dialogue = dialogue; + } + + /** + * Get the search input element. + * + * @return {HTMLElement} The search input element. + */ + getSearchInputElement() { + return this.modalBody.querySelector(selectors.actions.search); + } + + /** + * Get the closest chooser option element. + * + * @param {HTMLElement} element + * @return {HTMLElement|null} element + */ + getClosestChooserOption(element) { + return element.closest(selectors.regions.chooserOption.container); + } + + /** + * Check if the search tab is active. + * + * @return {Boolean} True if the search tab is active, false otherwise. + */ + isFavoutiteTabActive() { + const favouriteTab = this.modalBody.querySelector(selectors.regions.favouriteTabNav); + return favouriteTab && favouriteTab.classList.contains('active'); + } + + /** + * Show the search results. + * + * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria + */ + async refreshSearchResults(searchResultsData) { + const searchResultsContainer = this.modalBody.querySelector(selectors.regions.searchResults); + const clearSearchButton = this.modalBody.querySelector(selectors.actions.clearSearch); + + await this.renderSearchResults(searchResultsContainer, searchResultsData); + const chooserOptionsContainer = searchResultsContainer.querySelector(selectors.regions.chooserOptions); + const firstSearchResultItem = chooserOptionsContainer.querySelector(selectors.regions.chooserOption.container); + if (firstSearchResultItem) { + // Set the first result item to be focusable. + this.toggleFocusableChooserOption(firstSearchResultItem, true); + // Register keyboard events on the created search result items. + } + clearSearchButton.classList.remove('d-none'); + + // Results are rendered in the all activities tab, so we need to hide the category content. + const tabContent = searchResultsContainer.closest(selectors.regions.tabContent); + const categoryContent = tabContent.querySelector(selectors.regions.categoryContent); + categoryContent.classList.add('d-none'); + } + + /** + * Clear the search results. + */ + cleanSearchResults() { + const searchResultsContainer = this.modalBody.querySelector(selectors.regions.searchResults); + const clearSearchButton = this.modalBody.querySelector(selectors.actions.clearSearch); + searchResultsContainer.innerHTML = ''; + clearSearchButton.classList.add('d-none'); + + // Results are rendered in the all activities tab, so we need to show the category content again. + const tabContent = searchResultsContainer.closest(selectors.regions.tabContent); + const categoryContent = tabContent.querySelector(selectors.regions.categoryContent); + categoryContent.classList.remove('d-none'); + } + + /** + * Render the search results in a defined container + * + * @private + * @method renderSearchResults + * @param {HTMLElement} searchResultsContainer The container where the data should be rendered + * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria + */ + async renderSearchResults(searchResultsContainer, searchResultsData) { + const templateData = this.exporter.getSearchResultData(searchResultsData); + // Build up the html & js ready to place into the help section. + const {html, js} = await Templates.renderForPromise( + 'core_course/local/activitychooser/search_results', + templateData + ); + await Templates.replaceNodeContents(searchResultsContainer, html, js); + } + + /** + * Show the "All activities" tab. + * + * @method showAllActivitiesTab + * @return {HTMLElement} The "All activities" tab element. + */ + showAllActivitiesTab() { + const navTab = this.modalBody.querySelector(selectors.regions.allTabNav); + + if (navTab.classList.contains('active')) { + return navTab; + } + + const pendingPromise = new Pending('core_course/activitychooser:alltab'); + navTab.addEventListener('shown.bs.tab', pendingPromise.resolve, {once: true}); + + Tab.getOrCreateInstance(navTab).show(); + return navTab; + } + + /** + * Update the starred icons in the chooser modal. + * + * @method updateItemStarredIcons + * @param {String} internal The internal name of the module. + * @param {Boolean} favourite Whether the module is a favourite or not. + */ + updateItemStarredIcons(internal, favourite) { + const favouriteButtons = this.modalBody.querySelectorAll( + `${selectors.elements.moduleItem(internal)} ${selectors.actions.optionActions.manageFavourite}` + ); + Array.from(favouriteButtons).forEach((element) => { + element.classList.toggle('text-muted', !favourite); + element.classList.toggle('text-primary', favourite); + element.dataset.favourited = favourite; + element.setAttribute('aria-pressed', favourite); + element.querySelector(selectors.elements.favouriteIconActive)?.classList.toggle('d-none', !favourite); + element.querySelector(selectors.elements.favouriteIconInactive)?.classList.toggle('d-none', favourite); + + const iconSelectsor = favourite ? selectors.elements.favouriteIconActive : selectors.elements.favouriteIconInactive; + const favouriteIcon = element.querySelector(iconSelectsor); + element.setAttribute('aria-label', favouriteIcon?.getAttribute('data-action-label') || ''); + }); + } + + /** + * Refresh the favourite content. + * + * @param {Array} mappedModules The modules to be displayed in the favourite tab. + */ + async refreshFavouritesTabContent(mappedModules) { + const templateData = await this.exporter.getFavouriteTabData(mappedModules); + const favouriteArea = this.modalBody.querySelector(selectors.regions.favouriteTab); + const {html, js} = await Templates.renderForPromise( + 'core_course/local/activitychooser/tabcontent', + templateData, + ); + await Templates.replaceNodeContents(favouriteArea, html, js); + } + + /** + * Toggle the display of the favourite tab. + * + * The favourite tab is only displayed when there are favourite modules + * or when it is the active tab. + * + * @param {Boolean} displayed Whether we want to show or hide the favourite tab + */ + toggleFavouriteTabDisplay(displayed) { + const favouriteTabNav = this.modalBody.querySelector(selectors.regions.favouriteTabNav); + + let moveFocusTo; + if (!displayed && favouriteTabNav.classList.contains('active')) { + moveFocusTo = this.showAllActivitiesTab(); + } + + favouriteTabNav?.classList.toggle('d-none', !displayed); + favouriteTabNav.tabIndex = displayed ? 0 : -1; + // The disabled class is used by Boostrap Tab for keyboard navigation. + if (displayed) { + favouriteTabNav.classList.remove('disabled'); + } else { + favouriteTabNav.classList.add('disabled'); + } + + if (moveFocusTo) { + moveFocusTo.focus(); + } + this.initActiveTabNavigation(); + } + + /** + * Given an event from the main module 'page' navigate to it's help section via a carousel. + * + * @method showModuleHelp + * @param {Object} moduleData Data of the module to carousel to + * @param {Modal} modal The modal object + */ + showModuleHelp(moduleData, modal) { + const carousel = this.modalBody.querySelector(selectors.regions.carousel); + // If we have a real footer then we need to change temporarily. + if (moduleData.showFooter === true) { + modal.setFooter(Templates.render( + 'core_course/local/activitychooser/footer_partial', + moduleData + )); + } + const help = carousel.querySelector(selectors.regions.help); + help.innerHTML = ''; + help.classList.add('m-auto'); + + // Add a spinner. + const spinnerPromise = addIconToContainer(help); + + // Used later... + let transitionPromiseResolver = null; + const transitionPromise = new Promise(resolve => { + transitionPromiseResolver = resolve; + }); + + // Build up the html & js ready to place into the help section. + const contentPromise = Templates.renderForPromise( + 'core_course/local/activitychooser/help', + moduleData + ); + + // Wait for the content to be ready, and for the transition to be complet. + Promise.all([contentPromise, spinnerPromise, transitionPromise]) + .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js)) + .then(() => { + help.querySelector(selectors.regions.chooserSummary.header).focus(); + return help; + }) + .catch(Notification.exception); + + // Move to the next slide, and resolve the transition promise when it's done. + carousel.addEventListener( + 'slid.bs.carousel', + () => { + transitionPromiseResolver(); + }, + {once: true} + ); + // Trigger the transition between 'pages'. + Carousel.getInstance(carousel).next(); + } + + /** + * Hide the help section of the chooser. + * + * @param {String|null} internal The internal name of the module to return to, if any. + */ + hideModuleHelp(internal = null) { + const carousel = this.modalBody.querySelector(selectors.regions.carousel); + // Trigger the transition between 'pages'. + Carousel.getInstance(carousel).prev(); + if (internal === null) { + return; + } + carousel.addEventListener( + 'slid.bs.carousel', + () => { + this.focusChooserOption(internal); + }, + {once: true} + ); + } + + /** + * Focus on a specific activity inside the active tab (if present). + * + * @private + * @method focusChooserOption + * @param {String} internal The internal name of the module. + */ + focusChooserOption(internal) { + const currentTabNav = this.modalBody.querySelector(selectors.elements.activetab); + const activeSectionId = currentTabNav.getAttribute("href"); + const sectionChooserOptions = this.modalBody.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); + const newCurrent = sectionChooserOptions.querySelector(selectors.regions.getModuleSelector(internal)); + + if (!newCurrent) { + throw new Error(`Invalid chooser option to focus on: ${internal}`); + } + + // Chooser can only have one element focusable at a time, so we disable them all first. + this.disableFocusAllChooserOptions(currentTabNav); + this.toggleFocusableChooserOption(newCurrent, true); + + // Little hack: we want the element considered a focus-visible element. + // But the focus method does not trigger the focus-visible class. There's an + // experimental "{focusVisible: true}" option in the focus method, but it's not + // supported in all browsers yet so we need to fake an editable element. + newCurrent.contentEditable = true; + newCurrent.focus(); + newCurrent.contentEditable = false; + } + + /** + * Initialise the active tab navigation. + */ + initActiveTabNavigation() { + const activeSectionId = this.modalBody.querySelector(selectors.elements.activetab).getAttribute("href"); + const sectionChooserOptions = this.modalBody.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); + const firstChooserOption = sectionChooserOptions?.querySelector(selectors.regions.chooserOption.container); + if (!firstChooserOption) { + return; + } + this.toggleFocusableChooserOption(firstChooserOption, true); + } + + /** + * Initialise all Boostrap components. + */ + initBootstrapComponents() { + this.modalBody.querySelectorAll(selectors.elements.tab).forEach((navTab) => { + // Init the Bootstrap Tab navigation. + Tab.getOrCreateInstance(navTab); + }); + + // Set up the carousel. + const carousel = this.modalBody.querySelector(selectors.regions.carousel); + new Carousel(carousel, { + interval: false, + pause: true, + keyboard: false + }); + } + + /** + * Disable the focus of all chooser options in a specific container (section). + * + * @method disableFocusAllChooserOptions + * @param {HTMLElement} tabNav The tab navigation element (from the shown.bs.ta event). + */ + disableFocusAllChooserOptions(tabNav) { + const tabId = tabNav.getAttribute("href"); + const chooserOptions = this.modalBody.querySelector( + selectors.regions.getSectionChooserOptions(tabId) + ); + + if (chooserOptions === null) { + return; + } + + const allChooserOptions = chooserOptions.querySelectorAll(selectors.regions.chooserOption.container); + allChooserOptions.forEach((chooserOption) => { + this.toggleFocusableChooserOption(chooserOption, false); + }); + } + + /** + * Add or remove a chooser option from the focus order. + * + * @private + * @method toggleFocusableChooserOption + * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order + * @param {Boolean} isFocusable Whether the chooser element is focusable or not + */ + toggleFocusableChooserOption(chooserOption, isFocusable) { + const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser); + const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary); + const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite); + + if (isFocusable) { + // Set tabindex to 0 to add current chooser option element to the focus order. + chooserOption.tabIndex = 0; + chooserOptionLink.tabIndex = 0; + chooserOptionHelp.tabIndex = 0; + chooserOptionFavourite.tabIndex = 0; + } else { + // Set tabindex to -1 to remove the previous chooser option element from the focus order. + chooserOption.tabIndex = -1; + chooserOptionLink.tabIndex = -1; + chooserOptionHelp.tabIndex = -1; + chooserOptionFavourite.tabIndex = -1; + } + } + + /** + * Move the focus to the previous chooser option element. + * + * @param {HTMLElement} current The current chooser option element + */ + focusNextChooserOption(current) { + this.moveChooserOptionFocus( + current, + (currentOption) => currentOption.nextElementSibling ?? currentOption, + ); + } + + /** + * Move the focus to the previous chooser option element. + * + * @param {HTMLElement} current The current chooser option element + */ + focusPreviousChooserOption(current) { + this.moveChooserOptionFocus( + current, + (currentOption) => currentOption.previousElementSibling ?? currentOption, + ); + } + + /** + * Move the focus to the first chooser option element. + * + * @param {HTMLElement} current The current chooser option element + */ + focusFirstChooserOption(current) { + this.moveChooserOptionFocus( + current, + (currentOption, container) => container.firstElementChild ?? currentOption, + ); + } + + /** + * Move the focus to the last chooser option element. + * + * @param {HTMLElement} current The current chooser option element + */ + focusLastChooserOption(current) { + this.moveChooserOptionFocus( + current, + (currentOption, container) => container.lastElementChild ?? currentOption, + ); + } + + /** + * Move the focus to the next chooser option element. + * + * @private + * @param {HTMLElement} current The current chooser option element + * @param {Function} getNextFocus Function to get the next focusable element + */ + moveChooserOptionFocus(current, getNextFocus) { + const currentOption = this.getClosestChooserOption(current); + const container = current.closest(selectors.regions.chooserOptions); + + if (!container || !currentOption) { + throw new Error('Invalid chooser options container or current option'); + } + + const newFocusOption = getNextFocus(currentOption, container); + if (!newFocusOption) { + return; + } + + this.toggleFocusableChooserOption(currentOption, false); + this.toggleFocusableChooserOption(newFocusOption, true); + newFocusOption.focus(); + } +} diff --git a/course/amd/src/local/activitychooser/exporter.js b/course/amd/src/local/activitychooser/exporter.js new file mode 100644 index 00000000000..94769dc4e4c --- /dev/null +++ b/course/amd/src/local/activitychooser/exporter.js @@ -0,0 +1,230 @@ +// 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 . + +/** + * Module to generate template data for the activity chooser. + * + * @module core_course/local/activitychooser/exporter + * @copyright 2025 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {getStrings} from 'core/str'; + +const activityCategories = ['activities', 'resources']; + +let allStrings = null; + +loadNecessaryStrings(); + +export default class { + /** + * A tab data structure. + * + * @typedef {object} TabData + * @property {String} tabId the tab ID + * @property {Boolean} active whether the tab is active or not + * @property {Array} items the filtered modules to be displayed in the tab + * @property {Boolean} displayed whether the tab is displayed or not + * @property {String} tabLabel the tab label + * @property {String|null} tabHelp the help text for the tab (optional) + */ + + /** + * Generate a tab data object for the activity chooser. + * + * @private + * @param {String} tabId Tab ID. + * @param {Array} filteredModules Filtered modules to be displayed in the tab. + * @param {String} tabLabel Tab label. + * @param {String|null} tabHelp Help text for the tab (optional). + * @param {Boolean} active Whether the tab is active or not. + * @return {TabData} Tab data object. + */ + getTabData(tabId, filteredModules, tabLabel, tabHelp = null, active = false) { + const result = { + tabId: tabId, + active: active, + items: filteredModules, + displayed: filteredModules.length > 0, + tabLabel, + }; + if (tabHelp) { + result.tabHelp = tabHelp; + } + return result; + } + + /** + * Normalise the modules data to be used in the chooser. + * + * The modulesData can be a plain array or a Map. This method will convert it to a + * plain array of objects. + * + * @param {Array|Map} modulesData Modules data to be used in the chooser. + * @return {Array} Normalised modules data. + */ + normaliseModulesData(modulesData) { + if (modulesData instanceof Map) { + modulesData = Array.from(modulesData.values()); + } else if (!Array.isArray(modulesData)) { + throw new Error('Invalid modules data format. Expected an array or a Map.'); + } + return modulesData; + } + + /** + * Fetch the chooser template data for a specific section. + * + * @param {Array|Map} modulesData Modules data to be used in the chooser. + * @return {Promise} Promise resolved with the template data. + */ + async getModChooserTemplateData(modulesData) { + modulesData = this.normaliseModulesData(modulesData); + const allStrings = await loadNecessaryStrings(); + const favouriteTab = await this.getFavouriteTabData(modulesData); + + const tabs = [ + { + ...this.getTabData( + 'all', + modulesData, + allStrings.all, + null, + !favouriteTab.displayed, + ), + hasSearchResults: true, // The all tab will also show search results. + }, + favouriteTab, + { + ...this.getTabData( + 'recommended', + modulesData.filter(mod => mod.recommended === true), + allStrings.recommended, + allStrings.recommended_help + ), + separator: true, // Add a separator before the purpose categories. + }, + ]; + + activityCategories.forEach((category, index) => { + const categoryModules = modulesData.filter(mod => mod.archetype == index); + if (categoryModules.length === 0) { + return; + } + tabs.push( + this.getTabData( + category, + categoryModules, + allStrings[category], + allStrings[category + '_help'] + ) + ); + }); + + return { + modules: modulesData, + tabs, + }; + } + + /** + * Get the favourite tab data. + * + * @param {Array|Map} modulesData Modules data to be used in the chooser. + * @return {Promise} Promise resolved with the template data. + */ + async getFavouriteTabData(modulesData) { + modulesData = this.normaliseModulesData(modulesData); + const allStrings = await loadNecessaryStrings(); + + // We need to deconstruct the modules data to ensure it is an array. + const favouriteModules = modulesData.filter( + mod => { + return mod.favourite === true; + } + ); + + return this.getTabData( + 'favourites', + favouriteModules, + allStrings.favourites, + null, + favouriteModules.length > 0, + ); + } + + /** + * Get the search result template data. + * + * @param {Array|Map} resultsModulesData Modules data to be used in the chooser. + * @return {Object} The template data. + */ + getSearchResultData(resultsModulesData) { + resultsModulesData = this.normaliseModulesData(resultsModulesData); + return { + 'searchresultsnumber': resultsModulesData.length, + 'searchresults': resultsModulesData, + }; + } + + /** + * Get the number of items in a tab. + * + * @param {TabData} tabData The tab data. + * @return {Number} The number of items in the tab. + */ + countTabItems(tabData) { + return tabData.items?.length ?? 0; + } + +} + +/** + * Load the necessary strings for the activity chooser. + * + * @return {Promise} Promise resolved with the loaded strings. + */ +async function loadNecessaryStrings() { + if (allStrings !== null) { + return allStrings; + } + allStrings = {}; + + const stringToLoad = [ + {key: 'all', component: 'core'}, + {key: 'favourites', component: 'core'}, + {key: 'recommended', component: 'core'}, + {key: 'recommended_help', component: 'core_course'}, + ...activityCategories.map( + (key) => ({ + key: key, + component: 'core', + }) + ), + ...activityCategories.map( + (key) => ({ + key: key + '_help', + component: 'core', + }) + ), + ]; + + const loadedStrings = await getStrings(stringToLoad); + stringToLoad.forEach(({key}, index) => { + allStrings[key] = loadedStrings[index]; + }); + return allStrings; +} diff --git a/course/amd/src/local/activitychooser/repository.js b/course/amd/src/local/activitychooser/repository.js index f79d7822f1f..29a3a5ddcec 100644 --- a/course/amd/src/local/activitychooser/repository.js +++ b/course/amd/src/local/activitychooser/repository.js @@ -22,6 +22,12 @@ */ import ajax from 'core/ajax'; +// A promises map for caching specific section modules data. +const sectionsModulesPromises = new Map(); + +// A promises map for caching a specific section footer data. +const sectionsFooterPromises = new Map(); + /** * Fetch all the information on modules we'll need in the activity chooser. * @@ -58,7 +64,12 @@ export const favouriteModule = (modName, modID) => { contentitemid: modID, }, }; - return ajax.call([request])[0]; + const promise = ajax.call([request])[0]; + // After the promise is resolved, we need to invalidate the cache for the section. + return promise.then(() => { + sectionsModulesPromises.clear(); + return true; + }); }; /** @@ -78,7 +89,13 @@ export const unfavouriteModule = (modName, modID) => { contentitemid: modID, }, }; - return ajax.call([request])[0]; + + const promise = ajax.call([request])[0]; + // After the promise is resolved, we need to invalidate the cache for the section. + return promise.then(() => { + sectionsModulesPromises.clear(); + return true; + }); }; /** @@ -99,3 +116,87 @@ export const fetchFooterData = (courseid, sectionid) => { }; return ajax.call([request])[0]; }; + +/** + * Fetch all the information on modules we'll need in the activity chooser. + * + * @method fetchSectionModules + * + * @private + * @param {Number} courseId Course ID. + * @param {Number} sectionNum Section number. + * @param {Number} sectionReturnNum Section return. + * @param {Number} beforeMod Before module number to be used in the module. + * @return {Object} Tab data. + */ +export async function getModulesData(courseId, sectionNum, sectionReturnNum, beforeMod) { + const cacheKey = `${courseId}-${sectionNum}`; + if (!sectionsModulesPromises.has(cacheKey)) { + sectionsModulesPromises.set( + cacheKey, + new Promise((resolve) => { + resolve(activityModules(courseId, sectionNum)); + }) + ); + } + + const moduleData = await sectionsModulesPromises.get(cacheKey); + + // Early return if there is no module data. + if (!moduleData) { + throw new Error('Cannot fetch module data'); + } + + // Apply the section num to all the module instance links. + return sectionMapper( + moduleData, + sectionNum, + sectionReturnNum, + beforeMod + ); +} + +/** + * Given the web service data and an ID we want to make a deep copy + * of the WS data then add on the section num to the addoption URL + * + * @method sectionMapper + * @param {Object} webServiceData Our original data from the Web service call + * @param {Number} sectionNum The number of the section we need to append to the links + * @param {Number|null} sectionReturnNum The number of the section return we need to append to the links + * @param {Number|null} beforeMod The ID of the cm we need to append to the links + * @return {Array} [modules] with URL's built + */ +function sectionMapper(webServiceData, sectionNum, sectionReturnNum, beforeMod) { + // We need to take a fresh deep copy of the original data as an object is a reference type. + const newData = JSON.parse(JSON.stringify(webServiceData)); + newData.content_items.forEach((module) => { + module.link += '§ion=' + sectionNum + '&beforemod=' + (beforeMod ?? 0); + if (sectionReturnNum) { + module.link += '&sr=' + sectionReturnNum; + } + }); + return newData.content_items; +} + +/** + * Fetch the footer data for a specific section. + * + * @param {Number} courseId Course ID. + * @param {Number} sectionNum Section number. + * @return {Promise} Promise resolved with the footer data. + */ +export async function getModalFooterData(courseId, sectionNum) { + const cacheKey = `${courseId}-${sectionNum}`; + if (sectionsFooterPromises.has(cacheKey)) { + return sectionsFooterPromises.get(cacheKey); + } + + sectionsFooterPromises.set( + cacheKey, + new Promise((resolve) => { + resolve(fetchFooterData(courseId, sectionNum)); + }) + ); + return sectionsFooterPromises.get(cacheKey); +} diff --git a/course/amd/src/local/activitychooser/selectors.js b/course/amd/src/local/activitychooser/selectors.js index a0856625cc5..9268878fff6 100644 --- a/course/amd/src/local/activitychooser/selectors.js +++ b/course/amd/src/local/activitychooser/selectors.js @@ -34,8 +34,9 @@ const getDataSelector = (name, value) => { export default { regions: { - chooser: getDataSelector('region', 'chooser-container'), + categoryContent: getDataSelector('region', 'category-content'), getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`, + chooserOptions: getDataSelector('region', 'chooser-options-container'), chooserOption: { container: getDataSelector('region', 'chooser-option-container'), actions: getDataSelector('region', 'chooser-option-actions-container'), @@ -49,18 +50,12 @@ export default { }, carousel: getDataSelector('region', 'carousel'), help: getDataSelector('region', 'help'), - modules: getDataSelector('region', 'modules'), - favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'), - defaultTabNav: getDataSelector('region', 'default-tab-nav'), - activityTabNav: getDataSelector('region', 'activity-tab-nav'), + tabContent: getDataSelector('region', 'tabcontent'), + favouriteTabNav: getDataSelector('region', 'favourites-tab-nav'), + allTabNav: getDataSelector('region', 'all-tab-nav'), favouriteTab: getDataSelector('region', 'favourites'), - recommendedTab: getDataSelector('region', 'recommended'), - defaultTab: getDataSelector('region', 'default'), - activityTab: getDataSelector('region', 'activity'), - resourceTab: getDataSelector('region', 'resources'), getModuleSelector: modname => `[role="menuitem"][data-modname="${modname}"]`, searchResults: getDataSelector('region', 'search-results-container'), - searchResultItems: getDataSelector('region', 'search-result-items-container'), }, actions: { optionActions: { @@ -73,16 +68,13 @@ export default { search: getDataSelector('action', 'search'), clearSearch: getDataSelector('action', 'clearsearch'), }, - render: { - favourites: getDataSelector('render', 'favourites-area'), - }, elements: { section: '.section', sectionmodchooser: 'button.section-modchooser-link', - sitemenu: '.block_site_main_menu', - sitetopic: 'div.sitetopic', tab: 'a[data-bs-toggle="tab"]', activetab: 'a[data-bs-toggle="tab"][aria-selected="true"]', - visibletabs: 'a[data-bs-toggle="tab"]:not(.d-none)' + moduleItem: item => `[data-internal="${item}"]`, + favouriteIconActive: '[data-icon="favourite-active"]', + favouriteIconInactive: '[data-icon="favourite-inactive"]', }, }; diff --git a/course/renderer.php b/course/renderer.php index 825ef688634..caa83360f57 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -174,9 +174,7 @@ class core_course_renderer extends plugin_renderer_base { } // Build an object of config settings that we can then hook into in the Activity Chooser. - $chooserconfig = (object) [ - 'tabmode' => get_config('core', 'activitychoosertabmode'), - ]; + $chooserconfig = (object) []; $this->page->requires->js_call_amd('core_course/activitychooser', 'init', [$courseid, $chooserconfig]); return ''; diff --git a/course/templates/activitychooser.mustache b/course/templates/activitychooser.mustache index 5f917099480..d901b185f16 100644 --- a/course/templates/activitychooser.mustache +++ b/course/templates/activitychooser.mustache @@ -46,132 +46,7 @@