From 1f68dba5c2b0ac70be79fd4e87b8f0ca27efcdde Mon Sep 17 00:00:00 2001 From: Muhammad Arnaldo Date: Fri, 7 Nov 2025 14:33:25 +0700 Subject: [PATCH] MDL-86806 core_courseformat: Fix RTL keyboard navigation Ensure consistent navigation experience across LTR and RTL interfaces in activity chooser. --- .../local/activitychooser/dialogue.min.js | 2 +- .../local/activitychooser/dialogue.min.js.map | 2 +- .../amd/src/local/activitychooser/dialogue.js | 103 +++++++++--------- 3 files changed, 53 insertions(+), 54 deletions(-) diff --git a/course/amd/build/local/activitychooser/dialogue.min.js b/course/amd/build/local/activitychooser/dialogue.min.js index ab2293a115b..ace44f47f8d 100644 --- a/course/amd/build/local/activitychooser/dialogue.min.js +++ b/course/amd/build/local/activitychooser/dialogue.min.js @@ -1,3 +1,3 @@ -define("core_course/local/activitychooser/dialogue",["exports","theme_boost/bootstrap/carousel","core/modal_events","core_course/local/activitychooser/selectors","core/templates","core/key_codes","core/loadingicon","core_course/local/activitychooser/repository","core/notification","core/utils","core/normalise"],(function(_exports,_carousel,ModalEvents,_selectors,Templates,_key_codes,_loadingicon,Repository,_notification,_utils,_normalise){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayChooser=void 0,_carousel=_interopRequireDefault(_carousel),ModalEvents=_interopRequireWildcard(ModalEvents),_selectors=_interopRequireDefault(_selectors),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),_notification=_interopRequireDefault(_notification);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const showModuleHelp=function(carousel,moduleData){let modal=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;null!==modal&&!0===moduleData.showFooter&&modal.setFooter(Templates.render("core_course/local/activitychooser/footer_partial",moduleData));const help=carousel.querySelector(_selectors.default.regions.help);help.innerHTML="",help.classList.add("m-auto");const spinnerPromise=(0,_loadingicon.addIconToContainer)(help);let transitionPromiseResolver=null;const transitionPromise=new Promise((resolve=>{transitionPromiseResolver=resolve})),contentPromise=Templates.renderForPromise("core_course/local/activitychooser/help",moduleData);Promise.all([contentPromise,spinnerPromise,transitionPromise]).then((_ref=>{let[{html:html,js:js}]=_ref;return Templates.replaceNodeContents(help,html,js)})).then((()=>(help.querySelector(_selectors.default.regions.chooserSummary.header).focus(),help))).catch(_notification.default.exception),carousel.addEventListener("slid.bs.carousel",(()=>{transitionPromiseResolver()}),{once:!0}),_carousel.default.getInstance(carousel).next()},registerListenerEvents=(modal,mappedModules,partialFavourite,footerData)=>{const modalBody=(0,_normalise.getFirst)(modal.getBody()),bodyClickListener=async e=>{if(e.target.closest(_selectors.default.actions.optionActions.showSummary)){const carousel=modalBody.querySelector(_selectors.default.regions.carousel),moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName);moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}if(e.target.closest(_selectors.default.actions.optionActions.manageFavourite)){const caller=e.target.closest(_selectors.default.actions.optionActions.manageFavourite);await(async(modalBody,caller,partialFavourite)=>{const isFavourite=caller.dataset.favourited,id=caller.dataset.id,name=caller.dataset.name,internal=caller.dataset.internal;"true"===isFavourite?(await Repository.unfavouriteModule(name,id),partialFavourite(internal,!1,modalBody)):(await Repository.favouriteModule(name,id),partialFavourite(internal,!0,modalBody))})(modalBody,caller,partialFavourite);const activeSectionId=modalBody.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=modalBody.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(modalBody,mappedModules,sectionChooserOptions,modal)}if(e.target.matches(_selectors.default.actions.closeOption)){const carousel=modalBody.querySelector(_selectors.default.regions.carousel);_carousel.default.getInstance(carousel).prev(),carousel.addEventListener("slid.bs.carousel",(()=>{modalBody.querySelector(_selectors.default.regions.modules).querySelector(_selectors.default.regions.getModuleSelector(e.target.dataset.modname)).focus()}))}if(e.target.closest(_selectors.default.actions.clearSearch)){const searchInput=modalBody.querySelector(_selectors.default.actions.search);searchInput.value="",searchInput.focus(),toggleSearchResultsView(modal,mappedModules,searchInput.value)}},footerClickListener=async e=>{if(!0===footerData.footer){const footerjs=await(pluginName=footerData.customfooterjs,"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginName],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginName)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginName]));await footerjs.footerClickListener(e,footerData,modal)}var pluginName};modal.getBodyPromise().then((body=>body[0])).then((body=>{const carousel=document.querySelector(_selectors.default.regions.carousel);return new _carousel.default(carousel,{interval:!1,pause:!0,keyboard:!1}),body})).then((body=>(body.addEventListener("click",bodyClickListener),body))).then((body=>{const searchInput=body.querySelector(_selectors.default.actions.search);return searchInput.addEventListener("input",(0,_utils.debounce)((()=>{toggleSearchResultsView(modal,mappedModules,searchInput.value)}),300)),body})).then((body=>{const activeSectionId=body.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=body.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);return toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body,mappedModules,sectionChooserOptions,modal),body})).catch(_notification.default.exception),modal.getFooterPromise().then((footer=>footer[0])).then((footer=>(footer.addEventListener("click",footerClickListener),footer))).catch(_notification.default.exception)},initChooserOptionsKeyboardNavigation=function(body,mappedModules,chooserOptionsContainer){let modal=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const chooserOptions=chooserOptionsContainer.querySelectorAll(_selectors.default.regions.chooserOption.container);Array.from(chooserOptions).forEach((element=>element.addEventListener("keydown",(e=>{if((e.keyCode===_key_codes.enter||e.keyCode===_key_codes.space)&&e.target.matches(_selectors.default.actions.optionActions.showSummary)){e.preventDefault();const moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName),carousel=document.querySelector(_selectors.default.regions.carousel);new _carousel.default({interval:!1,pause:!0,keyboard:!1}),moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}if(e.keyCode===_key_codes.arrowRight){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),nextOption=currentOption.nextElementSibling,firstOption=chooserOptionsContainer.firstElementChild,toFocusOption=clickErrorHandler(nextOption,firstOption);focusChooserOption(toFocusOption,currentOption)}if(e.keyCode===_key_codes.arrowLeft){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),previousOption=currentOption.previousElementSibling,lastOption=chooserOptionsContainer.lastElementChild,toFocusOption=clickErrorHandler(previousOption,lastOption);focusChooserOption(toFocusOption,currentOption)}if(e.keyCode===_key_codes.home){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),firstOption=chooserOptionsContainer.firstElementChild;focusChooserOption(firstOption,currentOption)}if(e.keyCode===_key_codes.end){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),lastOption=chooserOptionsContainer.lastElementChild;focusChooserOption(lastOption,currentOption)}}))))},focusChooserOption=function(currentChooserOption){let previousChooserOption=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;null!==previousChooserOption&&toggleFocusableChooserOption(previousChooserOption,!1),toggleFocusableChooserOption(currentChooserOption,!0),currentChooserOption.focus()},toggleFocusableChooserOption=(chooserOption,isFocusable)=>{const chooserOptionLink=chooserOption.querySelector(_selectors.default.actions.addChooser),chooserOptionHelp=chooserOption.querySelector(_selectors.default.actions.optionActions.showSummary),chooserOptionFavourite=chooserOption.querySelector(_selectors.default.actions.optionActions.manageFavourite);isFocusable?(chooserOption.tabIndex=0,chooserOptionLink.tabIndex=0,chooserOptionHelp.tabIndex=0,chooserOptionFavourite.tabIndex=0):(chooserOption.tabIndex=-1,chooserOptionLink.tabIndex=-1,chooserOptionHelp.tabIndex=-1,chooserOptionFavourite.tabIndex=-1)},clickErrorHandler=(item,fallback)=>null!==item?item:fallback,toggleSearchResultsView=async(modal,mappedModules,searchQuery)=>{const modalBody=modal.getBody()[0],searchResultsContainer=modalBody.querySelector(_selectors.default.regions.searchResults),chooserContainer=modalBody.querySelector(_selectors.default.regions.chooser),clearSearchButton=modalBody.querySelector(_selectors.default.actions.clearSearch);if(searchQuery.length>0){const searchResultsData=searchModules(mappedModules,searchQuery);await(async(searchResultsContainer,searchResultsData)=>{const templateData={searchresultsnumber:searchResultsData.length,searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/search_results",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)})(searchResultsContainer,searchResultsData);const searchResultItemsContainer=searchResultsContainer.querySelector(_selectors.default.regions.searchResultItems),firstSearchResultItem=searchResultItemsContainer.querySelector(_selectors.default.regions.chooserOption.container);firstSearchResultItem&&(toggleFocusableChooserOption(firstSearchResultItem,!0),initChooserOptionsKeyboardNavigation(modalBody,mappedModules,searchResultItemsContainer,modal)),clearSearchButton.classList.remove("d-none"),chooserContainer.setAttribute("hidden","hidden"),searchResultsContainer.removeAttribute("hidden")}else clearSearchButton.classList.add("d-none"),searchResultsContainer.setAttribute("hidden","hidden"),chooserContainer.removeAttribute("hidden")},searchModules=(modules,searchTerm)=>{if(""===searchTerm)return modules;searchTerm=searchTerm.toLowerCase();const searchResults=[];return modules.forEach((activity=>{const activityName=activity.title.toLowerCase(),activityDesc=activity.help.toLowerCase();(activityName.includes(searchTerm)||activityDesc.includes(searchTerm))&&searchResults.push(activity)})),searchResults},disableFocusAllChooserOptions=sectionChooserOptions=>{sectionChooserOptions.querySelectorAll(_selectors.default.regions.chooserOption.container).forEach((chooserOption=>{toggleFocusableChooserOption(chooserOption,!1)}))};_exports.displayChooser=(modalPromise,sectionModules,partialFavourite,footerData)=>{const mappedModules=new Map;sectionModules.forEach((module=>{mappedModules.set(module.componentname+"_"+module.link,module)})),modalPromise.then((modal=>(registerListenerEvents(modal,mappedModules,partialFavourite,footerData),((modal,mappedModules)=>{modal.getModal()[0].tabIndex=-1,modal.getBodyPromise().then((body=>{document.querySelectorAll(_selectors.default.elements.tab).forEach((tab=>{tab.addEventListener("shown.bs.tab",(e=>{const activeSectionId=e.target.getAttribute("href"),activeSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=activeSectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container),prevActiveSectionId=e.relatedTarget.getAttribute("href"),prevActiveSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(prevActiveSectionId));disableFocusAllChooserOptions(prevActiveSectionChooserOptions),toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body[0],mappedModules,activeSectionChooserOptions,modal)}))}))})).catch(_notification.default.exception)})(modal,mappedModules),modal.getRoot().on(ModalEvents.hidden,(()=>{modal.destroy()})),modal))).catch(_notification.default.exception)}})); +define("core_course/local/activitychooser/dialogue",["exports","jquery","core/custom_interaction_events","theme_boost/bootstrap/carousel","core/modal_events","core_course/local/activitychooser/selectors","core/templates","core/key_codes","core/loadingicon","core_course/local/activitychooser/repository","core/notification","core/utils","core/normalise"],(function(_exports,_jquery,_custom_interaction_events,_carousel,ModalEvents,_selectors,Templates,_key_codes,_loadingicon,Repository,_notification,_utils,_normalise){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayChooser=void 0,_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_carousel=_interopRequireDefault(_carousel),ModalEvents=_interopRequireWildcard(ModalEvents),_selectors=_interopRequireDefault(_selectors),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),_notification=_interopRequireDefault(_notification);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const showModuleHelp=function(carousel,moduleData){let modal=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;null!==modal&&!0===moduleData.showFooter&&modal.setFooter(Templates.render("core_course/local/activitychooser/footer_partial",moduleData));const help=carousel.querySelector(_selectors.default.regions.help);help.innerHTML="",help.classList.add("m-auto");const spinnerPromise=(0,_loadingicon.addIconToContainer)(help);let transitionPromiseResolver=null;const transitionPromise=new Promise((resolve=>{transitionPromiseResolver=resolve})),contentPromise=Templates.renderForPromise("core_course/local/activitychooser/help",moduleData);Promise.all([contentPromise,spinnerPromise,transitionPromise]).then((_ref=>{let[{html:html,js:js}]=_ref;return Templates.replaceNodeContents(help,html,js)})).then((()=>(help.querySelector(_selectors.default.regions.chooserSummary.header).focus(),help))).catch(_notification.default.exception),carousel.addEventListener("slid.bs.carousel",(()=>{transitionPromiseResolver()}),{once:!0}),_carousel.default.getInstance(carousel).next()},registerListenerEvents=(modal,mappedModules,partialFavourite,footerData)=>{const modalBody=(0,_normalise.getFirst)(modal.getBody()),bodyClickListener=async e=>{if(e.target.closest(_selectors.default.actions.optionActions.showSummary)){const carousel=modalBody.querySelector(_selectors.default.regions.carousel),moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName);moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}if(e.target.closest(_selectors.default.actions.optionActions.manageFavourite)){const caller=e.target.closest(_selectors.default.actions.optionActions.manageFavourite);await(async(modalBody,caller,partialFavourite)=>{const isFavourite=caller.dataset.favourited,id=caller.dataset.id,name=caller.dataset.name,internal=caller.dataset.internal;"true"===isFavourite?(await Repository.unfavouriteModule(name,id),partialFavourite(internal,!1,modalBody)):(await Repository.favouriteModule(name,id),partialFavourite(internal,!0,modalBody))})(modalBody,caller,partialFavourite);const activeSectionId=modalBody.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=modalBody.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(modalBody,mappedModules,sectionChooserOptions,modal)}if(e.target.matches(_selectors.default.actions.closeOption)){const carousel=modalBody.querySelector(_selectors.default.regions.carousel);_carousel.default.getInstance(carousel).prev(),carousel.addEventListener("slid.bs.carousel",(()=>{modalBody.querySelector(_selectors.default.regions.modules).querySelector(_selectors.default.regions.getModuleSelector(e.target.dataset.modname)).focus()}))}if(e.target.closest(_selectors.default.actions.clearSearch)){const searchInput=modalBody.querySelector(_selectors.default.actions.search);searchInput.value="",searchInput.focus(),toggleSearchResultsView(modal,mappedModules,searchInput.value)}},footerClickListener=async e=>{if(!0===footerData.footer){const footerjs=await(pluginName=footerData.customfooterjs,"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginName],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginName)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginName]));await footerjs.footerClickListener(e,footerData,modal)}var pluginName};modal.getBodyPromise().then((body=>body[0])).then((body=>{const carousel=document.querySelector(_selectors.default.regions.carousel);return new _carousel.default(carousel,{interval:!1,pause:!0,keyboard:!1}),body})).then((body=>(body.addEventListener("click",bodyClickListener),body))).then((body=>{const searchInput=body.querySelector(_selectors.default.actions.search);return searchInput.addEventListener("input",(0,_utils.debounce)((()=>{toggleSearchResultsView(modal,mappedModules,searchInput.value)}),300)),body})).then((body=>{const activeSectionId=body.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=body.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);return toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body,mappedModules,sectionChooserOptions,modal),body})).catch(_notification.default.exception),modal.getFooterPromise().then((footer=>footer[0])).then((footer=>(footer.addEventListener("click",footerClickListener),footer))).catch(_notification.default.exception)},initChooserOptionsKeyboardNavigation=function(body,mappedModules,chooserOptionsContainer){let modal=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const chooserOptions=chooserOptionsContainer.querySelectorAll(_selectors.default.regions.chooserOption.container);Array.from(chooserOptions).forEach((element=>{const $element=(0,_jquery.default)(element);_custom_interaction_events.default.define($element,[_custom_interaction_events.default.events.next,_custom_interaction_events.default.events.previous,_custom_interaction_events.default.events.home,_custom_interaction_events.default.events.end]);const createNavHandler=resolver=>(e,data)=>{const currentOption=data.originalEvent.target.closest(_selectors.default.regions.chooserOption.container);if(null!==currentOption){const toFocusOption=resolver(currentOption);toFocusOption&&focusChooserOption(toFocusOption,currentOption)}};$element.on(_custom_interaction_events.default.events.next,createNavHandler((current=>current.nextElementSibling||chooserOptionsContainer.firstElementChild))),$element.on(_custom_interaction_events.default.events.previous,createNavHandler((current=>current.previousElementSibling||chooserOptionsContainer.lastElementChild))),$element.on(_custom_interaction_events.default.events.home,createNavHandler((()=>chooserOptionsContainer.firstElementChild))),$element.on(_custom_interaction_events.default.events.end,createNavHandler((()=>chooserOptionsContainer.lastElementChild))),element.addEventListener("keydown",(e=>{if((e.keyCode===_key_codes.enter||e.keyCode===_key_codes.space)&&e.target.matches(_selectors.default.actions.optionActions.showSummary)){e.preventDefault();const moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName),carousel=document.querySelector(_selectors.default.regions.carousel);new _carousel.default({interval:!1,pause:!0,keyboard:!1}),moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}}))}))},focusChooserOption=function(currentChooserOption){let previousChooserOption=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;null!==previousChooserOption&&toggleFocusableChooserOption(previousChooserOption,!1),toggleFocusableChooserOption(currentChooserOption,!0),currentChooserOption.focus()},toggleFocusableChooserOption=(chooserOption,isFocusable)=>{const chooserOptionLink=chooserOption.querySelector(_selectors.default.actions.addChooser),chooserOptionHelp=chooserOption.querySelector(_selectors.default.actions.optionActions.showSummary),chooserOptionFavourite=chooserOption.querySelector(_selectors.default.actions.optionActions.manageFavourite);isFocusable?(chooserOption.tabIndex=0,chooserOptionLink.tabIndex=0,chooserOptionHelp.tabIndex=0,chooserOptionFavourite.tabIndex=0):(chooserOption.tabIndex=-1,chooserOptionLink.tabIndex=-1,chooserOptionHelp.tabIndex=-1,chooserOptionFavourite.tabIndex=-1)},toggleSearchResultsView=async(modal,mappedModules,searchQuery)=>{const modalBody=modal.getBody()[0],searchResultsContainer=modalBody.querySelector(_selectors.default.regions.searchResults),chooserContainer=modalBody.querySelector(_selectors.default.regions.chooser),clearSearchButton=modalBody.querySelector(_selectors.default.actions.clearSearch);if(searchQuery.length>0){const searchResultsData=searchModules(mappedModules,searchQuery);await(async(searchResultsContainer,searchResultsData)=>{const templateData={searchresultsnumber:searchResultsData.length,searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/search_results",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)})(searchResultsContainer,searchResultsData);const searchResultItemsContainer=searchResultsContainer.querySelector(_selectors.default.regions.searchResultItems),firstSearchResultItem=searchResultItemsContainer.querySelector(_selectors.default.regions.chooserOption.container);firstSearchResultItem&&(toggleFocusableChooserOption(firstSearchResultItem,!0),initChooserOptionsKeyboardNavigation(modalBody,mappedModules,searchResultItemsContainer,modal)),clearSearchButton.classList.remove("d-none"),chooserContainer.setAttribute("hidden","hidden"),searchResultsContainer.removeAttribute("hidden")}else clearSearchButton.classList.add("d-none"),searchResultsContainer.setAttribute("hidden","hidden"),chooserContainer.removeAttribute("hidden")},searchModules=(modules,searchTerm)=>{if(""===searchTerm)return modules;searchTerm=searchTerm.toLowerCase();const searchResults=[];return modules.forEach((activity=>{const activityName=activity.title.toLowerCase(),activityDesc=activity.help.toLowerCase();(activityName.includes(searchTerm)||activityDesc.includes(searchTerm))&&searchResults.push(activity)})),searchResults},disableFocusAllChooserOptions=sectionChooserOptions=>{sectionChooserOptions.querySelectorAll(_selectors.default.regions.chooserOption.container).forEach((chooserOption=>{toggleFocusableChooserOption(chooserOption,!1)}))};_exports.displayChooser=(modalPromise,sectionModules,partialFavourite,footerData)=>{const mappedModules=new Map;sectionModules.forEach((module=>{mappedModules.set(module.componentname+"_"+module.link,module)})),modalPromise.then((modal=>(registerListenerEvents(modal,mappedModules,partialFavourite,footerData),((modal,mappedModules)=>{modal.getModal()[0].tabIndex=-1,modal.getBodyPromise().then((body=>{document.querySelectorAll(_selectors.default.elements.tab).forEach((tab=>{tab.addEventListener("shown.bs.tab",(e=>{const activeSectionId=e.target.getAttribute("href"),activeSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=activeSectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container),prevActiveSectionId=e.relatedTarget.getAttribute("href"),prevActiveSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(prevActiveSectionId));disableFocusAllChooserOptions(prevActiveSectionChooserOptions),toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body[0],mappedModules,activeSectionChooserOptions,modal)}))}))})).catch(_notification.default.exception)})(modal,mappedModules),modal.getRoot().on(ModalEvents.hidden,(()=>{modal.destroy()})),modal))).catch(_notification.default.exception)}})); //# sourceMappingURL=dialogue.min.js.map \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/dialogue.min.js.map b/course/amd/build/local/activitychooser/dialogue.min.js.map index 4898b189a0f..c96b25fb02c 100644 --- a/course/amd/build/local/activitychooser/dialogue.min.js.map +++ b/course/amd/build/local/activitychooser/dialogue.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dialogue.min.js","sources":["../../../src/local/activitychooser/dialogue.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A type of dialogue used as for choosing options.\n *\n * @module core_course/local/activitychooser/dialogue\n * @copyright 2019 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Carousel from 'theme_boost/bootstrap/carousel';\nimport * as ModalEvents from 'core/modal_events';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport * as Templates from 'core/templates';\nimport {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';\nimport {addIconToContainer} from 'core/loadingicon';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport Notification from 'core/notification';\nimport {debounce} from 'core/utils';\nimport {getFirst} from 'core/normalise';\nconst getPlugin = pluginName => import(pluginName);\n\n/**\n * Given an event from the main module 'page' navigate to it's help section via a carousel.\n *\n * @method showModuleHelp\n * @param {Element} carousel Our initialized carousel to manipulate\n * @param {Object} moduleData Data of the module to carousel to\n * @param {jQuery} modal We need to figure out if the current modal has a footer.\n */\nconst showModuleHelp = (carousel, moduleData, modal = null) => {\n // If we have a real footer then we need to change temporarily.\n if (modal !== null && moduleData.showFooter === true) {\n modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));\n }\n const help = carousel.querySelector(selectors.regions.help);\n help.innerHTML = '';\n help.classList.add('m-auto');\n\n // Add a spinner.\n const spinnerPromise = addIconToContainer(help);\n\n // Used later...\n let transitionPromiseResolver = null;\n const transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n // Build up the html & js ready to place into the help section.\n const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);\n\n // Wait for the content to be ready, and for the transition to be complet.\n Promise.all([contentPromise, spinnerPromise, transitionPromise])\n .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))\n .then(() => {\n help.querySelector(selectors.regions.chooserSummary.header).focus();\n return help;\n })\n .catch(Notification.exception);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.addEventListener('slid.bs.carousel', () => {\n transitionPromiseResolver();\n }, {once: true});\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).next();\n};\n\n/**\n * Given a user wants to change the favourite state of a module we either add or remove the status.\n * We also propergate this change across our map of modals.\n *\n * @method manageFavouriteState\n * @param {HTMLElement} modalBody The DOM node of the modal to manipulate\n * @param {HTMLElement} caller\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n */\nconst manageFavouriteState = async(modalBody, caller, partialFavourite) => {\n const isFavourite = caller.dataset.favourited;\n const id = caller.dataset.id;\n const name = caller.dataset.name;\n const internal = caller.dataset.internal;\n // Switch on fave or not.\n if (isFavourite === 'true') {\n await Repository.unfavouriteModule(name, id);\n\n partialFavourite(internal, false, modalBody);\n } else {\n await Repository.favouriteModule(name, id);\n\n partialFavourite(internal, true, modalBody);\n }\n\n};\n\n/**\n * Register chooser related event listeners.\n *\n * @method registerListenerEvents\n * @param {Promise} modal Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nconst registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {\n const modalBody = getFirst(modal.getBody());\n const bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = modalBody.querySelector(selectors.regions.carousel);\n\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n\n if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {\n const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);\n await manageFavouriteState(modalBody, caller, partialFavourite);\n const activeSectionId = modalBody.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modalBody\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, sectionChooserOptions, modal);\n }\n\n // From the help screen go back to the module overview.\n if (e.target.matches(selectors.actions.closeOption)) {\n const carousel = modalBody.querySelector(selectors.regions.carousel);\n\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).prev();\n carousel.addEventListener('slid.bs.carousel', () => {\n const allModules = modalBody.querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));\n caller.focus();\n });\n }\n\n // The \"clear search\" button is triggered.\n if (e.target.closest(selectors.actions.clearSearch)) {\n // Clear the entered search query in the search bar and hide the search results container.\n const searchInput = modalBody.querySelector(selectors.actions.search);\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }\n };\n\n // We essentially have two types of footer.\n // A fake one that is handled within the template for chooser_help and then all of the stuff for\n // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we\n // need to manage. The below code handles a real footer going to a mnet carousel item.\n const footerClickListener = async(e) => {\n if (footerData.footer === true) {\n const footerjs = await getPlugin(footerData.customfooterjs);\n await footerjs.footerClickListener(e, footerData, modal);\n }\n };\n\n modal.getBodyPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(body => body[0])\n\n // Set up the carousel.\n .then(body => {\n const carousel = document.querySelector(selectors.regions.carousel);\n new Carousel(carousel, {\n interval: false,\n pause: true,\n keyboard: false\n });\n\n return body;\n })\n\n // Add the listener for clicks on the body.\n .then(body => {\n body.addEventListener('click', bodyClickListener);\n return body;\n })\n\n // Add a listener for an input change in the activity chooser's search bar.\n .then(body => {\n const searchInput = body.querySelector(selectors.actions.search);\n // The search input is triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }, 300));\n return body;\n })\n\n // Register event listeners related to the keyboard navigation controls.\n .then(body => {\n // Get the active chooser options section.\n const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);\n\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);\n\n return body;\n })\n .catch(Notification.exception);\n\n modal.getFooterPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(footer => footer[0])\n // Add the listener for clicks on the footer.\n .then(footer => {\n footer.addEventListener('click', footerClickListener);\n return footer;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Initialise the keyboard navigation controls for the chooser options.\n *\n * @method initChooserOptionsKeyboardNavigation\n * @param {HTMLElement} body Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items\n * @param {Object} modal Our created modal for the section\n */\nconst initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {\n const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);\n\n Array.from(chooserOptions).forEach((element) => {\n return element.addEventListener('keydown', (e) => {\n\n // Check for enter/ space triggers for showing the help.\n if (e.keyCode === enter || e.keyCode === space) {\n if (e.target.matches(selectors.actions.optionActions.showSummary)) {\n e.preventDefault();\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n const carousel = document.querySelector(selectors.regions.carousel);\n new Carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n }\n\n // Next.\n if (e.keyCode === arrowRight) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const nextOption = currentOption.nextElementSibling;\n const firstOption = chooserOptionsContainer.firstElementChild;\n const toFocusOption = clickErrorHandler(nextOption, firstOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n // Previous.\n if (e.keyCode === arrowLeft) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const previousOption = currentOption.previousElementSibling;\n const lastOption = chooserOptionsContainer.lastElementChild;\n const toFocusOption = clickErrorHandler(previousOption, lastOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n if (e.keyCode === home) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const firstOption = chooserOptionsContainer.firstElementChild;\n focusChooserOption(firstOption, currentOption);\n }\n\n if (e.keyCode === end) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const lastOption = chooserOptionsContainer.lastElementChild;\n focusChooserOption(lastOption, currentOption);\n }\n });\n });\n};\n\n/**\n * Focus on a chooser option element and remove the previous chooser element from the focus order\n *\n * @method focusChooserOption\n * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus\n * @param {HTMLElement|null} previousChooserOption The previous focused option element\n */\nconst focusChooserOption = (currentChooserOption, previousChooserOption = null) => {\n if (previousChooserOption !== null) {\n toggleFocusableChooserOption(previousChooserOption, false);\n }\n\n toggleFocusableChooserOption(currentChooserOption, true);\n currentChooserOption.focus();\n};\n\n/**\n * Add or remove a chooser option from the focus order.\n *\n * @method toggleFocusableChooserOption\n * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order\n * @param {Boolean} isFocusable Whether the chooser element is focusable or not\n */\nconst toggleFocusableChooserOption = (chooserOption, isFocusable) => {\n const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);\n const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);\n const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);\n\n if (isFocusable) {\n // Set tabindex to 0 to add current chooser option element to the focus order.\n chooserOption.tabIndex = 0;\n chooserOptionLink.tabIndex = 0;\n chooserOptionHelp.tabIndex = 0;\n chooserOptionFavourite.tabIndex = 0;\n } else {\n // Set tabindex to -1 to remove the previous chooser option element from the focus order.\n chooserOption.tabIndex = -1;\n chooserOptionLink.tabIndex = -1;\n chooserOptionHelp.tabIndex = -1;\n chooserOptionFavourite.tabIndex = -1;\n }\n};\n\n/**\n * Small error handling function to make sure the navigated to object exists\n *\n * @method clickErrorHandler\n * @param {HTMLElement} item What we want to check exists\n * @param {HTMLElement} fallback If we dont match anything fallback the focus\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Render the search results in a defined container\n *\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The container where the data should be rendered\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\nconst renderSearchResults = async(searchResultsContainer, searchResultsData) => {\n const templateData = {\n 'searchresultsnumber': searchResultsData.length,\n 'searchresults': searchResultsData\n };\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query\n *\n * @method toggleSearchResultsView\n * @param {Object} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {String} searchQuery The search query\n */\nconst toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {\n const modalBody = modal.getBody()[0];\n const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);\n const chooserContainer = modalBody.querySelector(selectors.regions.chooser);\n const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);\n\n if (searchQuery.length > 0) { // Search query is present.\n const searchResultsData = searchModules(mappedModules, searchQuery);\n await renderSearchResults(searchResultsContainer, searchResultsData);\n const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);\n const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);\n if (firstSearchResultItem) {\n // Set the first result item to be focusable.\n toggleFocusableChooserOption(firstSearchResultItem, true);\n // Register keyboard events on the created search result items.\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);\n }\n // Display the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.remove('d-none');\n // Hide the default chooser options container.\n chooserContainer.setAttribute('hidden', 'hidden');\n // Display the search results container.\n searchResultsContainer.removeAttribute('hidden');\n } else { // Search query is not present.\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n // Hide the search results container.\n searchResultsContainer.setAttribute('hidden', 'hidden');\n // Display the default chooser options container.\n chooserContainer.removeAttribute('hidden');\n }\n};\n\n/**\n * Return the list of modules which have a name or description that matches the given search term.\n *\n * @method searchModules\n * @param {Array} modules List of available modules\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchModules = (modules, searchTerm) => {\n if (searchTerm === '') {\n return modules;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n modules.forEach((activity) => {\n const activityName = activity.title.toLowerCase();\n const activityDesc = activity.help.toLowerCase();\n if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {\n searchResults.push(activity);\n }\n });\n\n return searchResults;\n};\n\n/**\n * Set up our tabindex information across the chooser.\n *\n * @method setupKeyboardAccessibility\n * @param {Promise} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the built module information\n */\nconst setupKeyboardAccessibility = (modal, mappedModules) => {\n modal.getModal()[0].tabIndex = -1;\n\n modal.getBodyPromise().then(body => {\n document.querySelectorAll(selectors.elements.tab).forEach((tab) => {\n tab.addEventListener('shown.bs.tab', (e) => {\n const activeSectionId = e.target.getAttribute(\"href\");\n const activeSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = activeSectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n const prevActiveSectionId = e.relatedTarget.getAttribute(\"href\");\n const prevActiveSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));\n\n // Disable the focus of every chooser option in the previous active section.\n disableFocusAllChooserOptions(prevActiveSectionChooserOptions);\n // Enable the focus of the first chooser option in the current active section.\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);\n });\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Disable the focus of all chooser options in a specific container (section).\n *\n * @method disableFocusAllChooserOptions\n * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items\n */\nconst disableFocusAllChooserOptions = (sectionChooserOptions) => {\n const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);\n allChooserOptions.forEach((chooserOption) => {\n toggleFocusableChooserOption(chooserOption, false);\n });\n};\n\n/**\n * Display the module chooser.\n *\n * @method displayChooser\n * @param {Promise} modalPromise Our created modal for the section\n * @param {Array} sectionModules An array of all of the built module information\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nexport const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {\n // Make a map so we can quickly fetch a specific module's object for either rendering or searching.\n const mappedModules = new Map();\n sectionModules.forEach((module) => {\n mappedModules.set(module.componentname + '_' + module.link, module);\n });\n\n // Register event listeners.\n modalPromise.then(modal => {\n registerListenerEvents(modal, mappedModules, partialFavourite, footerData);\n\n // We want to focus on the first chooser option element as soon as the modal is opened.\n setupKeyboardAccessibility(modal, mappedModules);\n\n // We want to focus on the action select when the dialog is closed.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n\n return modal;\n }).catch(Notification.exception);\n};\n"],"names":["showModuleHelp","carousel","moduleData","modal","showFooter","setFooter","Templates","render","help","querySelector","selectors","regions","innerHTML","classList","add","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","resolve","contentPromise","renderForPromise","all","then","_ref","html","js","replaceNodeContents","chooserSummary","header","focus","catch","Notification","exception","addEventListener","once","getInstance","next","registerListenerEvents","mappedModules","partialFavourite","footerData","modalBody","getBody","bodyClickListener","async","e","target","closest","actions","optionActions","showSummary","moduleName","chooserOption","container","dataset","modname","get","hasFooterContent","manageFavourite","caller","isFavourite","favourited","id","name","internal","Repository","unfavouriteModule","favouriteModule","manageFavouriteState","activeSectionId","elements","activetab","getAttribute","sectionChooserOptions","getSectionChooserOptions","firstChooserOption","toggleFocusableChooserOption","initChooserOptionsKeyboardNavigation","matches","closeOption","prev","modules","getModuleSelector","clearSearch","searchInput","search","value","toggleSearchResultsView","footerClickListener","footer","footerjs","pluginName","customfooterjs","getBodyPromise","body","document","Carousel","interval","pause","keyboard","getFooterPromise","chooserOptionsContainer","chooserOptions","querySelectorAll","Array","from","forEach","element","keyCode","enter","space","preventDefault","arrowRight","currentOption","nextOption","nextElementSibling","firstOption","firstElementChild","toFocusOption","clickErrorHandler","focusChooserOption","arrowLeft","previousOption","previousElementSibling","lastOption","lastElementChild","home","end","currentChooserOption","previousChooserOption","isFocusable","chooserOptionLink","addChooser","chooserOptionHelp","chooserOptionFavourite","tabIndex","item","fallback","searchQuery","searchResultsContainer","searchResults","chooserContainer","chooser","clearSearchButton","length","searchResultsData","searchModules","templateData","renderSearchResults","searchResultItemsContainer","searchResultItems","firstSearchResultItem","remove","setAttribute","removeAttribute","searchTerm","toLowerCase","activity","activityName","title","activityDesc","includes","push","disableFocusAllChooserOptions","modalPromise","sectionModules","Map","module","set","componentname","link","getModal","tab","activeSectionChooserOptions","prevActiveSectionId","relatedTarget","prevActiveSectionChooserOptions","setupKeyboardAccessibility","getRoot","on","ModalEvents","hidden","destroy"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"dialogue.min.js","sources":["../../../src/local/activitychooser/dialogue.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A type of dialogue used as for choosing options.\n *\n * @module core_course/local/activitychooser/dialogue\n * @copyright 2019 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport CustomEvents from 'core/custom_interaction_events';\nimport Carousel from 'theme_boost/bootstrap/carousel';\nimport * as ModalEvents from 'core/modal_events';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport * as Templates from 'core/templates';\nimport {enter, space} from 'core/key_codes';\nimport {addIconToContainer} from 'core/loadingicon';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport Notification from 'core/notification';\nimport {debounce} from 'core/utils';\nimport {getFirst} from 'core/normalise';\nconst getPlugin = pluginName => import(pluginName);\n\n/**\n * Given an event from the main module 'page' navigate to it's help section via a carousel.\n *\n * @method showModuleHelp\n * @param {Element} carousel Our initialized carousel to manipulate\n * @param {Object} moduleData Data of the module to carousel to\n * @param {jQuery} modal We need to figure out if the current modal has a footer.\n */\nconst showModuleHelp = (carousel, moduleData, modal = null) => {\n // If we have a real footer then we need to change temporarily.\n if (modal !== null && moduleData.showFooter === true) {\n modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));\n }\n const help = carousel.querySelector(selectors.regions.help);\n help.innerHTML = '';\n help.classList.add('m-auto');\n\n // Add a spinner.\n const spinnerPromise = addIconToContainer(help);\n\n // Used later...\n let transitionPromiseResolver = null;\n const transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n // Build up the html & js ready to place into the help section.\n const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);\n\n // Wait for the content to be ready, and for the transition to be complet.\n Promise.all([contentPromise, spinnerPromise, transitionPromise])\n .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))\n .then(() => {\n help.querySelector(selectors.regions.chooserSummary.header).focus();\n return help;\n })\n .catch(Notification.exception);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.addEventListener('slid.bs.carousel', () => {\n transitionPromiseResolver();\n }, {once: true});\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).next();\n};\n\n/**\n * Given a user wants to change the favourite state of a module we either add or remove the status.\n * We also propergate this change across our map of modals.\n *\n * @method manageFavouriteState\n * @param {HTMLElement} modalBody The DOM node of the modal to manipulate\n * @param {HTMLElement} caller\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n */\nconst manageFavouriteState = async(modalBody, caller, partialFavourite) => {\n const isFavourite = caller.dataset.favourited;\n const id = caller.dataset.id;\n const name = caller.dataset.name;\n const internal = caller.dataset.internal;\n // Switch on fave or not.\n if (isFavourite === 'true') {\n await Repository.unfavouriteModule(name, id);\n\n partialFavourite(internal, false, modalBody);\n } else {\n await Repository.favouriteModule(name, id);\n\n partialFavourite(internal, true, modalBody);\n }\n\n};\n\n/**\n * Register chooser related event listeners.\n *\n * @method registerListenerEvents\n * @param {Promise} modal Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nconst registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {\n const modalBody = getFirst(modal.getBody());\n const bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = modalBody.querySelector(selectors.regions.carousel);\n\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n\n if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {\n const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);\n await manageFavouriteState(modalBody, caller, partialFavourite);\n const activeSectionId = modalBody.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modalBody\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, sectionChooserOptions, modal);\n }\n\n // From the help screen go back to the module overview.\n if (e.target.matches(selectors.actions.closeOption)) {\n const carousel = modalBody.querySelector(selectors.regions.carousel);\n\n // Trigger the transition between 'pages'.\n Carousel.getInstance(carousel).prev();\n carousel.addEventListener('slid.bs.carousel', () => {\n const allModules = modalBody.querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));\n caller.focus();\n });\n }\n\n // The \"clear search\" button is triggered.\n if (e.target.closest(selectors.actions.clearSearch)) {\n // Clear the entered search query in the search bar and hide the search results container.\n const searchInput = modalBody.querySelector(selectors.actions.search);\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }\n };\n\n // We essentially have two types of footer.\n // A fake one that is handled within the template for chooser_help and then all of the stuff for\n // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we\n // need to manage. The below code handles a real footer going to a mnet carousel item.\n const footerClickListener = async(e) => {\n if (footerData.footer === true) {\n const footerjs = await getPlugin(footerData.customfooterjs);\n await footerjs.footerClickListener(e, footerData, modal);\n }\n };\n\n modal.getBodyPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(body => body[0])\n\n // Set up the carousel.\n .then(body => {\n const carousel = document.querySelector(selectors.regions.carousel);\n new Carousel(carousel, {\n interval: false,\n pause: true,\n keyboard: false\n });\n\n return body;\n })\n\n // Add the listener for clicks on the body.\n .then(body => {\n body.addEventListener('click', bodyClickListener);\n return body;\n })\n\n // Add a listener for an input change in the activity chooser's search bar.\n .then(body => {\n const searchInput = body.querySelector(selectors.actions.search);\n // The search input is triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }, 300));\n return body;\n })\n\n // Register event listeners related to the keyboard navigation controls.\n .then(body => {\n // Get the active chooser options section.\n const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);\n\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);\n\n return body;\n })\n .catch(Notification.exception);\n\n modal.getFooterPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(footer => footer[0])\n // Add the listener for clicks on the footer.\n .then(footer => {\n footer.addEventListener('click', footerClickListener);\n return footer;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Initialise the keyboard navigation controls for the chooser options.\n *\n * @method initChooserOptionsKeyboardNavigation\n * @param {HTMLElement} body Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items\n * @param {Object} modal Our created modal for the section\n */\nconst initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {\n const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);\n\n Array.from(chooserOptions).forEach((element) => {\n const $element = $(element);\n\n // Set up custom interaction events for RTL-aware keyboard navigation.\n CustomEvents.define($element, [\n CustomEvents.events.next,\n CustomEvents.events.previous,\n CustomEvents.events.home,\n CustomEvents.events.end,\n ]);\n\n // Handle focus move (automatically handles RTL).\n const createNavHandler = (resolver) => (e, data) => {\n const currentOption = data.originalEvent.target.closest(\n selectors.regions.chooserOption.container\n );\n if (currentOption !== null) {\n const toFocusOption = resolver(currentOption);\n if (toFocusOption) {\n focusChooserOption(toFocusOption, currentOption);\n }\n }\n };\n\n $element.on(\n CustomEvents.events.next,\n createNavHandler(\n (current) => current.nextElementSibling || chooserOptionsContainer.firstElementChild\n )\n );\n\n $element.on(\n CustomEvents.events.previous,\n createNavHandler(\n (current) => current.previousElementSibling || chooserOptionsContainer.lastElementChild\n )\n );\n\n $element.on(\n CustomEvents.events.home,\n createNavHandler(() => chooserOptionsContainer.firstElementChild)\n );\n\n $element.on(\n CustomEvents.events.end,\n createNavHandler(() => chooserOptionsContainer.lastElementChild)\n );\n\n element.addEventListener('keydown', (e) => {\n\n // Check for enter/ space triggers for showing the help.\n if (e.keyCode === enter || e.keyCode === space) {\n if (e.target.matches(selectors.actions.optionActions.showSummary)) {\n e.preventDefault();\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n const carousel = document.querySelector(selectors.regions.carousel);\n new Carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n }\n });\n });\n};\n\n/**\n * Focus on a chooser option element and remove the previous chooser element from the focus order\n *\n * @method focusChooserOption\n * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus\n * @param {HTMLElement|null} previousChooserOption The previous focused option element\n */\nconst focusChooserOption = (currentChooserOption, previousChooserOption = null) => {\n if (previousChooserOption !== null) {\n toggleFocusableChooserOption(previousChooserOption, false);\n }\n\n toggleFocusableChooserOption(currentChooserOption, true);\n currentChooserOption.focus();\n};\n\n/**\n * Add or remove a chooser option from the focus order.\n *\n * @method toggleFocusableChooserOption\n * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order\n * @param {Boolean} isFocusable Whether the chooser element is focusable or not\n */\nconst toggleFocusableChooserOption = (chooserOption, isFocusable) => {\n const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);\n const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);\n const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);\n\n if (isFocusable) {\n // Set tabindex to 0 to add current chooser option element to the focus order.\n chooserOption.tabIndex = 0;\n chooserOptionLink.tabIndex = 0;\n chooserOptionHelp.tabIndex = 0;\n chooserOptionFavourite.tabIndex = 0;\n } else {\n // Set tabindex to -1 to remove the previous chooser option element from the focus order.\n chooserOption.tabIndex = -1;\n chooserOptionLink.tabIndex = -1;\n chooserOptionHelp.tabIndex = -1;\n chooserOptionFavourite.tabIndex = -1;\n }\n};\n\n/**\n * Render the search results in a defined container\n *\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The container where the data should be rendered\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\nconst renderSearchResults = async(searchResultsContainer, searchResultsData) => {\n const templateData = {\n 'searchresultsnumber': searchResultsData.length,\n 'searchresults': searchResultsData\n };\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query\n *\n * @method toggleSearchResultsView\n * @param {Object} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {String} searchQuery The search query\n */\nconst toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {\n const modalBody = modal.getBody()[0];\n const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);\n const chooserContainer = modalBody.querySelector(selectors.regions.chooser);\n const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);\n\n if (searchQuery.length > 0) { // Search query is present.\n const searchResultsData = searchModules(mappedModules, searchQuery);\n await renderSearchResults(searchResultsContainer, searchResultsData);\n const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);\n const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);\n if (firstSearchResultItem) {\n // Set the first result item to be focusable.\n toggleFocusableChooserOption(firstSearchResultItem, true);\n // Register keyboard events on the created search result items.\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);\n }\n // Display the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.remove('d-none');\n // Hide the default chooser options container.\n chooserContainer.setAttribute('hidden', 'hidden');\n // Display the search results container.\n searchResultsContainer.removeAttribute('hidden');\n } else { // Search query is not present.\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n // Hide the search results container.\n searchResultsContainer.setAttribute('hidden', 'hidden');\n // Display the default chooser options container.\n chooserContainer.removeAttribute('hidden');\n }\n};\n\n/**\n * Return the list of modules which have a name or description that matches the given search term.\n *\n * @method searchModules\n * @param {Array} modules List of available modules\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchModules = (modules, searchTerm) => {\n if (searchTerm === '') {\n return modules;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n modules.forEach((activity) => {\n const activityName = activity.title.toLowerCase();\n const activityDesc = activity.help.toLowerCase();\n if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {\n searchResults.push(activity);\n }\n });\n\n return searchResults;\n};\n\n/**\n * Set up our tabindex information across the chooser.\n *\n * @method setupKeyboardAccessibility\n * @param {Promise} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the built module information\n */\nconst setupKeyboardAccessibility = (modal, mappedModules) => {\n modal.getModal()[0].tabIndex = -1;\n\n modal.getBodyPromise().then(body => {\n document.querySelectorAll(selectors.elements.tab).forEach((tab) => {\n tab.addEventListener('shown.bs.tab', (e) => {\n const activeSectionId = e.target.getAttribute(\"href\");\n const activeSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = activeSectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n const prevActiveSectionId = e.relatedTarget.getAttribute(\"href\");\n const prevActiveSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));\n\n // Disable the focus of every chooser option in the previous active section.\n disableFocusAllChooserOptions(prevActiveSectionChooserOptions);\n // Enable the focus of the first chooser option in the current active section.\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);\n });\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Disable the focus of all chooser options in a specific container (section).\n *\n * @method disableFocusAllChooserOptions\n * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items\n */\nconst disableFocusAllChooserOptions = (sectionChooserOptions) => {\n const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);\n allChooserOptions.forEach((chooserOption) => {\n toggleFocusableChooserOption(chooserOption, false);\n });\n};\n\n/**\n * Display the module chooser.\n *\n * @method displayChooser\n * @param {Promise} modalPromise Our created modal for the section\n * @param {Array} sectionModules An array of all of the built module information\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nexport const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {\n // Make a map so we can quickly fetch a specific module's object for either rendering or searching.\n const mappedModules = new Map();\n sectionModules.forEach((module) => {\n mappedModules.set(module.componentname + '_' + module.link, module);\n });\n\n // Register event listeners.\n modalPromise.then(modal => {\n registerListenerEvents(modal, mappedModules, partialFavourite, footerData);\n\n // We want to focus on the first chooser option element as soon as the modal is opened.\n setupKeyboardAccessibility(modal, mappedModules);\n\n // We want to focus on the action select when the dialog is closed.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n\n return modal;\n }).catch(Notification.exception);\n};\n"],"names":["showModuleHelp","carousel","moduleData","modal","showFooter","setFooter","Templates","render","help","querySelector","selectors","regions","innerHTML","classList","add","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","resolve","contentPromise","renderForPromise","all","then","_ref","html","js","replaceNodeContents","chooserSummary","header","focus","catch","Notification","exception","addEventListener","once","getInstance","next","registerListenerEvents","mappedModules","partialFavourite","footerData","modalBody","getBody","bodyClickListener","async","e","target","closest","actions","optionActions","showSummary","moduleName","chooserOption","container","dataset","modname","get","hasFooterContent","manageFavourite","caller","isFavourite","favourited","id","name","internal","Repository","unfavouriteModule","favouriteModule","manageFavouriteState","activeSectionId","elements","activetab","getAttribute","sectionChooserOptions","getSectionChooserOptions","firstChooserOption","toggleFocusableChooserOption","initChooserOptionsKeyboardNavigation","matches","closeOption","prev","modules","getModuleSelector","clearSearch","searchInput","search","value","toggleSearchResultsView","footerClickListener","footer","footerjs","pluginName","customfooterjs","getBodyPromise","body","document","Carousel","interval","pause","keyboard","getFooterPromise","chooserOptionsContainer","chooserOptions","querySelectorAll","Array","from","forEach","element","$element","define","CustomEvents","events","previous","home","end","createNavHandler","resolver","data","currentOption","originalEvent","toFocusOption","focusChooserOption","on","current","nextElementSibling","firstElementChild","previousElementSibling","lastElementChild","keyCode","enter","space","preventDefault","currentChooserOption","previousChooserOption","isFocusable","chooserOptionLink","addChooser","chooserOptionHelp","chooserOptionFavourite","tabIndex","searchQuery","searchResultsContainer","searchResults","chooserContainer","chooser","clearSearchButton","length","searchResultsData","searchModules","templateData","renderSearchResults","searchResultItemsContainer","searchResultItems","firstSearchResultItem","remove","setAttribute","removeAttribute","searchTerm","toLowerCase","activity","activityName","title","activityDesc","includes","push","disableFocusAllChooserOptions","modalPromise","sectionModules","Map","module","set","componentname","link","getModal","tab","activeSectionChooserOptions","prevActiveSectionId","relatedTarget","prevActiveSectionChooserOptions","setupKeyboardAccessibility","getRoot","ModalEvents","hidden","destroy"],"mappings":"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"} \ No newline at end of file diff --git a/course/amd/src/local/activitychooser/dialogue.js b/course/amd/src/local/activitychooser/dialogue.js index 618077db350..0b5795d48ce 100644 --- a/course/amd/src/local/activitychooser/dialogue.js +++ b/course/amd/src/local/activitychooser/dialogue.js @@ -21,11 +21,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import $ from 'jquery'; +import CustomEvents from 'core/custom_interaction_events'; import Carousel from 'theme_boost/bootstrap/carousel'; import * as ModalEvents from 'core/modal_events'; import selectors from 'core_course/local/activitychooser/selectors'; import * as Templates from 'core/templates'; -import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes'; +import {enter, space} from 'core/key_codes'; import {addIconToContainer} from 'core/loadingicon'; import * as Repository from 'core_course/local/activitychooser/repository'; import Notification from 'core/notification'; @@ -248,7 +250,54 @@ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOption const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container); Array.from(chooserOptions).forEach((element) => { - return element.addEventListener('keydown', (e) => { + const $element = $(element); + + // Set up custom interaction events for RTL-aware keyboard navigation. + CustomEvents.define($element, [ + CustomEvents.events.next, + CustomEvents.events.previous, + CustomEvents.events.home, + CustomEvents.events.end, + ]); + + // Handle focus move (automatically handles RTL). + const createNavHandler = (resolver) => (e, data) => { + const currentOption = data.originalEvent.target.closest( + selectors.regions.chooserOption.container + ); + if (currentOption !== null) { + const toFocusOption = resolver(currentOption); + if (toFocusOption) { + focusChooserOption(toFocusOption, currentOption); + } + } + }; + + $element.on( + CustomEvents.events.next, + createNavHandler( + (current) => current.nextElementSibling || chooserOptionsContainer.firstElementChild + ) + ); + + $element.on( + CustomEvents.events.previous, + createNavHandler( + (current) => current.previousElementSibling || chooserOptionsContainer.lastElementChild + ) + ); + + $element.on( + CustomEvents.events.home, + createNavHandler(() => chooserOptionsContainer.firstElementChild) + ); + + $element.on( + CustomEvents.events.end, + createNavHandler(() => chooserOptionsContainer.lastElementChild) + ); + + element.addEventListener('keydown', (e) => { // Check for enter/ space triggers for showing the help. if (e.keyCode === enter || e.keyCode === space) { @@ -269,40 +318,6 @@ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOption showModuleHelp(carousel, moduleData, modal); } } - - // Next. - if (e.keyCode === arrowRight) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const nextOption = currentOption.nextElementSibling; - const firstOption = chooserOptionsContainer.firstElementChild; - const toFocusOption = clickErrorHandler(nextOption, firstOption); - focusChooserOption(toFocusOption, currentOption); - } - - // Previous. - if (e.keyCode === arrowLeft) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const previousOption = currentOption.previousElementSibling; - const lastOption = chooserOptionsContainer.lastElementChild; - const toFocusOption = clickErrorHandler(previousOption, lastOption); - focusChooserOption(toFocusOption, currentOption); - } - - if (e.keyCode === home) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const firstOption = chooserOptionsContainer.firstElementChild; - focusChooserOption(firstOption, currentOption); - } - - if (e.keyCode === end) { - e.preventDefault(); - const currentOption = e.target.closest(selectors.regions.chooserOption.container); - const lastOption = chooserOptionsContainer.lastElementChild; - focusChooserOption(lastOption, currentOption); - } }); }); }; @@ -350,22 +365,6 @@ const toggleFocusableChooserOption = (chooserOption, isFocusable) => { } }; -/** - * Small error handling function to make sure the navigated to object exists - * - * @method clickErrorHandler - * @param {HTMLElement} item What we want to check exists - * @param {HTMLElement} fallback If we dont match anything fallback the focus - * @return {HTMLElement} - */ -const clickErrorHandler = (item, fallback) => { - if (item !== null) { - return item; - } else { - return fallback; - } -}; - /** * Render the search results in a defined container *