diff --git a/course/amd/build/activitychooser.min.js b/course/amd/build/activitychooser.min.js index c341ef88217..65a0f2e7df3 100644 --- a/course/amd/build/activitychooser.min.js +++ b/course/amd/build/activitychooser.min.js @@ -5,6 +5,6 @@ define("core_course/activitychooser",["exports","core_course/local/activitychoos * @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),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=null,sectionid=null;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"),sectionid=sectionDiv.getAttribute("data-id")):(caller=button,caller.hasAttribute("data-sectionid")&&(window.console.warn("The data-sectionid attribute has been deprecated. Please update your code to use data-section-id passing the real section ID instead."),caller.setAttribute("data-sectionnum",caller.dataset.sectionid)),sectionnum=caller.dataset.sectionnum,sectionid=caller.getAttribute("data-section-id"));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,sectionid);ChooserDialogue.displayChooser(sectionModal,builtModuleData,partiallyAppliedFavouriteManager(data,sectionnum,sectionid),footerData),bodyPromiseResolver(await Templates.render("core_course/activitychooser",templateDataBuilder(builtModuleData,chooserConfig)))}}))})),initialized=!0},sectionMapper=function(webServiceData,num,sectionreturnnum,beforemod){let id=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;const newData=JSON.parse(JSON.stringify(webServiceData));let urlParams="&beforemod="+(null!=beforemod?beforemod:0);return id&&(urlParams+="§ionid=".concat(id)),urlParams+="§ion=".concat(num),sectionreturnnum&&(urlParams+="&sr=".concat(sectionreturnnum)),newData.content_items.forEach((module=>{module.link+=urlParams})),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}),nullFavouriteDomManager=(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);if(!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")}else{activitiesTabNav.classList.add("active"),activitiesTabNav.setAttribute("aria-selected","true"),activitiesTabNav.tabIndex=0,activitiesTabNav.focus();modalBody.querySelector(_selectors.default.regions.activityTab).classList.add("active")}}},partiallyAppliedFavouriteManager=function(moduleData,sectionnum){let sectionid=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;return 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,null,null,sectionid),{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&&nullFavouriteDomManager(favouriteTabNav,modalBody)}}}})); //# 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..83afaddbfeb 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":";;;;;;;ubA8CIA,aAAc,gBASE,CAACC,SAAUC,uBACrBC,eAAiB,IAAIC,iBAE3BC,uBAAuBJ,SAAUC,eAEjCC,eAAeG,iBAUbD,uBAAyB,CAACJ,SAAUC,oBAGlCF,yBAIEO,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,kBAGlBC,gBAAkB,UAChBC,cAAgB,IAAIC,WAEhBC,aACAF,cAAcG,IAAID,aAItBF,cAAcI,IACVF,WACA,IAAIG,SAASX,UACTA,QAAQY,WAAWC,gBAAgBlB,SAAUa,iBAN1CF,cAAcQ,IAAIN,cALb,GAkBlBO,gBAAkB,UAChBC,mBAAqB,YAEjBR,aACCQ,qBACDA,mBAAqB,IAAIL,SAASX,UAC9BA,QAAQY,WAAWG,gBAAgBpB,SAAUa,iBAI9CQ,qBAVS,sCAcXC,OAAOC,SAAUjB,QAG9BA,OAAOkB,SAASC,QACZF,SAASG,iBAAiBD,OAAOE,MAAAA,OACzBC,EAAEC,OAAOC,QAAQC,mBAAUC,SAASC,mBAAoB,KACpDC,OACAC,iBAGEC,WAAaR,EAAEC,OAAOC,QAAQC,mBAAUC,SAASK,SAEjDC,OAASV,EAAEC,OAAOC,QAAQC,mBAAUC,SAASC,uBAwB/CM,oBAlBe,OAAfH,YAAuBA,WAAWI,aAAa,gBAE/CN,OAASE,WACTD,WAAaC,WAAWK,aAAa,iBAErCP,OAASI,OAELJ,OAAOM,aAAa,oBACpBE,OAAOC,QAAQC,KACX,6GAGJV,OAAOW,aAAa,kBAAmBX,OAAOY,QAAQC,YAE1DZ,WAAaD,OAAOY,QAAQX,kBAK1Ba,YAAc,IAAIhC,SAAQX,UAC5BkC,oBAAsBlC,WAGpB4C,iBAAmB7B,gBAAgBe,YACnCe,aAAeC,WAAWH,YAAaC,YAIvCG,WAAa1C,gBAAgByB,YAAYkB,OAAM1B,MAAAA,UAC3C2B,kBAAoB,cACN1B,EAAE2B,SAEtBhB,0BAA0BiB,UAAUC,OAAO,0CAA2CH,2BAIrFF,kBAKCM,gBAAkBC,cACpBP,KACAjB,WACAD,OAAOY,QAAQc,iBACf1B,OAAOY,QAAQe,WAGnBC,gBAAgBC,eACZb,aACAQ,gBACAM,iCAAiCZ,KAAMjB,YACvCc,YAGJV,0BAA0BiB,UAAUC,OAChC,8BACAQ,oBAAoBP,gBAAiBzD,wBAMrDF,aAAc,GAcZ4D,cAAgB,CAACO,eAAgBC,IAAKP,iBAAkBC,mBAEpDO,QAAUC,KAAKC,MAAMD,KAAKE,UAAUL,wBAC1CE,QAAQI,cAAchD,SAASiD,SAC3BA,OAAOC,MAAQ,YAAcP,IAAM,eAAiBN,MAAAA,UAAAA,UAAa,GAC7DD,mBACAa,OAAOC,MAAQ,OAASd,qBAGzBQ,QAAQI,eAWbP,oBAAsB,CAACb,KAAMnD,qBAE3B0E,WAAa,GACbC,UAAY,GACZC,SAAU,EACVC,gBAAiB,EACjBC,eAAgB,QAGdC,QAAUC,SAAShF,cAAciF,SAGjCC,WAAa/B,KAAKgC,QAAOC,MAAyB,IAAlBA,IAAIC,YACpCC,YAAcnC,KAAKgC,QAAOC,MAA2B,IAApBA,IAAIE,cAGPP,CAAAA,UAQa,IAPrB,CApND,EAEG,EADN,EAGG,GAuNAQ,QAAQR,SAI/BS,CAA2BT,WAE3BL,WAAavB,KAAKgC,QAAOC,KAzNhB,IAyNuBA,IAAIK,YACpCd,UAAYxB,KAAKgC,QAAOC,KAzNf,IAyNsBA,IAAIK,YACnCZ,gBAAiB,EACjBC,eAAgB,EAnOI,IAsOhBC,SAnOmB,IAmOgBA,UACnCH,SAAU,UAUZc,sBAAmE,IAN3C,CA1OA,EACf,EACY,GA8OwBH,QAAQR,SAIrDY,kBAAoBT,WAAWU,OAC/BC,kBAAuC,IAApBF,kBAAsD,IAAzBD,wBAAmCJ,YAAYM,aAM9F,SACQzC,KACXyB,QAASA,QACTF,WAAYA,WACZG,eAAgBA,eAChBiB,iBATgC,IAAZlB,UAAyC,IAApBe,kBAAkD,IAArBE,iBAUtElB,UAAWA,UACXG,cAAeA,cACfI,WAAYA,WACZI,YAAaA,YACbO,iBAAkBA,iBAClBH,qBAAsBA,qBACtBC,gBAAiBA,gBACjBI,UAfyB,IAAZnB,UAAwC,IAApBe,kBAAkD,IAArBE,mBA2BhE3C,WAAa,CAAC8C,KAAMC,SAAWC,eAAMC,OAAO,CAC9CH,KAAAA,KACAI,OAAO,kBAAU,yBACjBH,OAAQA,OAAOI,qBACfC,OAAO,EACPC,YAAY,EACZC,gBAAiB,CACbC,QAAS,cAEbC,MAAM,IAkDJ3C,iCAAmC,CAAC4C,WAAYzE,aAQ3CR,MAAMkF,SAAUvB,UAAWwB,mBACxBC,cAAgBD,UAAUE,cAAcjF,mBAAU0B,OAAO0B,YAGzD8B,iBAAmBH,UAAUI,2CAAoCL,uBAAc9E,mBAAUoF,QAAQC,cAAcC,kBAC/GC,gBAAkBR,UAAUE,cAAcjF,mBAAUwF,QAAQD,iBAC5DE,OAASZ,WAAWpC,cAAciD,MAAKC,WAACC,KAACA,kBAAUA,OAASd,YAC5De,SAAW,MACbJ,UACIlC,UAAW,CACXkC,OAAOlC,WAAY,EAGnBsC,SAASpD,cAAgBoC,WAAWpC,cAAcY,QAAOC,MAAyB,IAAlBA,IAAIC,kBAE9DuC,WAAalE,cAAciE,SAAUzF,aAErC2F,KAACA,KAADC,GAAOA,UAAYvE,UAAUwE,iBAAiB,+CAChD,CAAC7C,WAAY0C,mBAEXrE,UAAUyE,oBAAoBlB,cAAee,KAAMC,IAEzDG,MAAMC,KAAKlB,kBAAkBzF,SAAS4G,UAClCA,QAAQC,UAAUC,OAAO,cACzBF,QAAQC,UAAUE,IAAI,gBACtBH,QAAQtF,QAAQ0F,WAAa,OAC7BJ,QAAQvF,aAAa,gBAAgB,GACrCuF,QAAQK,kBAAkBJ,UAAUC,OAAO,aAC3CF,QAAQK,kBAAkBJ,UAAUE,IAAI,cAG5CjB,gBAAgBe,UAAUC,OAAO,cAC9B,CACHd,OAAOlC,WAAY,QAEboD,aAAe3B,cAAcC,wCAAiCH,gBAEpE6B,aAAaC,WAAWC,YAAYF,cAEpCR,MAAMC,KAAKlB,kBAAkBzF,SAAS4G,UAClCA,QAAQC,UAAUE,IAAI,cACtBH,QAAQC,UAAUC,OAAO,gBACzBF,QAAQtF,QAAQ0F,WAAa,QAC7BJ,QAAQvF,aAAa,gBAAgB,GACrCuF,QAAQK,kBAAkBJ,UAAUC,OAAO,WAC3CF,QAAQK,kBAAkBJ,UAAUE,IAAI,gBAIpB,IAFP3B,WAAWpC,cAAcY,QAAOC,MAAyB,IAAlBA,IAAIC,YAE/CO,QAhGG,EAACyB,gBAAiBR,gBAC9CQ,gBAAgBuB,UAAY,EAC5BvB,gBAAgBe,UAAUE,IAAI,UAE1BjB,gBAAgBe,UAAUS,SAAS,UAAW,CAC9CxB,gBAAgBe,UAAUC,OAAO,UACjChB,gBAAgBzE,aAAa,gBAAiB,SACzBiE,UAAUE,cAAcjF,mBAAUwF,QAAQwB,cAClDV,UAAUC,OAAO,gBACxBU,cAAgBlC,UAAUE,cAAcjF,mBAAUwF,QAAQyB,eAC1DC,iBAAmBnC,UAAUE,cAAcjF,mBAAUwF,QAAQ2B,iBAChB,IAA/CF,cAAcX,UAAUS,SAAS,WACjCE,cAAcX,UAAUE,IAAI,UAC5BS,cAAcnG,aAAa,gBAAiB,QAC5CmG,cAAcH,SAAW,EACzBG,cAAcG,QACKrC,UAAUE,cAAcjF,mBAAUwF,QAAQ6B,YAClDf,UAAUE,IAAI,YAEzBU,iBAAiBZ,UAAUE,IAAI,UAC/BU,iBAAiBpG,aAAa,gBAAiB,QAC/CoG,iBAAiBJ,SAAW,EAC5BI,iBAAiBE,QACKrC,UAAUE,cAAcjF,mBAAUwF,QAAQ8B,aAClDhB,UAAUE,IAAI,aAyEpBe,CAAwBhC,gBAAiBR"} \ 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 * 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 = null;\n let sectionid = null;\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 sectionid = sectionDiv.getAttribute('data-id');\n } else {\n caller = button;\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-section-id passing the real section ID instead.'\n );\n caller.setAttribute('data-sectionnum', caller.dataset.sectionid);\n }\n sectionnum = caller.dataset.sectionnum;\n sectionid = caller.getAttribute('data-section-id');\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 sectionid\n );\n\n ChooserDialogue.displayChooser(\n sectionModal,\n builtModuleData,\n partiallyAppliedFavouriteManager(data, sectionnum, sectionid),\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 * @param {Number|null} id The number of the section we need to append to the links\n * @return {Array} [modules] with URL's built\n */\nconst sectionMapper = (webServiceData, num, sectionreturnnum, beforemod, id = null) => {\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 let urlParams = '&beforemod=' + (beforemod ?? 0);\n if (id) {\n urlParams += `§ionid=${id}`;\n }\n urlParams += `§ion=${num}`;\n if (sectionreturnnum) {\n urlParams += `&sr=${sectionreturnnum}`;\n }\n newData.content_items.forEach((module) => {\n module.link += urlParams;\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 * @param {Number|null} sectionid We need this to add the sectionid to the URL's in the faves area\n * Section ID is preferred over section number, as section numbers can change.\n * @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array\n */\nconst partiallyAppliedFavouriteManager = (moduleData, sectionnum, sectionid = null) => {\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, null, null, sectionid);\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","sectionid","sectionDiv","section","button","bodyPromiseResolver","hasAttribute","getAttribute","window","console","warn","setAttribute","dataset","bodyPromise","footerData","sectionModal","buildModal","data","catch","errorTemplateData","message","Templates","render","builtModuleData","sectionMapper","sectionreturnnum","beforemod","ChooserDialogue","displayChooser","partiallyAppliedFavouriteManager","templateDataBuilder","webServiceData","num","id","newData","JSON","parse","stringify","urlParams","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","nullFavouriteDomManager","favouriteTabNav","modalBody","tabIndex","classList","add","contains","remove","querySelector","regions","favouriteTab","defaultTabNav","activitiesTabNav","activityTabNav","focus","defaultTab","activityTab","moduleData","internal","favouriteArea","favouriteButtons","querySelectorAll","actions","optionActions","manageFavourite","result","find","_ref","name","newFaves","builtFaves","html","js","renderForPromise","replaceNodeContents","Array","from","element","favourited","firstElementChild","nodeToRemove","parentNode","removeChild"],"mappings":"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"} \ No newline at end of file diff --git a/course/amd/src/activitychooser.js b/course/amd/src/activitychooser.js index 1bc047ef9f0..ad32a5cbd3e 100644 --- a/course/amd/src/activitychooser.js +++ b/course/amd/src/activitychooser.js @@ -120,7 +120,8 @@ const registerListenerEvents = (courseId, chooserConfig) => { document.addEventListener(event, async(e) => { if (e.target.closest(selectors.elements.sectionmodchooser)) { let caller; - let sectionnum; + let sectionnum = null; + let sectionid = null; // 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); @@ -135,17 +136,18 @@ const registerListenerEvents = (courseId, chooserConfig) => { // We check for attributes just in case of outdated contrib course formats. caller = sectionDiv; sectionnum = sectionDiv.getAttribute('data-number'); + sectionid = sectionDiv.getAttribute('data-id'); } 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.' + 'Please update your code to use data-section-id passing the real section ID instead.' ); caller.setAttribute('data-sectionnum', caller.dataset.sectionid); } sectionnum = caller.dataset.sectionnum; + sectionid = caller.getAttribute('data-section-id'); } // We want to show the modal instantly but loading whilst waiting for our data. @@ -176,13 +178,14 @@ const registerListenerEvents = (courseId, chooserConfig) => { data, sectionnum, caller.dataset.sectionreturnnum, - caller.dataset.beforemod + caller.dataset.beforemod, + sectionid ); ChooserDialogue.displayChooser( sectionModal, builtModuleData, - partiallyAppliedFavouriteManager(data, sectionnum), + partiallyAppliedFavouriteManager(data, sectionnum, sectionid), footerData, ); @@ -206,16 +209,22 @@ const registerListenerEvents = (courseId, chooserConfig) => { * @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 + * @param {Number|null} id The number of the section we need to append to the links * @return {Array} [modules] with URL's built */ -const sectionMapper = (webServiceData, num, sectionreturnnum, beforemod) => { +const sectionMapper = (webServiceData, num, sectionreturnnum, beforemod, id = null) => { // 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)); + let urlParams = '&beforemod=' + (beforemod ?? 0); + if (id) { + urlParams += `§ionid=${id}`; + } + urlParams += `§ion=${num}`; + if (sectionreturnnum) { + urlParams += `&sr=${sectionreturnnum}`; + } newData.content_items.forEach((module) => { - module.link += '§ion=' + num + '&beforemod=' + (beforemod ?? 0); - if (sectionreturnnum) { - module.link += '&sr=' + sectionreturnnum; - } + module.link += urlParams; }); return newData.content_items; }; @@ -368,9 +377,11 @@ const nullFavouriteDomManager = (favouriteTabNav, modalBody) => { * @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 + * @param {Number|null} sectionid We need this to add the sectionid to the URL's in the faves area + * Section ID is preferred over section number, as section numbers can change. * @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array */ -const partiallyAppliedFavouriteManager = (moduleData, sectionnum) => { +const partiallyAppliedFavouriteManager = (moduleData, sectionnum, sectionid = null) => { /** * Curried function that is being returned. * @@ -393,7 +404,7 @@ const partiallyAppliedFavouriteManager = (moduleData, sectionnum) => { // eslint-disable-next-line camelcase newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true); - const builtFaves = sectionMapper(newFaves, sectionnum); + const builtFaves = sectionMapper(newFaves, sectionnum, null, null, sectionid); const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/favourites', {favourites: builtFaves}); diff --git a/course/classes/output/activitychooserbutton.php b/course/classes/output/activitychooserbutton.php index 1255dbc6be4..58b373ba81a 100644 --- a/course/classes/output/activitychooserbutton.php +++ b/course/classes/output/activitychooserbutton.php @@ -72,7 +72,9 @@ class activitychooserbutton implements templatable, renderable { ); return (object)[ - 'sectionnum' => $this->section->section, + // We keep the old sectionnum properties for backwards compatibility. + 'sectionnum' => $this->section->sectionnum, + 'sectionid' => $this->section->id, 'sectionname' => get_section_name($this->section->course, $this->section), 'sectionreturn' => $this->sectionreturn ?? false, 'modid' => $this->mod ? $this->mod->id : false, diff --git a/course/mod.php b/course/mod.php index fffc8530ab5..2e0e6fbf9a3 100644 --- a/course/mod.php +++ b/course/mod.php @@ -76,16 +76,22 @@ require_login(); //check if we are adding / editing a module that has new forms using formslib if (!empty($add)) { $id = required_param('id', PARAM_INT); - $section = required_param('section', PARAM_INT); + $sectionid = optional_param('sectionid', null, PARAM_INT); $type = optional_param('type', '', PARAM_ALPHA); $returntomod = optional_param('return', 0, PARAM_BOOL); $beforemod = optional_param('beforemod', 0, PARAM_INT); + if (empty($sectionid)) { + $section = required_param('section', PARAM_INT); + $sectioninfo = get_fast_modinfo($id)->get_section_info($section); + $sectionid = $sectioninfo?->id; + } + $params = [ 'add' => $add, 'type' => $type, 'course' => $id, - 'section' => $section, + 'sectionid' => $sectionid, 'return' => $returntomod, 'beforemod' => $beforemod, ]; diff --git a/course/modedit.php b/course/modedit.php index 087a1379a37..5d38b3aa56f 100644 --- a/course/modedit.php +++ b/course/modedit.php @@ -56,11 +56,17 @@ if (!empty($showonly)) { } if (!empty($add)) { - $section = required_param('section', PARAM_INT); $course = required_param('course', PARAM_INT); + $sectionid = optional_param('sectionid', null, PARAM_INT); + if (empty($sectionid)) { + $sectionnum = required_param('section', PARAM_INT); + } else { + $sectionnum = get_fast_modinfo($course)->get_section_info_by_id($sectionid, MUST_EXIST)->sectionnum; + } + $url->param('add', $add); - $url->param('section', $section); + $url->param('section', $sectionnum); $url->param('course', $course); $PAGE->set_url($url); @@ -70,18 +76,18 @@ if (!empty($add)) { // There is no page for this in the navigation. The closest we'll have is the course section. // If the course section isn't displayed on the navigation this will fall back to the course which // will be the closest match we have. - navigation_node::override_active_url(course_get_url($course, $section)); + navigation_node::override_active_url(course_get_url($course, $sectionnum)); - // MDL-69431 Validate that $section (url param) does not exceed the maximum for this course / format. + // MDL-69431 Validate that $sectionnum (from url param) does not exceed the maximum for this course / format. // If too high (e.g. section *id* not number) non-sequential sections inserted in course_sections table. // Then on import, backup fills 'gap' with empty sections (see restore_rebuild_course_cache). Avoid this. $courseformat = course_get_format($course); $maxsections = $courseformat->get_max_sections(); - if ($section > $maxsections) { + if ($sectionnum > $maxsections) { throw new \moodle_exception('maxsectionslimit', 'moodle', '', $maxsections); } - list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $add, $section); + [$module, $context, $cw, $cm, $data] = prepare_new_moduleinfo_data($course, $add, $sectionnum); $data->return = 0; if (!is_null($sectionreturn)) { $data->sr = $sectionreturn; diff --git a/course/templates/addresourceoractivitybutton.mustache b/course/templates/addresourceoractivitybutton.mustache index fcdcfeb86d4..449f8aaa230 100644 --- a/course/templates/addresourceoractivitybutton.mustache +++ b/course/templates/addresourceoractivitybutton.mustache @@ -22,6 +22,7 @@ Example context (json): { "sectionnum": 0, + "sectionid": 0, "modid": 1, "activityname": "Activity example", "sectionreturn": 0 @@ -32,6 +33,11 @@ }}{{#hasactionlinks}}dropdown-item{{/hasactionlinks}}" data-action="open-chooser" data-sectionnum="{{sectionnum}}" + {{! + data-sectionid was wrongly used for the section number in Moodle 4.4, + so we use data-section-id instead for now. + }} + {{#sectionid}}data-section-id="{{.}}"{{/sectionid}} {{#sectionreturn}}data-sectionreturnnum="{{.}}"{{/sectionreturn}} {{#modid}} data-beforemod="{{modid}}"