MDL-85597 course: add selection to activity chooser

This commit is contained in:
ferran
2025-07-21 16:58:43 +02:00
parent ef6df9e70e
commit f54d7eefcd
19 changed files with 276 additions and 51 deletions
+1 -1
View File
@@ -12,6 +12,6 @@
* @copyright 2020 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("tool_moodlenet/instance_form",["tool_moodlenet/validator","tool_moodlenet/selectors","core/loadingicon","core/templates","core/notification","jquery","theme_boost/bootstrap/carousel","core/normalise"],(function(Validator,Selectors,LoadingIcon,Templates,Notification,$,Carousel,Normalise){var chooserNavigateToMnet=function(showMoodleNet,footerData,carousel,modal){showMoodleNet.innerHTML="";var page,spinnerPromise=LoadingIcon.addIconToContainer(showMoodleNet),transitionPromiseResolver=null,transitionPromise=new Promise((resolve=>{transitionPromiseResolver=resolve}));$.when(spinnerPromise,transitionPromise).then((function(){Templates.replaceNodeContents(showMoodleNet,footerData.customcarouseltemplate,"")})).catch(Notification.exception),(page=showMoodleNet).addEventListener("click",(function(e){if(e.target.matches(Selectors.action.submit)){var input=page.querySelector('[data-var="mnet-link"]'),overlay=page.querySelector(Selectors.region.spinner),validationArea=document.querySelector(Selectors.region.validationArea);overlay.classList.remove("d-none");var spinner=LoadingIcon.addIconToContainerWithPromise(overlay);Validator.validation(input).then((function(result){spinner.resolve(),overlay.classList.add("d-none"),result.result?(input.classList.remove("is-invalid"),input.classList.add("is-valid"),validationArea.innerText=result.message,validationArea.classList.remove("text-danger"),validationArea.classList.add("text-success"),setTimeout((function(){window.location=result.domain}),1e3)):(input.classList.add("is-invalid"),validationArea.innerText=result.message,validationArea.classList.add("text-danger"))})).catch(Notification.exception)}})),carousel.addEventListener("slid.bs.carousel",(function(){transitionPromiseResolver()}),{once:!0}),Carousel.getInstance(carousel).to(2),modal.setFooter(Templates.render("tool_moodlenet/chooser_footer_close_mnet",{}))};return{footerClickListener:function(e,footerData,modal){const modalBody=Normalise.getFirst(modal.getBody());if(e.target.matches(Selectors.action.showMoodleNet)||e.target.closest(Selectors.action.showMoodleNet)){e.preventDefault();const carousel=modalBody.querySelector(Selectors.region.carousel),showMoodleNet=carousel.querySelector(Selectors.region.moodleNet);chooserNavigateToMnet(showMoodleNet,footerData,carousel,modal)}if(e.target.matches(Selectors.action.closeOption)){!function(carousel,modal,footerData){Carousel.getInstance(carousel).to(0),modal.setFooter(footerData.customfootertemplate)}(modalBody.querySelector(Selectors.region.carousel),modal,footerData)}}}}));
define("tool_moodlenet/instance_form",["tool_moodlenet/validator","tool_moodlenet/selectors","core/loadingicon","core/templates","core/notification","jquery","theme_boost/bootstrap/carousel","core/normalise"],(function(Validator,Selectors,LoadingIcon,Templates,Notification,$,Carousel,Normalise){var chooserNavigateToMnet=function(showMoodleNet,footerData,carousel){showMoodleNet.innerHTML="";var page,spinnerPromise=LoadingIcon.addIconToContainer(showMoodleNet),transitionPromiseResolver=null,transitionPromise=new Promise((resolve=>{transitionPromiseResolver=resolve}));$.when(spinnerPromise,transitionPromise).then((function(){Templates.replaceNodeContents(showMoodleNet,footerData.customcarouseltemplate,"")})).catch(Notification.exception),(page=showMoodleNet).addEventListener("click",(function(e){if(e.target.matches(Selectors.action.submit)){var input=page.querySelector('[data-var="mnet-link"]'),overlay=page.querySelector(Selectors.region.spinner),validationArea=document.querySelector(Selectors.region.validationArea);overlay.classList.remove("d-none");var spinner=LoadingIcon.addIconToContainerWithPromise(overlay);Validator.validation(input).then((function(result){spinner.resolve(),overlay.classList.add("d-none"),result.result?(input.classList.remove("is-invalid"),input.classList.add("is-valid"),validationArea.innerText=result.message,validationArea.classList.remove("text-danger"),validationArea.classList.add("text-success"),setTimeout((function(){window.location=result.domain}),1e3)):(input.classList.add("is-invalid"),validationArea.innerText=result.message,validationArea.classList.add("text-danger"))})).catch(Notification.exception)}})),carousel.addEventListener("slid.bs.carousel",(function(){transitionPromiseResolver()}),{once:!0}),Carousel.getInstance(carousel).to(2)};return{footerClickListener:function(e,footerData,modal){const modalBody=Normalise.getFirst(modal.getBody());if(e.target.matches(Selectors.action.showMoodleNet)||e.target.closest(Selectors.action.showMoodleNet)){e.preventDefault();const carousel=modalBody.querySelector(Selectors.region.carousel),showMoodleNet=carousel.querySelector(Selectors.region.moodleNet);chooserNavigateToMnet(showMoodleNet,footerData,carousel)}if(e.target.matches(Selectors.action.closeOption)){!function(carousel,modal,footerData){Carousel.getInstance(carousel).to(0),modal.setFooter(footerData.customfootertemplate)}(modalBody.querySelector(Selectors.region.carousel),modal,footerData)}}}}));
//# sourceMappingURL=instance_form.min.js.map
File diff suppressed because one or more lines are too long
@@ -94,9 +94,8 @@ define(['tool_moodlenet/validator',
* @param {HTMLElement} showMoodleNet The chooser's area for ment
* @param {Object} footerData Our footer object to render out
* @param {Element} carousel Our carousel instance to manage
* @param {jQuery} modal Our modal instance to manage
*/
var chooserNavigateToMnet = function(showMoodleNet, footerData, carousel, modal) {
var chooserNavigateToMnet = function(showMoodleNet, footerData, carousel) {
showMoodleNet.innerHTML = '';
// Add a spinner.
@@ -125,7 +124,6 @@ define(['tool_moodlenet/validator',
}, {once: true});
// Trigger the transition between 'pages'.
Carousel.getInstance(carousel).to(2);
modal.setFooter(Templates.render('tool_moodlenet/chooser_footer_close_mnet', {}));
};
/**
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,6 +6,6 @@ define("core_course/local/activitychooser/exporter",["exports","core/str"],(func
* @copyright 2025 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const activityCategories=["administration","assessment","collaboration","communication","content","interactivecontent"];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:"mod_purpose_"+key,component:"core_course"}))),...activityCategories.map((key=>({key:"mod_purpose_"+key+"_help",component:"core_course"})))],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=>{const categoryModules=modulesData.filter((mod=>mod.purpose==category));0!==categoryModules.length&&tabs.push(this.getTabData(category,categoryModules,allStrings["mod_purpose_"+category],allStrings["mod_purpose_"+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}));
const activityCategories=["administration","assessment","collaboration","communication","content","interactivecontent"];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:"mod_purpose_"+key,component:"core_course"}))),...activityCategories.map((key=>({key:"mod_purpose_"+key+"_help",component:"core_course"})))],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=>{const categoryModules=modulesData.filter((mod=>mod.purpose==category));0!==categoryModules.length&&tabs.push(this.getTabData(category,categoryModules,allStrings["mod_purpose_"+category],allStrings["mod_purpose_"+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}getFooterData(footerData){return{activeFooter:footerData.customfootertemplate}}},_exports.default}));
//# sourceMappingURL=exporter.min.js.map
File diff suppressed because one or more lines are too long
@@ -6,6 +6,6 @@ define("core_course/local/activitychooser/selectors",["exports"],(function(_expo
* @copyright 2019 Mathew May <mathew.solutions>
* @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:{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}));
const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{activeFooter:getDataSelector("region","active-footer-container"),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"),addSelectedChooserOption:getDataSelector("action","add-selected-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
File diff suppressed because one or more lines are too long
@@ -53,12 +53,19 @@ export async function displayActivityChooserModal(
bodyPromiseResolver = resolve;
});
const exporter = new Exporter();
const footerData = await footerDataPromise;
const footerPromise = Templates.render(
'core_course/local/activitychooser/footer',
exporter.getFooterData(footerData),
);
const sectionModal = Modal.create({
body: bodyPromise,
title: getString('addresourceoractivity'),
footer: footerData.customfootertemplate,
footer: footerPromise,
large: true,
scrollable: false,
templateContext: {
@@ -75,9 +82,9 @@ export async function displayActivityChooserModal(
}
const modal = await sectionModal;
const dialogue = new ActivityChooserDialogue(modal, modulesData, footerData);
new ActivityChooserDialogue(modal, modulesData, footerData);
const templateData = await dialogue.exporter.getModChooserTemplateData(modulesData);
const templateData = await exporter.getModChooserTemplateData(modulesData);
bodyPromiseResolver(await Templates.render('core_course/activitychooser', templateData));
} catch (error) {
const errorTemplateData = {
@@ -131,6 +138,7 @@ class ActivityChooserDialogue {
this.dialogueDom = null; // We cannot init until we have the modal body loaded.
this.footerData = footerData;
this.exporter = new Exporter();
this.selectedModule = null;
// 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;
@@ -148,14 +156,17 @@ class ActivityChooserDialogue {
* @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);
await this.modal.getBodyPromise();
await this.modal.getFooterPromise();
this.dialogueDom = new DialogueDom(this, this.modal, 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();
});
// When modal is open the add button should be disabled because there is no activity selected.
this.dialogueDom.unmarkAllChooserOptionAsSelected();
}
/**
@@ -170,6 +181,7 @@ class ActivityChooserDialogue {
modalRoot.addEventListener(
'shown.bs.tab',
(event) => {
this.dialogueDom.unmarkAllChooserOptionAsSelected();
// 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;
@@ -187,6 +199,10 @@ class ActivityChooserDialogue {
'click',
this.handleModalClick.bind(this),
);
modalRoot.addEventListener(
'dblclick',
this.handleModalDoubleClick.bind(this),
);
// Add a listener for an input change in the activity chooser's search bar.
const searchInput = this.dialogueDom.getSearchInputElement();
@@ -227,6 +243,19 @@ class ActivityChooserDialogue {
'click',
this.handleFooterClick.bind(this),
);
// Adapt modal footer depending on the displayed carousel page.
modalRoot.addEventListener('slide.bs.carousel', (event) => {
if (event.to === undefined) {
return;
}
// The boostrap carousel event.to contains the index of the newly active item.
// The zero index is the chooser options, the first index is the module help,
// any other index are custom footer pages.
this.dialogueDom.toggleActiveFooter(event.to === 0);
this.dialogueDom.toggleBackButton(event.to !== 0);
this.dialogueDom.toggleAddButton(event.to < 2);
});
}
/**
@@ -236,7 +265,14 @@ class ActivityChooserDialogue {
* @return {Promise} A promise that resolves when the event is handled
*/
async handleFooterClick(event) {
if (this.footerData.footer === true) {
if (event.target.closest(selectors.actions.addSelectedChooserOption)) {
this.submitAddSelectedModule();
}
if (
event.target.closest(selectors.regions.activeFooter)
&& this.footerData.footer === true
) {
const footerjs = await getPlugin(this.footerData.customfooterjs);
await footerjs.footerClickListener(event, this.footerData, this.modal);
}
@@ -253,20 +289,41 @@ class ActivityChooserDialogue {
if (target.closest(selectors.actions.optionActions.showSummary)) {
this.handleShowSummary(target);
return;
}
if (target.closest(selectors.actions.optionActions.manageFavourite)) {
await this.handleFavouriteClick(target);
return;
}
// From the help screen go back to the module overview.
if (target.matches(selectors.actions.closeOption)) {
this.dialogueDom.hideModuleHelp(target.dataset.modname);
return;
}
// The "clear search" button is triggered.
if (target.closest(selectors.actions.clearSearch)) {
this.handleClearSearch();
return;
}
if (target.closest(selectors.regions.chooserOption.container)) {
event.preventDefault();
this.handleOptionSelection(target);
return;
}
}
/**
* Handle the double click event on the modal.
* @param {Object} event The event object
*/
handleModalDoubleClick(event) {
const option = this.dialogueDom.getClosestChooserOption(event.target);
if (option !== null) {
this.submitAddSelectedModule(this.selectedModule);
}
}
@@ -279,8 +336,11 @@ class ActivityChooserDialogue {
const module = this.dialogueDom.getClosestChooserOption(target);
const moduleName = module.dataset.modname;
const moduleData = this.mappedModules.get(moduleName);
// We select the module now. This way the back button will keep the module selected.
this.handleOptionSelection(target);
// 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.setBackButtonModuleData(moduleData);
this.dialogueDom.showModuleHelp(moduleData, this.modal);
}
@@ -317,6 +377,38 @@ class ActivityChooserDialogue {
this.toggleSearchResultsView(searchInput.value);
}
/**
* Handle the click on a chooser option.
*
* @param {HTMLElement} target The target element that triggered the event
*/
handleOptionSelection(target) {
const option = this.dialogueDom.getClosestChooserOption(target);
if (option === null) {
return;
}
this.selectedModule = option;
this.dialogueDom.markChooserOptionAsSelected(option, getFirst(this.modal.getFooter()));
}
/**
* Submit the selected module to the chooser.
*
* This method will redirect the user to the URL of the selected module, if one is selected.
*
* @param {HTMLElement} newSelectedModule optional new selected module element.
* @return {void}
*/
submitAddSelectedModule(newSelectedModule = null) {
if (newSelectedModule) {
this.handleOptionSelection(newSelectedModule);
}
if (this.selectedModule === null) {
return;
}
window.location.href = this.dialogueDom.getChooserOptionUrl(this.selectedModule);
}
/**
* Set up our tabindex information across the chooser.
*
@@ -333,14 +425,22 @@ class ActivityChooserDialogue {
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);
// Check first if the target is an internal control button (favourite or help).
// If that is the case, the regular click will handle the event.
if (e.target.closest(selectors.regions.chooserOption.actions)) {
return;
}
}
if (e.keyCode === space) {
e.preventDefault();
this.handleOptionSelection(currentOption);
}
if (e.keyCode === enter) {
e.preventDefault();
this.submitAddSelectedModule(currentOption);
}
if (e.keyCode === arrowRight) {
e.preventDefault();
this.dialogueDom.focusNextChooserOption(currentOption);
@@ -15,6 +15,7 @@
import {addIconToContainer} from 'core/loadingicon';
import Carousel from 'theme_boost/bootstrap/carousel';
import {getFirst} from 'core/normalise';
import Notification from 'core/notification';
import Pending from 'core/pending';
import selectors from 'core_course/local/activitychooser/selectors';
@@ -30,9 +31,16 @@ import * as Templates from 'core/templates';
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default class ChooserDialogueDOM {
constructor(dialogue, modalBody, exporter) {
this.modalBody = modalBody;
/**
* ChooserDialogueDOM constructor.
*
* @param {Object} dialogue The dialogue object.
* @param {Modal} modal The modal object.
* @param {Object} exporter The exporter object to generate template data.
*/
constructor(dialogue, modal, exporter) {
this.modal = modal;
this.modalBody = getFirst(this.modal.getBody());
this.exporter = exporter;
// Temporal variable while migrating methods.
this.dialogue = dialogue;
@@ -67,6 +75,21 @@ export default class ChooserDialogueDOM {
return favouriteTab && favouriteTab.classList.contains('active');
}
/**
* Get the URL of a chooser option.
*
* @param {HTMLElement} optionContainer The container of the chooser option.
* @return {String} The URL of the chooser option.
* @throws {Error} If the option container does not contain a link.
*/
getChooserOptionUrl(optionContainer) {
const optionLink = optionContainer.querySelector(selectors.actions.addChooser);
if (!optionLink) {
throw new Error('Invalid chooser option container: no link found');
}
return optionLink.getAttribute('href');
}
/**
* Show the search results.
*
@@ -221,17 +244,10 @@ export default class ChooserDialogueDOM {
*
* @method showModuleHelp
* @param {Object} moduleData Data of the module to carousel to
* @param {Modal} modal The modal object
*/
showModuleHelp(moduleData, modal) {
showModuleHelp(moduleData) {
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');
@@ -280,8 +296,9 @@ export default class ChooserDialogueDOM {
hideModuleHelp(internal = null) {
const carousel = this.modalBody.querySelector(selectors.regions.carousel);
// Trigger the transition between 'pages'.
Carousel.getInstance(carousel).prev();
if (internal === null) {
Carousel.getInstance(carousel).to(0);
// Some active footers may not provide a valid internal value.
if (!internal) {
return;
}
carousel.addEventListener(
@@ -293,6 +310,47 @@ export default class ChooserDialogueDOM {
);
}
/**
* Set the module data for the back button.
*
* @param {Object} moduleData The module data to set for the back button.
*/
setBackButtonModuleData(moduleData) {
const footer = getFirst(this.modal.getFooter());
const modnameValue = `${moduleData.componentname}_${moduleData.link}`;
footer.querySelector(selectors.actions.closeOption).dataset.modname = modnameValue;
}
/**
* Toggle the visibility of the back button.
*
* @param {Boolean} show Whether to show or hide the back button.
*/
toggleBackButton(show) {
const footer = getFirst(this.modal.getFooter());
footer.querySelector(selectors.actions.closeOption).classList.toggle('d-none', !show);
}
/**
* Toggle the visibility of the "Add selected" button.
*
* @param {Boolean} show Whether to show or hide the "Add selected" button.
*/
toggleAddButton(show) {
const footer = getFirst(this.modal.getFooter());
footer.querySelector(selectors.actions.addSelectedChooserOption).classList.toggle('d-none', !show);
}
/**
* Toggle the visibility of the active footer.
*
* @param {Boolean} show Whether to show or hide the active footer.
*/
toggleActiveFooter(show) {
const footer = getFirst(this.modal.getFooter());
footer.querySelector(selectors.regions.activeFooter).classList.toggle('d-none', !show);
}
/**
* Focus on a specific activity inside the active tab (if present).
*
@@ -476,4 +534,40 @@ export default class ChooserDialogueDOM {
this.toggleFocusableChooserOption(newFocusOption, true);
newFocusOption.focus();
}
/**
* Mark a chooser option as selected.
*
* @param {HTMLElement} chooserOption The chooser option element to mark as selected.
*/
markChooserOptionAsSelected(chooserOption) {
this.unmarkAllChooserOptionAsSelected();
chooserOption.classList.add('selected');
chooserOption.classList.add('border-primary');
chooserOption.setAttribute('aria-selected', 'true');
const footer = getFirst(this.modal.getFooter());
const addButton = footer.querySelector(selectors.actions.addSelectedChooserOption);
if (addButton) {
addButton.removeAttribute('disabled');
}
}
/**
* Unmark all chooser options as selected.
*/
unmarkAllChooserOptionAsSelected() {
const selectedOptions = this.modalBody.querySelectorAll(`${selectors.regions.chooserOption.container}.selected`);
selectedOptions.forEach((option) => {
option.classList.remove('selected');
option.classList.remove('border-primary');
option.setAttribute('aria-selected', 'false');
});
const footer = getFirst(this.modal.getFooter());
const addButton = footer.querySelector(selectors.actions.addSelectedChooserOption);
if (addButton) {
addButton.setAttribute('disabled', 'disabled');
}
}
}
@@ -197,6 +197,16 @@ export default class {
return tabData.items?.length ?? 0;
}
/**
* Get the activity chooser footer template data.
* @param {Object} footerData The active footer data object.
* @return {Object} The template data.
*/
getFooterData(footerData) {
return {
'activeFooter': footerData.customfootertemplate,
};
}
}
/**
@@ -34,6 +34,7 @@ const getDataSelector = (name, value) => {
export default {
regions: {
activeFooter: getDataSelector('region', 'active-footer-container'),
categoryContent: getDataSelector('region', 'category-content'),
getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,
chooserOptions: getDataSelector('region', 'chooser-options-container'),
@@ -63,6 +64,7 @@ export default {
manageFavourite: getDataSelector('action', 'manage-module-favourite'),
},
addChooser: getDataSelector('action', 'add-chooser-option'),
addSelectedChooserOption: getDataSelector('action', 'add-selected-chooser-option'),
closeOption: getDataSelector('action', 'close-chooser-option-summary'),
hide: getDataSelector('action', 'hide'),
search: getDataSelector('action', 'search'),
@@ -15,19 +15,37 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_course/local/activitychooser/footer_partial
@template core_course/local/activitychooser/footer
Chooser favourite template partial.
This template renders the footer of the activity chooser modal.
Example context (json):
{
"activeFooter": "Some content"
}
}}
<div class="w-100 d-flex justify-content-between" data-region="chooser-option-summary-actions-container">
<button data-action="close-chooser-option-summary" class="closeoptionsummary btn btn-secondary" tabindex="0" data-modname="{{componentname}}_{{link}}">
{{#str}} back {{/str}}
</button>
<a href="{{link}}" title="{{#str}} addnew, moodle, {{title}} {{/str}}" data-action="add-chooser-option" class="addoption btn btn-primary" tabindex="0">
{{#str}} add {{/str}}
</a>
<div data-region="active-footer-container">
{{{activeFooter}}}
</div>
<div>
<button
class="closeoptionsummary btn btn-secondary d-none"
data-action="close-chooser-option-summary"
tabindex="0"
data-modname=""
>
{{#str}} back {{/str}}
</button>
</div>
<div>
<button
class="addoption btn btn-primary"
title="{{#str}} addselectedactivity, course {{/str}}"
data-action="add-selected-chooser-option"
tabindex="0"
>
{{#str}} add {{/str}}
</button>
</div>
</div>
@@ -50,9 +50,4 @@
{{{help}}}
</div>
</div>
{{^showFooter}}
<div class="modal-footer">
{{>core_course/local/activitychooser/footer_partial}}
</div>
{{/showFooter}}
</div>
@@ -332,6 +332,13 @@ class behat_course extends behat_base {
"/parent::a";
$this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']);
$this->execute('behat_general::i_click_on_in_the', [
get_string('addselectedactivity', 'course'),
'button',
get_string('addresourceoractivity', 'moodle'),
'dialogue',
]);
}
/**
+1
View File
@@ -32,6 +32,7 @@ $string['activitydate:closed'] = 'Closed:';
$string['activitydate:closes'] = 'Closes:';
$string['activitydate:opened'] = 'Opened:';
$string['activitydate:opens'] = 'Opens:';
$string['addselectedactivity'] = 'Add selected activity';
$string['aria:coursecategory'] = 'Course category';
$string['aria:courseshortname'] = 'Course short name';
$string['aria:coursename'] = 'Course name';