@@ -53,7 +53,7 @@
src="{{nocoursesimg}}"
alt=""
>
-
diff --git a/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature b/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
index b983ce742ec7a..8b9fc3400ca66 100644
--- a/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
+++ b/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
@@ -314,6 +314,6 @@ Feature: The my overview block allows users to easily access their courses
Then I should not see "Category 1" in the "Course overview" "block"
@accessibility
- Scenario: The dashboard page must have sufficient colour contrast
+ Scenario: The My courses page must meet accessibility standards
When I am on the "My courses" page logged in as "student1"
- Then the page should meet "wcag143" accessibility standards
+ Then the page should meet accessibility standards with "best-practice" extra tests
diff --git a/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature b/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature
index 678dd960440a2..bb8c8b72bb3ef 100644
--- a/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature
+++ b/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature
@@ -88,4 +88,4 @@ Feature: Zero state on my overview block
@javascript @accessibility
Scenario: Evaluate the accessibility of the My courses (zero state)
When I am on the "My courses" page logged in as "manager"
- Then the page should meet accessibility standards
+ Then the page should meet accessibility standards with "best-practice" extra tests
diff --git a/composer.lock b/composer.lock
index 59ef58c56d4b0..60942f3310f3a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4037,16 +4037,16 @@
},
{
"name": "symfony/process",
- "version": "v6.4.4",
+ "version": "v6.4.33",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "710e27879e9be3395de2b98da3f52a946039f297"
+ "reference": "596cc67dc84d1718679240e0805dd50dbbd33992"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297",
- "reference": "710e27879e9be3395de2b98da3f52a946039f297",
+ "url": "https://api.github.com/repos/symfony/process/zipball/596cc67dc84d1718679240e0805dd50dbbd33992",
+ "reference": "596cc67dc84d1718679240e0805dd50dbbd33992",
"shasum": ""
},
"require": {
@@ -4078,7 +4078,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.4.4"
+ "source": "https://github.com/symfony/process/tree/v6.4.33"
},
"funding": [
{
@@ -4094,7 +4094,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-20T12:31:00+00:00"
+ "time": "2026-01-28T10:02:31+00:00"
},
{
"name": "symfony/service-contracts",
diff --git a/course/amd/build/local/activitychooser/dialogue.min.js b/course/amd/build/local/activitychooser/dialogue.min.js
index 4945e3b9d8b64..4dfb7dd7f959d 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","jquery","core/modal_events","core_course/local/activitychooser/selectors","core/templates","core/key_codes","core/loadingicon","core_course/local/activitychooser/repository","core/notification","core/utils"],(function(_exports,_jquery,ModalEvents,_selectors,Templates,_key_codes,_loadingicon,Repository,_notification,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayChooser=void 0,_jquery=_interopRequireDefault(_jquery),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.find(_selectors.default.regions.help)[0];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.one("slid.bs.carousel",(()=>{transitionPromiseResolver()})),carousel.carousel("next")},registerListenerEvents=(modal,mappedModules,partialFavourite,footerData)=>{const bodyClickListener=async e=>{if(e.target.closest(_selectors.default.actions.optionActions.showSummary)){const carousel=(0,_jquery.default)(modal.getBody()[0].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))})(modal.getBody()[0],caller,partialFavourite);const activeSectionId=modal.getBody()[0].querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=modal.getBody()[0].querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(modal.getBody()[0],mappedModules,sectionChooserOptions,modal)}if(e.target.matches(_selectors.default.actions.closeOption)){const carousel=(0,_jquery.default)(modal.getBody()[0].querySelector(_selectors.default.regions.carousel));carousel.carousel("prev"),carousel.on("slid.bs.carousel",(()=>{modal.getBody()[0].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=modal.getBody()[0].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=>((0,_jquery.default)(body.querySelector(_selectors.default.regions.carousel)).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(),modal.getFooterPromise().then((footer=>footer[0])).then((footer=>(footer.addEventListener("click",footerClickListener),footer))).catch()},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=(0,_jquery.default)(body.querySelector(_selectors.default.regions.carousel));carousel.carousel({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=>{(0,_jquery.default)(_selectors.default.elements.tab).on("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()}}));
+define("core_course/local/activitychooser/dialogue",["exports","jquery","core/custom_interaction_events","core/modal_events","core_course/local/activitychooser/selectors","core/templates","core/key_codes","core/loadingicon","core_course/local/activitychooser/repository","core/notification","core/utils"],(function(_exports,_jquery,_custom_interaction_events,ModalEvents,_selectors,Templates,_key_codes,_loadingicon,Repository,_notification,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayChooser=void 0,_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),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.find(_selectors.default.regions.help)[0];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.one("slid.bs.carousel",(()=>{transitionPromiseResolver()})),carousel.carousel("next")},registerListenerEvents=(modal,mappedModules,partialFavourite,footerData)=>{const bodyClickListener=async e=>{if(e.target.closest(_selectors.default.actions.optionActions.showSummary)){const carousel=(0,_jquery.default)(modal.getBody()[0].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))})(modal.getBody()[0],caller,partialFavourite);const activeSectionId=modal.getBody()[0].querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=modal.getBody()[0].querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(modal.getBody()[0],mappedModules,sectionChooserOptions,modal)}if(e.target.matches(_selectors.default.actions.closeOption)){const carousel=(0,_jquery.default)(modal.getBody()[0].querySelector(_selectors.default.regions.carousel));carousel.carousel("prev"),carousel.on("slid.bs.carousel",(()=>{modal.getBody()[0].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=modal.getBody()[0].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=>((0,_jquery.default)(body.querySelector(_selectors.default.regions.carousel)).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(),modal.getFooterPromise().then((footer=>footer[0])).then((footer=>(footer.addEventListener("click",footerClickListener),footer))).catch()},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=(0,_jquery.default)(body.querySelector(_selectors.default.regions.carousel));carousel.carousel({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=>{(0,_jquery.default)(_selectors.default.elements.tab).on("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()}}));
//# 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 311cbac79965e..4fca168ede8c3 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 $ from 'jquery';\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';\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 {jQuery} 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.find(selectors.regions.help)[0];\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.one('slid.bs.carousel', () => {\n transitionPromiseResolver();\n });\n // Trigger the transition between 'pages'.\n carousel.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 bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = $(modal.getBody()[0].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(modal.getBody()[0], caller, partialFavourite);\n const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modal.getBody()[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modal.getBody()[0], 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 = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n carousel.on('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].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 = modal.getBody()[0].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 $(body.querySelector(selectors.regions.carousel))\n .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();\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();\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 = $(body.querySelector(selectors.regions.carousel));\n carousel.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 $(selectors.elements.tab).on('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 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();\n};\n"],"names":["showModuleHelp","carousel","moduleData","modal","showFooter","setFooter","Templates","render","help","find","selectors","regions","innerHTML","classList","add","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","resolve","contentPromise","renderForPromise","all","then","_ref","html","js","replaceNodeContents","querySelector","chooserSummary","header","focus","catch","Notification","exception","one","registerListenerEvents","mappedModules","partialFavourite","footerData","bodyClickListener","async","e","target","closest","actions","optionActions","showSummary","getBody","moduleName","chooserOption","container","dataset","modname","get","hasFooterContent","manageFavourite","caller","modalBody","isFavourite","favourited","id","name","internal","Repository","unfavouriteModule","favouriteModule","manageFavouriteState","activeSectionId","elements","activetab","getAttribute","sectionChooserOptions","getSectionChooserOptions","firstChooserOption","toggleFocusableChooserOption","initChooserOptionsKeyboardNavigation","matches","closeOption","on","modules","getModuleSelector","clearSearch","searchInput","search","value","toggleSearchResultsView","footerClickListener","footer","footerjs","pluginName","customfooterjs","getBodyPromise","body","interval","pause","keyboard","addEventListener","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","ModalEvents","hidden","destroy"],"mappings":"o5DA0CMA,eAAiB,SAACC,SAAUC,gBAAYC,6DAAQ,KAEpC,OAAVA,QAA4C,IAA1BD,WAAWE,YAC7BD,MAAME,UAAUC,UAAUC,OAAO,mDAAoDL,mBAEnFM,KAAOP,SAASQ,KAAKC,mBAAUC,QAAQH,MAAM,GACnDA,KAAKI,UAAY,GACjBJ,KAAKK,UAAUC,IAAI,gBAGbC,gBAAiB,mCAAmBP,UAGtCQ,0BAA4B,WAC1BC,kBAAoB,IAAIC,SAAQC,UAClCH,0BAA4BG,WAI1BC,eAAiBd,UAAUe,iBAAiB,yCAA0CnB,YAG5FgB,QAAQI,IAAI,CAACF,eAAgBL,eAAgBE,oBACxCM,MAAKC,YAAEC,KAACA,KAADC,GAAOA,iBAASpB,UAAUqB,oBAAoBnB,KAAMiB,KAAMC,OACjEH,MAAK,KACFf,KAAKoB,cAAclB,mBAAUC,QAAQkB,eAAeC,QAAQC,QACrDvB,QAEVwB,MAAMC,sBAAaC,WAGxBjC,SAASkC,IAAI,oBAAoB,KAC7BnB,+BAGJf,SAASA,SAAS,SAuChBmC,uBAAyB,CAACjC,MAAOkC,cAAeC,iBAAkBC,oBAC9DC,kBAAoBC,MAAAA,OAClBC,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQC,cAAcC,aAAc,OACzD9C,UAAW,mBAAEE,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUC,QAAQV,WAGhEgD,WADSP,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACtCC,QAAQC,QAC5BnD,WAAamC,cAAciB,IAAIL,YAErC/C,WAAWE,WAAaD,MAAMoD,mBAC9BvD,eAAeC,SAAUC,WAAYC,UAGrCuC,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQC,cAAcU,iBAAkB,OAC7DC,OAASf,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQC,cAAcU,sBAzC/Cf,OAAMiB,UAAWD,OAAQnB,0BAC5CqB,YAAcF,OAAOL,QAAQQ,WAC7BC,GAAKJ,OAAOL,QAAQS,GACpBC,KAAOL,OAAOL,QAAQU,KACtBC,SAAWN,OAAOL,QAAQW,SAEZ,SAAhBJ,mBACMK,WAAWC,kBAAkBH,KAAMD,IAEzCvB,iBAAiByB,UAAU,EAAOL,mBAE5BM,WAAWE,gBAAgBJ,KAAMD,IAEvCvB,iBAAiByB,UAAU,EAAML,aA6BvBS,CAAqBhE,MAAM6C,UAAU,GAAIS,OAAQnB,wBACjD8B,gBAAkBjE,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAU2D,SAASC,WAAWC,aAAa,QAC9FC,sBAAwBrE,MAAM6C,UAAU,GACzCpB,cAAclB,mBAAUC,QAAQ8D,yBAAyBL,kBACxDM,mBAAqBF,sBACtB5C,cAAclB,mBAAUC,QAAQuC,cAAcC,WACnDwB,6BAA6BD,oBAAoB,GACjDE,qCAAqCzE,MAAM6C,UAAU,GAAIX,cAAemC,sBAAuBrE,UAI/FuC,EAAEC,OAAOkC,QAAQnE,mBAAUmC,QAAQiC,aAAc,OAC3C7E,UAAW,mBAAEE,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUC,QAAQV,WAGtEA,SAASA,SAAS,QAClBA,SAAS8E,GAAG,oBAAoB,KACT5E,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUC,QAAQqE,SAC5CpD,cAAclB,mBAAUC,QAAQsE,kBAAkBvC,EAAEC,OAAOS,QAAQC,UACtFtB,cAKXW,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQqC,aAAc,OAE3CC,YAAchF,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUmC,QAAQuC,QACvED,YAAYE,MAAQ,GACpBF,YAAYpD,QACZuD,wBAAwBnF,MAAOkC,cAAe8C,YAAYE,SAQ5DE,oBAAsB9C,MAAAA,QACE,IAAtBF,WAAWiD,OAAiB,OACtBC,eA1IAC,WA0I2BnD,WAAWoD,+NA1IjBD,4WAAAA,oBA2IrBD,SAASF,oBAAoB7C,EAAGH,WAAYpC,OA3I5CuF,IAAAA,YA+IdvF,MAAMyF,iBAGLrE,MAAKsE,MAAQA,KAAK,KAGlBtE,MAAKsE,2BACAA,KAAKjE,cAAclB,mBAAUC,QAAQV,WAClCA,SAAS,CACN6F,UAAU,EACVC,OAAO,EACPC,UAAU,IAGXH,QAIVtE,MAAKsE,OACFA,KAAKI,iBAAiB,QAASzD,mBACxBqD,QAIVtE,MAAKsE,aACIV,YAAcU,KAAKjE,cAAclB,mBAAUmC,QAAQuC,eAEzDD,YAAYc,iBAAiB,SAAS,oBAAS,KAE3CX,wBAAwBnF,MAAOkC,cAAe8C,YAAYE,SAC3D,MACIQ,QAIVtE,MAAKsE,aAEIzB,gBAAkByB,KAAKjE,cAAclB,mBAAU2D,SAASC,WAAWC,aAAa,QAChFC,sBAAwBqB,KAAKjE,cAAclB,mBAAUC,QAAQ8D,yBAAyBL,kBACtFM,mBAAqBF,sBAAsB5C,cAAclB,mBAAUC,QAAQuC,cAAcC,kBAE/FwB,6BAA6BD,oBAAoB,GACjDE,qCAAqCiB,KAAMxD,cAAemC,sBAAuBrE,OAE1E0F,QAEV7D,QAED7B,MAAM+F,mBAGL3E,MAAKiE,QAAUA,OAAO,KAEtBjE,MAAKiE,SACFA,OAAOS,iBAAiB,QAASV,qBAC1BC,UAEVxD,SAYC4C,qCAAuC,SAACiB,KAAMxD,cAAe8D,6BAAyBhG,6DAAQ,WAC1FiG,eAAiBD,wBAAwBE,iBAAiB3F,mBAAUC,QAAQuC,cAAcC,WAEhGmD,MAAMC,KAAKH,gBAAgBI,SAASC,SACzBA,QAAQR,iBAAiB,WAAYvD,QAGpCA,EAAEgE,UAAYC,kBAASjE,EAAEgE,UAAYE,mBACjClE,EAAEC,OAAOkC,QAAQnE,mBAAUmC,QAAQC,cAAcC,aAAc,CAC/DL,EAAEmE,uBAEI5D,WADSP,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACtCC,QAAQC,QAC5BnD,WAAamC,cAAciB,IAAIL,YAC/BhD,UAAW,mBAAE4F,KAAKjE,cAAclB,mBAAUC,QAAQV,WACxDA,SAASA,SAAS,CACd6F,UAAU,EACVC,OAAO,EACPC,UAAU,IAId9F,WAAWE,WAAaD,MAAMoD,mBAC9BvD,eAAeC,SAAUC,WAAYC,UAKzCuC,EAAEgE,UAAYI,sBAAY,CAC1BpE,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjE6D,WAAaD,cAAcE,mBAC3BC,YAAcf,wBAAwBgB,kBACtCC,cAAgBC,kBAAkBL,WAAYE,aACpDI,mBAAmBF,cAAeL,kBAIlCrE,EAAEgE,UAAYa,qBAAW,CACzB7E,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjEqE,eAAiBT,cAAcU,uBAC/BC,WAAavB,wBAAwBwB,iBACrCP,cAAgBC,kBAAkBG,eAAgBE,YACxDJ,mBAAmBF,cAAeL,kBAGlCrE,EAAEgE,UAAYkB,gBAAM,CACpBlF,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjE+D,YAAcf,wBAAwBgB,kBAC5CG,mBAAmBJ,YAAaH,kBAGhCrE,EAAEgE,UAAYmB,eAAK,CACnBnF,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjEuE,WAAavB,wBAAwBwB,iBAC3CL,mBAAmBI,WAAYX,sBAazCO,mBAAqB,SAACQ,0BAAsBC,6EAAwB,KACxC,OAA1BA,uBACApD,6BAA6BoD,uBAAuB,GAGxDpD,6BAA6BmD,sBAAsB,GACnDA,qBAAqB/F,SAUnB4C,6BAA+B,CAACzB,cAAe8E,qBAC3CC,kBAAoB/E,cAActB,cAAclB,mBAAUmC,QAAQqF,YAClEC,kBAAoBjF,cAActB,cAAclB,mBAAUmC,QAAQC,cAAcC,aAChFqF,uBAAyBlF,cAActB,cAAclB,mBAAUmC,QAAQC,cAAcU,iBAEvFwE,aAEA9E,cAAcmF,SAAW,EACzBJ,kBAAkBI,SAAW,EAC7BF,kBAAkBE,SAAW,EAC7BD,uBAAuBC,SAAW,IAGlCnF,cAAcmF,UAAY,EAC1BJ,kBAAkBI,UAAY,EAC9BF,kBAAkBE,UAAY,EAC9BD,uBAAuBC,UAAY,IAYrChB,kBAAoB,CAACiB,KAAMC,WAChB,OAATD,KACOA,KAEAC,SA6BTjD,wBAA0B7C,MAAMtC,MAAOkC,cAAemG,qBAClD9E,UAAYvD,MAAM6C,UAAU,GAC5ByF,uBAAyB/E,UAAU9B,cAAclB,mBAAUC,QAAQ+H,eACnEC,iBAAmBjF,UAAU9B,cAAclB,mBAAUC,QAAQiI,SAC7DC,kBAAoBnF,UAAU9B,cAAclB,mBAAUmC,QAAQqC,gBAEhEsD,YAAYM,OAAS,EAAG,OAClBC,kBAAoBC,cAAc3G,cAAemG,kBAzBnC/F,OAAMgG,uBAAwBM,2BAChDE,aAAe,qBACMF,kBAAkBD,qBACxBC,oBAGftH,KAACA,KAADC,GAAOA,UAAYpB,UAAUe,iBAAiB,mDAAoD4H,oBAClG3I,UAAUqB,oBAAoB8G,uBAAwBhH,KAAMC,KAmBxDwH,CAAoBT,uBAAwBM,yBAC5CI,2BAA6BV,uBAAuB7G,cAAclB,mBAAUC,QAAQyI,mBACpFC,sBAAwBF,2BAA2BvH,cAAclB,mBAAUC,QAAQuC,cAAcC,WACnGkG,wBAEA1E,6BAA6B0E,uBAAuB,GAEpDzE,qCAAqClB,UAAWrB,cAAe8G,2BAA4BhJ,QAG/F0I,kBAAkBhI,UAAUyI,OAAO,UAEnCX,iBAAiBY,aAAa,SAAU,UAExCd,uBAAuBe,gBAAgB,eAGvCX,kBAAkBhI,UAAUC,IAAI,UAEhC2H,uBAAuBc,aAAa,SAAU,UAE9CZ,iBAAiBa,gBAAgB,WAYnCR,cAAgB,CAAChE,QAASyE,iBACT,KAAfA,kBACOzE,QAEXyE,WAAaA,WAAWC,oBAClBhB,cAAgB,UACtB1D,QAAQwB,SAASmD,iBACPC,aAAeD,SAASE,MAAMH,cAC9BI,aAAeH,SAASnJ,KAAKkJ,eAC/BE,aAAaG,SAASN,aAAeK,aAAaC,SAASN,cAC3Df,cAAcsB,KAAKL,aAIpBjB,eAwCLuB,8BAAiCzF,wBACTA,sBAAsB6B,iBAAiB3F,mBAAUC,QAAQuC,cAAcC,WAC/EqD,SAAStD,gBACvByB,6BAA6BzB,eAAe,+BAatB,CAACgH,aAAcC,eAAgB7H,iBAAkBC,oBAErEF,cAAgB,IAAI+H,IAC1BD,eAAe3D,SAAS6D,SACpBhI,cAAciI,IAAID,OAAOE,cAAgB,IAAMF,OAAOG,KAAMH,WAIhEH,aAAa3I,MAAKpB,QACdiC,uBAAuBjC,MAAOkC,cAAeC,iBAAkBC,YAvDpC,EAACpC,MAAOkC,iBACvClC,MAAMsK,WAAW,GAAGpC,UAAY,EAEhClI,MAAMyF,iBAAiBrE,MAAKsE,2BACtBnF,mBAAU2D,SAASqG,KAAK3F,GAAG,gBAAiBrC,UACpC0B,gBAAkB1B,EAAEC,OAAO4B,aAAa,QACxCoG,4BAA8B9E,KAAK,GACpCjE,cAAclB,mBAAUC,QAAQ8D,yBAAyBL,kBACxDM,mBAAqBiG,4BACtB/I,cAAclB,mBAAUC,QAAQuC,cAAcC,WAC7CyH,oBAAsBlI,EAAEmI,cAActG,aAAa,QACnDuG,gCAAkCjF,KAAK,GACxCjE,cAAclB,mBAAUC,QAAQ8D,yBAAyBmG,sBAG9DX,8BAA8Ba,iCAE9BnG,6BAA6BD,oBAAoB,GACjDE,qCAAqCiB,KAAK,GAAIxD,cAAesI,4BAA6BxK,aAG/F6B,MAAMC,sBAAaC,YAqClB6I,CAA2B5K,MAAOkC,eAGlClC,MAAM6K,UAAUjG,GAAGkG,YAAYC,QAAQ,KACnC/K,MAAMgL,aAGHhL,SACR6B"}
\ 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 * 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';\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 {jQuery} 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.find(selectors.regions.help)[0];\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.one('slid.bs.carousel', () => {\n transitionPromiseResolver();\n });\n // Trigger the transition between 'pages'.\n carousel.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 bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = $(modal.getBody()[0].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(modal.getBody()[0], caller, partialFavourite);\n const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modal.getBody()[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modal.getBody()[0], 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 = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n carousel.on('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].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 = modal.getBody()[0].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 $(body.querySelector(selectors.regions.carousel))\n .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();\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();\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 = $(body.querySelector(selectors.regions.carousel));\n carousel.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 $(selectors.elements.tab).on('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 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();\n};\n"],"names":["showModuleHelp","carousel","moduleData","modal","showFooter","setFooter","Templates","render","help","find","selectors","regions","innerHTML","classList","add","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","resolve","contentPromise","renderForPromise","all","then","_ref","html","js","replaceNodeContents","querySelector","chooserSummary","header","focus","catch","Notification","exception","one","registerListenerEvents","mappedModules","partialFavourite","footerData","bodyClickListener","async","e","target","closest","actions","optionActions","showSummary","getBody","moduleName","chooserOption","container","dataset","modname","get","hasFooterContent","manageFavourite","caller","modalBody","isFavourite","favourited","id","name","internal","Repository","unfavouriteModule","favouriteModule","manageFavouriteState","activeSectionId","elements","activetab","getAttribute","sectionChooserOptions","getSectionChooserOptions","firstChooserOption","toggleFocusableChooserOption","initChooserOptionsKeyboardNavigation","matches","closeOption","on","modules","getModuleSelector","clearSearch","searchInput","search","value","toggleSearchResultsView","footerClickListener","footer","footerjs","pluginName","customfooterjs","getBodyPromise","body","interval","pause","keyboard","addEventListener","getFooterPromise","chooserOptionsContainer","chooserOptions","querySelectorAll","Array","from","forEach","element","$element","define","CustomEvents","events","next","previous","home","end","createNavHandler","resolver","data","currentOption","originalEvent","toFocusOption","focusChooserOption","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 433469a1387a1..b90638f04dd93 100644
--- a/course/amd/src/local/activitychooser/dialogue.js
+++ b/course/amd/src/local/activitychooser/dialogue.js
@@ -22,10 +22,11 @@
*/
import $ from 'jquery';
+import CustomEvents from 'core/custom_interaction_events';
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';
@@ -246,7 +247,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) {
@@ -267,40 +315,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);
- }
});
});
};
@@ -348,22 +362,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
*
diff --git a/course/classes/category.php b/course/classes/category.php
index 04f3abb0c5852..d4ef42ae1a900 100644
--- a/course/classes/category.php
+++ b/course/classes/category.php
@@ -3274,6 +3274,7 @@ public static function get_nearest_editable_subcategory(core_course_category $pa
ON uc.roleid = rc.roleid
AND ( $eqpaths OR $likechild )
WHERE (ra.id IS NOT NULL OR uc.upath IS NOT NULL)
+ ORDER BY cc.sortorder, cc.id
";
$params = [
diff --git a/course/format/templates/local/content/movecmsection.mustache b/course/format/templates/local/content/movecmsection.mustache
index f801ecda1345c..224c072423c47 100644
--- a/course/format/templates/local/content/movecmsection.mustache
+++ b/course/format/templates/local/content/movecmsection.mustache
@@ -73,7 +73,8 @@
class="collapse-list-link icons-collapse-expand collapsed"
>
- {{#pix}} t/collapsedchevron, core {{/pix}}
+ {{#pix}} t/collapsedchevron, core {{/pix}}
+ {{#pix}} t/collapsedchevron_rtl, core {{/pix}}
{{#str}} expand, core {{/str}}
diff --git a/course/tests/behat/restrict_available_activities.feature b/course/tests/behat/restrict_available_activities.feature
index 2ca3e8d2a64bd..46589f587a4ce 100644
--- a/course/tests/behat/restrict_available_activities.feature
+++ b/course/tests/behat/restrict_available_activities.feature
@@ -17,6 +17,7 @@ Feature: Restrict activities availability
And the following "activities" exist:
| activity | course | name |
| assign | C1 | Test assign name |
+ And I enable "bigbluebuttonbn" "mod" plugin
Scenario: Activities can be added with the default permissions
Given I log in as "teacher1"
@@ -27,17 +28,34 @@ Feature: Restrict activities availability
And I should see "Test assign name"
@javascript @skip_chrome_zerosize
- Scenario: Activities can not be added when the admin restricts the permissions
+ Scenario Outline: Activities can not be added when the admin restricts the permissions
Given the following "role capability" exists:
- | role | editingteacher |
- | mod/assign:addinstance | prohibit |
- And I log in as "admin"
- And I am on the "Course 1" "permissions" page
- And I override the system permissions of "Teacher" role with:
- | mod/glossary:addinstance | Prohibit |
- And I log out
+ | role | editingteacher |
+ | | prohibit |
And I log in as "teacher1"
When I am on "Course 1" course homepage with editing mode on
- And I click on "Add an activity or resource" "button" in the "New section" "section"
- Then "Add a new Assignment" "link" should not exist in the "Add an activity or resource" "dialogue"
- Then "Add a new Glossary" "link" should not exist in the "Add an activity or resource" "dialogue"
+ And I open the activity chooser
+ Then "Add a new " "link" should not exist in the "Add an activity or resource" "dialogue"
+
+ Examples:
+ | activityrole | activitytype |
+ | mod/assign:addinstance | Assignment |
+ | mod/bigbluebuttonbn:addinstance | BigBlueButton |
+ | mod/book:addinstance | Book |
+ | mod/choice:addinstance | Choice |
+ | mod/data:addinstance | Database |
+ | mod/feedback:addinstance | Feedback |
+ | mod/folder:addinstance | Folder |
+ | mod/forum:addinstance | Forum |
+ | mod/glossary:addinstance | Glossary |
+ | mod/h5pactivity:addinstance | H5P |
+ | mod/imscp:addinstance | IMS content package |
+ | mod/label:addinstance | Text and media area |
+ | mod/lesson:addinstance | Lesson |
+ | mod/page:addinstance | Page |
+ | mod/quiz:addinstance | Quiz |
+ | mod/resource:addinstance | File |
+ | mod/scorm:addinstance | SCORM package |
+ | mod/url:addinstance | URL |
+ | mod/wiki:addinstance | Wiki |
+ | mod/workshop:addinstance | Workshop |
diff --git a/enrol/database/db/upgrade.php b/enrol/database/db/upgrade.php
index 792fbe2e29a59..d5c3c7e657b8d 100644
--- a/enrol/database/db/upgrade.php
+++ b/enrol/database/db/upgrade.php
@@ -58,15 +58,19 @@ function xmldb_enrol_database_upgrade($oldversion) {
// Migrate enrolments where possible.
// First, get the user enrolments that can be migrated.
+ // Only select the earliest (MIN id) user_enrolments per user to avoid
+ // duplicate key violations when a user has multiple enrolments across
+ // duplicate database enrol instances.
$migrateusers = $DB->get_records_sql(
- "SELECT ue.id
+ "SELECT MIN(ue.id) AS id
FROM {user_enrolments} ue
WHERE ue.enrolid $insql
AND NOT EXISTS (
SELECT 1
FROM {user_enrolments} ue2
WHERE ue2.userid = ue.userid
- AND ue2.enrolid = :idtokeep)",
+ AND ue2.enrolid = :idtokeep)
+ GROUP BY ue.userid",
array_merge($inparams, ['idtokeep' => $idtokeep]),
);
diff --git a/enrol/lti/classes/local/ltiadvantage/task/sync_tool_grades.php b/enrol/lti/classes/local/ltiadvantage/task/sync_tool_grades.php
index 96e26afe1cc77..5fddab5988c81 100644
--- a/enrol/lti/classes/local/ltiadvantage/task/sync_tool_grades.php
+++ b/enrol/lti/classes/local/ltiadvantage/task/sync_tool_grades.php
@@ -122,6 +122,7 @@ protected function sync_grades_for_resource($resource): array {
mtrace("Skipping - Invalid grade $mtracecontent.");
continue;
}
+ $grade = floatval($grade); // Grade must be sent as a numeric value, not a string.
if (empty($grademax)) {
mtrace("Skipping - Invalid grademax $mtracecontent.");
diff --git a/filter/tex/latex.php b/filter/tex/latex.php
index 7c6b3665b053d..4c9343fc6ff4b 100644
--- a/filter/tex/latex.php
+++ b/filter/tex/latex.php
@@ -143,7 +143,7 @@ function render($formula, $filename, $fontsize=12, $density=240, $background='',
// Run convert on document (.ps to .gif/.png) or run dvisvgm (.ps to .svg).
if ($background) {
- $bg_opt = "-transparent \"$background\""; // Makes transparent background
+ $bg_opt = '-transparent ' . escapeshellarg($background); // Makes transparent background
} else {
$bg_opt = "";
}
diff --git a/filter/tex/lib.php b/filter/tex/lib.php
index a1d296ab9e43a..86f7ab967109f 100644
--- a/filter/tex/lib.php
+++ b/filter/tex/lib.php
@@ -26,10 +26,22 @@
defined('MOODLE_INTERNAL') || die();
+// Default timeout in seconds for mimetex command execution.
+defined('FILTER_TEX_MIMETEX_TIMEOUT') || define('FILTER_TEX_MIMETEX_TIMEOUT', 5);
+
+/**
+ * Check if the current operating system is Windows.
+ *
+ * @return bool True if running on Windows, false otherwise.
+ */
+function filter_tex_is_windows(): bool {
+ return (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows");
+}
+
function filter_tex_get_executable($debug=false) {
global $CFG;
- if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
+ if (filter_tex_is_windows()) {
return "$CFG->dirroot/filter/tex/mimetex.exe";
}
@@ -134,7 +146,7 @@ function filter_tex_get_cmd($pathname, $texexp) {
$texexp = escapeshellarg($texexp);
$executable = filter_tex_get_executable(false);
- if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
+ if (filter_tex_is_windows()) {
$executable = str_replace(' ', '^ ', $executable);
return "$executable ++ -e \"$pathname\" -- $texexp";
@@ -143,6 +155,173 @@ function filter_tex_get_cmd($pathname, $texexp) {
}
}
+/**
+ * Run mimetex command with a timeout on Windows.
+ *
+ * @param string $cmd Command string to execute.
+ * @param int $timeoutmicros Timeout in microseconds.
+ * @return array Array with keys: code, timedout, status, errors.
+ */
+function filter_tex_exec_windows(string $cmd, int $timeoutmicros): array {
+ // Create temporary file for stderr.
+ $temperr = tempnam(sys_get_temp_dir(), 'err_');
+
+ $descriptors = [
+ 0 => ['file', 'NUL', 'r'], // STDIN.
+ 1 => ['file', 'NUL', 'w'], // STDOUT.
+ 2 => ['file', $temperr, 'w'], // STDERR.
+ ];
+
+ $process = proc_open($cmd, $descriptors, $pipes);
+ if (!is_resource($process)) {
+ unlink($temperr);
+ return [
+ 'code' => 127, // Command not found.
+ 'timedout' => false,
+ 'status' => [],
+ 'errors' => '',
+ ];
+ }
+
+ $timedout = false;
+ while ($timeoutmicros > 0) {
+ $start = microtime(true);
+ $status = proc_get_status($process);
+
+ if (!$status['running']) {
+ break;
+ }
+
+ $timeoutmicros -= (microtime(true) - $start) * 1000000;
+ if ($timeoutmicros <= 0) {
+ $timedout = true;
+ $pid = (int)($status['pid'] ?? 0);
+ exec('taskkill /F /T /PID ' . $pid . ' 2>NUL');
+ break;
+ }
+
+ usleep(50000); // Sleep for 50ms.
+ }
+
+ $status = proc_get_status($process);
+ $code = proc_close($process);
+
+ // Capture stderr from temp file.
+ $errors = file_get_contents($temperr);
+ unlink($temperr);
+
+ return [
+ 'code' => $code,
+ 'timedout' => $timedout,
+ 'status' => $status,
+ 'errors' => $errors,
+ ];
+}
+
+/**
+ * Run mimetex command with a timeout on Unix-like systems.
+ *
+ * @param string $cmd Command string to execute.
+ * @param int $timeoutmicros Timeout in microseconds.
+ * @return array Array with keys: code, timedout, status, errors.
+ */
+function filter_tex_exec_unix(string $cmd, int $timeoutmicros): array {
+ // File descriptors passed to the process.
+ $descriptors = [
+ 0 => ['pipe', 'r'], // STDIN.
+ 1 => ['pipe', 'w'], // STDOUT.
+ 2 => ['pipe', 'w'], // STDERR.
+ ];
+
+ $process = proc_open('exec ' . $cmd, $descriptors, $pipes);
+ if (!is_resource($process)) {
+ return [
+ 'code' => 127, // Command not found.
+ 'timedout' => false,
+ 'status' => [],
+ 'errors' => '',
+ ];
+ }
+
+ fclose($pipes[0]);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+
+ $errors = '';
+ $timedout = false;
+ while ($timeoutmicros > 0) {
+ $start = microtime(true);
+
+ $read = [$pipes[1], $pipes[2]];
+ $other = [];
+ stream_select($read, $other, $other, 0, (int)$timeoutmicros);
+
+ $status = proc_get_status($process);
+
+ stream_get_contents($pipes[1]); // Discard stdout to prevent pipe blocking.
+ $errors .= stream_get_contents($pipes[2]);
+
+ if (!$status['running']) {
+ break;
+ }
+
+ $timeoutmicros -= (microtime(true) - $start) * 1000000;
+ if ($timeoutmicros <= 0) {
+ $timedout = true;
+ proc_terminate($process);
+ break;
+ }
+
+ usleep(50000); // Sleep for 50ms.
+ }
+
+ // Read any remaining data from pipes.
+ stream_get_contents($pipes[1]); // Discard remaining stdout.
+ $errors .= stream_get_contents($pipes[2]);
+
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+
+ $status = proc_get_status($process);
+ $code = proc_close($process);
+
+ return [
+ 'code' => $code,
+ 'timedout' => $timedout,
+ 'status' => $status,
+ 'errors' => $errors,
+ ];
+}
+
+/**
+ * Run mimetex command with a timeout.
+ *
+ * @param string $cmd Command string to execute.
+ * @param int|null &$code Exit code (passed by reference, set by function).
+ * @return void
+ */
+function filter_tex_exec(string $cmd, ?int &$code): void {
+ $timeoutmicros = FILTER_TEX_MIMETEX_TIMEOUT * 1000000;
+
+ if (filter_tex_is_windows()) {
+ $result = filter_tex_exec_windows($cmd, $timeoutmicros);
+ } else {
+ $result = filter_tex_exec_unix($cmd, $timeoutmicros);
+ }
+
+ if ($result['errors']) {
+ debugging('filter_tex_exec errors: ' . $result['errors'], DEBUG_DEVELOPER);
+ }
+
+ if ($result['timedout']) {
+ $code = 124;
+ } else if ($result['code'] === -1 && isset($result['status']['exitcode']) && $result['status']['exitcode'] !== -1) {
+ $code = $result['status']['exitcode'];
+ } else {
+ $code = $result['code'];
+ }
+}
+
/**
* Purge all caches when settings changed.
*/
diff --git a/filter/tex/pix.php b/filter/tex/pix.php
index e4697cf48fff4..a30317aef4d6e 100644
--- a/filter/tex/pix.php
+++ b/filter/tex/pix.php
@@ -18,7 +18,6 @@
require_once($CFG->dirroot.'/filter/tex/latex.php');
$cmd = ''; // Initialise these variables
- $status = '';
$relativepath = get_file_argument();
@@ -58,7 +57,7 @@
$texexp = preg_replace('!\r\n?!', ' ', $texexp);
$texexp = '\Large '.$texexp;
$cmd = filter_tex_get_cmd($pathname, $texexp);
- system($cmd, $status);
+ filter_tex_exec($cmd, $status);
}
}
}
diff --git a/filter/tex/settings.php b/filter/tex/settings.php
index e0d5ddce68f8b..38a4fd255359e 100644
--- a/filter/tex/settings.php
+++ b/filter/tex/settings.php
@@ -33,7 +33,7 @@
$items[] = new admin_setting_heading('filter_tex/latexheading', get_string('latexsettings', 'filter_tex'), '');
$items[] = new admin_setting_configtextarea('filter_tex/latexpreamble', get_string('latexpreamble','filter_tex'),
'', "\\usepackage[latin1]{inputenc}\n\\usepackage{amsmath}\n\\usepackage{amsfonts}\n\\RequirePackage{amsmath,amssymb,latexsym}\n");
- $items[] = new admin_setting_configtext('filter_tex/latexbackground', get_string('backgroundcolour', 'admin'), '', '#FFFFFF');
+ $items[] = new admin_setting_configcolourpicker('filter_tex/latexbackground', get_string('backgroundcolour', 'admin'), '', '#FFFFFF');
$items[] = new admin_setting_configtext('filter_tex/density', get_string('density', 'admin'), '', '120', PARAM_INT);
$default_filter_tex_pathlatex = '';
diff --git a/filter/tex/texdebug.php b/filter/tex/texdebug.php
index 25481b3101e17..6570ac64cd814 100644
--- a/filter/tex/texdebug.php
+++ b/filter/tex/texdebug.php
@@ -153,7 +153,7 @@ function tex2image($texexp, $return=false) {
$texexp = '\Large '.$texexp;
$commandpath = filter_tex_get_executable(true);
$cmd = filter_tex_get_cmd($pathname, $texexp);
- system($cmd, $status);
+ filter_tex_exec($cmd, $status);
if ($return) {
return $image;
@@ -172,6 +172,10 @@ function tex2image($texexp, $return=false) {
echo "Status corresponds to bus error
\n";
} else if ($status == 22) {
echo "Status corresponds to abnormal termination
\n";
+ } else if ($status == 124) {
+ echo "Status corresponds to timeout
\n";
+ } else if ($status == 127) {
+ echo "Status corresponds to command not found
\n";
}
if (file_exists($commandpath)) {
echo "File size of mimetex executable $commandpath is " . filesize($commandpath) . "
";
diff --git a/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php b/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
index 7c90524c67f4f..c122e9cd3e03e 100644
--- a/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
+++ b/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
@@ -97,6 +97,55 @@ public function i_define_the_following_marking_guide(TableNode $guide) {
}
}
+ /**
+ * Edits an existing marking guide with the provided data.
+ *
+ * This method edits the marking guide of the marking guide definition
+ * form; the provided TableNode should contain one row for
+ * each field and each cell of the row should contain:
+ * | Field name | New value |
+ * | shortname | Updated Grade criterion |
+ *
+ * @When /^I edit the marking guide criterion "([^"]*)" with the following values:$/
+ * @param string $criterionname
+ * @param TableNode $fields
+ */
+ public function i_edit_the_marking_guide_criterion_with_the_following_values(string $criterionname, TableNode $fields) {
+ if ($fieldvalues = $fields->getHash()) {
+ $criterionid = 0;
+ $locator = "//tr[contains(@class, 'criterion')]//div[@class='criterionname']"
+ . "//span[@class='textvalue'][text()='$criterionname']/ancestor::tr";
+ if ($criterionrow = $this->find('xpath', $locator)) {
+ $criterionid = str_replace('guide-criteria-', '', $criterionrow->getAttribute('id'));
+ }
+
+ if ($criterionid) {
+ $criterionroot = 'guide[criteria]' . '[' . $criterionid . ']';
+
+ foreach ($fieldvalues as $fieldvalue) {
+ // Make sure the fieldvalue array has 2 elements.
+ if (count($fieldvalue) != 2) {
+ throw new ExpectationException(
+ 'The field definition should contain field name and new value. ' .
+ 'Please follow this format: | Field name | New value |',
+ $this->getSession()
+ );
+ }
+
+ $fieldname = $fieldvalue['Field name'];
+ $newvalue = $fieldvalue['New value'];
+
+ $this->set_guide_field_value($criterionroot . "[$fieldname]", $newvalue);
+ }
+ } else {
+ throw new ExpectationException(
+ 'Criterion with name "' . $criterionname . '" not found.',
+ $this->getSession()
+ );
+ }
+ }
+ }
+
/**
* Defines the marking guide with the provided data, following marking guide's definition grid cells.
*
diff --git a/grade/grading/form/guide/tests/behat/edit_marking_guide_detail.feature b/grade/grading/form/guide/tests/behat/edit_marking_guide_detail.feature
new file mode 100644
index 0000000000000..0c7d8dfa6d83c
--- /dev/null
+++ b/grade/grading/form/guide/tests/behat/edit_marking_guide_detail.feature
@@ -0,0 +1,86 @@
+@gradingform @gradingform_guide
+Feature: Editing a marking guide already used for grading updates regrade state and student visible grades
+ In order to update marking guide details
+ As a teacher
+ I need to be able to grade a submission
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | One | teacher1@example.com |
+ | student1 | Student | One | student1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And the following "activities" exist:
+ | activity | course | name | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled |
+ | assign | C1 | Assign 1 | guide | 1 |
+ And the following "mod_assign > submissions" exist:
+ | assign | user | onlinetext |
+ | Assign 1 | student1 | Assign 1 submission |
+ And I am on the "Course 1" course page logged in as teacher1
+ And I go to "Assign 1" advanced grading definition page
+ And I set the following fields to these values:
+ | Name | Assign 1 marking guide |
+ | Description | Marking guide description |
+ And I define the following marking guide:
+ | Criterion name | Description for students | Description for markers | Maximum score |
+ | Grade criterion A | Grade 1 description for students | Grade 1 description for markers | 70 |
+ | Grade criterion B | Grade 2 description for students | Grade 2 description for markers | 30 |
+ And I press "Save marking guide and make it ready"
+ And I navigate to "Assignment" in current page administration
+ And I go to "Student One" "Assign 1" activity advanced grading page
+ And I grade by filling the marking guide with:
+ | Grade criterion A | 25 | Needs improvement |
+ | Grade criterion B | 20 | Excellent! |
+ And I press "Save changes"
+ And I am on "Course 1" course homepage
+ And I go to "Assign 1" advanced grading definition page
+ And I edit the marking guide criterion "Grade criterion A" with the following values:
+ | Field name | New value |
+ | shortname | Updated Grade criterion A |
+ | description | Updated description for students |
+ | descriptionmarkers | Updated description for markers |
+ | maxscore | 60 |
+ And I press "Save"
+
+ @javascript
+ Scenario: Teacher edits a used marking guide and mark it for regrade and student sees breakdown only after teacher regrades
+ # Set the marking guide to be "Mark for regrade".
+ Given I set the field "menuguideregrade" to "Mark for regrade"
+ And I should see "You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded."
+ And I click on "Continue" "button"
+ And I am on the "Assign 1" "assign activity" page logged in as student1
+ # Student should not see the grade breakdown as the activity is marked for regrade.
+ And I should not see "Grade breakdown"
+ When I am on the "Assign 1" "assign activity" page logged in as teacher1
+ And I go to "Student One" "Assign 1" activity advanced grading page
+ And I grade by filling the marking guide with:
+ | Updated Grade criterion A | 50 | I changed my mind |
+ | Grade criterion B | 30 | It's all good now |
+ And I press "Save changes"
+ And I am on the "Assign 1" "assign activity" page logged in as student1
+ # Now the student should see the updated grade breakdown.
+ Then I should see "Grade breakdown"
+ And I should see the marking guide information displayed as:
+ | criteria | description | remark | maxscore | criteriascore |
+ | Updated Grade criterion A | Updated description for students | I changed my mind | 60 | 50 / 60 |
+ | Grade criterion B | Grade 2 description for students | It's all good now | 30 | 30 / 30 |
+
+ @javascript
+ Scenario: Teacher edits a used marking guide does not mark for regrade existing student grades remain visible and unchanged
+ # Set the marking guide to be "Do not mark for regrade".
+ Given I set the field "menuguideregrade" to "Do not mark for regrade"
+ And I should see "You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded."
+ And I click on "Continue" "button"
+ When I am on the "Assign 1" "assign activity" page logged in as student1
+ # Student should see the updated grade breakdown as the activity was not marked for regrading.
+ Then I should see "Grade breakdown"
+ And I should see the marking guide information displayed as:
+ | criteria | description | remark | maxscore | criteriascore |
+ | Updated Grade criterion A | Updated description for students | Needs improvement | 60 | 25 / 60 |
+ | Grade criterion B | Grade 2 description for students | Excellent! | 30 | 20 / 30 |
diff --git a/grade/tests/behat/grade_aggregation.feature b/grade/tests/behat/grade_aggregation.feature
index 7329d47d40126..09222de71d4e7 100644
--- a/grade/tests/behat/grade_aggregation.feature
+++ b/grade/tests/behat/grade_aggregation.feature
@@ -250,7 +250,8 @@ Feature: We can use calculated grade totals
| Aggregation | Natural |
| Include outcomes in aggregation | 1 |
| Exclude empty grades | 0 |
- And I change window size to "large"
+ # Change window size to ultra-wide to avoid 'element-click-intercepted' failures.
+ And I change window size to "5120x2160"
And I navigate to "View > Grader report" in the course gradebook
And I give the grade "Excellent" to the user "Student 1" for the grade item "Test outcome item one"
And I press "Save changes"
@@ -305,7 +306,8 @@ Feature: We can use calculated grade totals
And I set the following settings for grade item "Test outcome item one" of type "gradeitem" on "setup" page:
| Weight adjusted | 1 |
| aggregationcoef2 | 100 |
- And I change window size to "large"
+ # Change window size to ultra-wide to avoid 'element-click-intercepted' failures.
+ And I change window size to "5120x2160"
And I navigate to "View > Grader report" in the course gradebook
And I give the grade "Excellent" to the user "Student 1" for the grade item "Test outcome item one"
And I press "Save changes"
diff --git a/grade/tests/behat/grade_view.feature b/grade/tests/behat/grade_view.feature
index 278ce753c0763..0bec74849b8ce 100644
--- a/grade/tests/behat/grade_view.feature
+++ b/grade/tests/behat/grade_view.feature
@@ -1,4 +1,4 @@
-@core @core_grades @javascript
+@core @core_grades
Feature: We can enter in grades and view reports from the gradebook
In order to check the expected results are displayed
As a teacher
@@ -46,6 +46,7 @@ Feature: We can enter in grades and view reports from the gradebook
And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
And I press "Save changes"
+ @javascript
Scenario: Grade a grade item and ensure the results display correctly in the gradebook
When I navigate to "View > User report" in the course gradebook
And the "Gradebook navigation menu" select menu should contain "Grader report"
@@ -68,6 +69,7 @@ Feature: We can enter in grades and view reports from the gradebook
And "Course 1" row "Grade" column of "overview-grade" table should contain "170.00"
And "Course 1" row "Grade" column of "overview-grade" table should not contain "90.00"
+ @javascript
Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
When I navigate to "Setup > Gradebook setup" in the course gradebook
And I set the following settings for grade item "Course 1" of type "course" on "setup" page:
@@ -92,3 +94,8 @@ Feature: We can enter in grades and view reports from the gradebook
| Test assignment name 1 | 0.72% | 0.72% |
| Test assignment name 2 | 1.00% | 1.00% |
| Course total | 1.00% | 1.00% |
+
+ Scenario: User not enrolled in any course should not see the grades report
+ Given I log in as "admin"
+ When I follow "Grades" in the user menu
+ Then I should see "You are not enrolled in, nor teaching any courses on this site."
diff --git a/group/templates/comboboxsearch/resultitem.mustache b/group/templates/comboboxsearch/resultitem.mustache
index 2dd40d997d28c..355362a6e8ebc 100644
--- a/group/templates/comboboxsearch/resultitem.mustache
+++ b/group/templates/comboboxsearch/resultitem.mustache
@@ -30,11 +30,11 @@
}}
{{
+
-
-
+
+
{{name}}
diff --git a/install/lang/hi/admin.php b/install/lang/hi/admin.php
index dfaaf2117cb66..a76284d54853f 100644
--- a/install/lang/hi/admin.php
+++ b/install/lang/hi/admin.php
@@ -39,3 +39,6 @@
{$a}
कृपया --help विकल्प का उपयोग करें।';
$string['cliyesnoprompt'] = 'टाइप Y (का मतलब है हाँ) या N (का मतलब है नहीं )';
+$string['environmentrequireinstall'] = 'स्थापित और सक्षम होना चाहिए';
+$string['environmentrequireversion'] = 'version {$a->needed} की आवश्यकता है और आप {$a-> current} चला रहे हैं';
+$string['upgradekeyset'] = 'उन्नयन कुंजी (इसे सेट न करने के लिए खाली छोड़ दें)';
diff --git a/install/lang/hi/error.php b/install/lang/hi/error.php
new file mode 100644
index 0000000000000..147eb2f5595c1
--- /dev/null
+++ b/install/lang/hi/error.php
@@ -0,0 +1,50 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['cannotcreatedboninstall'] = ' डेटाबेस नहीं बना सकता है।
निर्दिष्ट डेटाबेस मौजूद नहीं है और दिए गए उपयोगकर्ता के पास डेटाबेस बनाने की अनुमति नहीं है।
साइट प्रशासक को डेटाबेस विन्यास को सत्यापित करना चाहिए।
';
+$string['cannotcreatelangdir'] = 'लैंग निर्देशिका नहीं बना सकता है';
+$string['cannotcreatetempdir'] = 'अस्थायी निर्देशिका नहीं बना सकता है';
+$string['cannotdownloadcomponents'] = 'घटकों को डाउनलोड नहीं कर सकते';
+$string['cannotdownloadzipfile'] = 'ज़िप दाखिल करना डाउनलोड नहीं कर सकते';
+$string['cannotfindcomponent'] = 'अवयव नहीं मिल रहा है';
+$string['cannotsavemd5file'] = 'md5 दाखिल करना को सहेज नहीं सकते';
+$string['cannotsavezipfile'] = 'ज़िप दाखिल करना को सहेज नहीं सकते';
+$string['cannotunzipfile'] = 'दाखिल करना को अनज़िप नहीं कर सकते';
+$string['componentisuptodate'] = 'अवयव अद्यतित है';
+$string['dmlexceptiononinstall'] = 'डेटाबेस में त्रुटि आ गई है [{$a->errorcode}]।
{$a->debuginfo}
';
+$string['downloadedfilecheckfailed'] = 'डाउनलोड की गई दाखिल करना जाँच विफल रही';
+$string['invalidmd5'] = 'चेक चर गलत था-फिर से कोशिश करें';
+$string['missingrequiredfield'] = 'कुछ अपेक्षित क्षेत्र गायब है';
+$string['remotedownloaderror'] = 'आपके सर्वर पर कंपोनेंट डाउनलोड करने में विफलता मिली। कृपया प्रॉक्सी सेटिंग्स की जाँच करें; PHP cURL एक्सटेंशन का उपयोग करने की अत्यधिक अनुशंसा की जाती है।
+आपको {$a->url} फ़ाइल को मैन्युअल रूप से डाउनलोड करना होगा, इसे अपने सर्वर के "{$a->dest}" फ़ोल्डर में कॉपी करना होगा और वहाँ इसे अनज़िप करना होगा।
';
+$string['wrongdestpath'] = 'गलत लक्ष्य मार्ग';
+$string['wrongsourcebase'] = 'गलत उद्गम URL मूल';
+$string['wrongzipfilename'] = 'गलत ज़िप दाखिल करना नाम';
diff --git a/install/lang/hi/install.php b/install/lang/hi/install.php
new file mode 100644
index 0000000000000..e13abe6fcc63c
--- /dev/null
+++ b/install/lang/hi/install.php
@@ -0,0 +1,101 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['admindirname'] = 'प्रशासक निर्देशिका';
+$string['availablelangs'] = 'उपलब्ध भाषा पैक';
+$string['chooselanguagehead'] = 'एक भाषा चुनें';
+$string['chooselanguagesub'] = 'कृपया अधिष्ठापन के लिए एक भाषा चुनें। इस भाषा का उपयोग साइट के लिए चूकना भाषा के रूप में भी किया जाएगा, हालांकि इसे बाद में बदला जा सकता है।';
+$string['clialreadyconfigured'] = 'कॉन्फ़िगरेशन फ़ाइल config.php पहले से मौजूद है। कृपया इस साइट के लिए Moodle इंस्टॉल करने के लिए admin/cli/install_database.php का उपयोग करें।';
+$string['clialreadyinstalled'] = 'कॉन्फ़िगरेशन फ़ाइल config.php पहले से मौजूद है। कृपया इस साइट के लिए Moodle इंस्टॉल करने के लिए admin/cli/install_database.php का उपयोग करें।';
+$string['cliinstallheader'] = 'मूडल {$a} कमांड लाइन अधिष्ठापन कार्यक्रम';
+$string['clitablesexist'] = 'डेटाबेस तालिकाएँ पहले से मौजूद हैं; CLI अधिष्ठापन जारी नहीं रह सकती है।';
+$string['databasehost'] = 'डेटाबेस मेजबान';
+$string['databasename'] = 'डेटाबेस का नाम';
+$string['databasetypehead'] = 'डेटाबेस चालक चुनें';
+$string['dataroot'] = 'डेटा निर्देशिका';
+$string['datarootpermission'] = 'डेटा निर्देशिकाओं की अनुमति';
+$string['dbprefix'] = 'तालिका उपसर्ग';
+$string['dirroot'] = 'मूडल निर्देशिका';
+$string['environmenthead'] = 'अपने पर्यावरण की जाँच करें।....';
+$string['environmentsub2'] = 'प्रत्येक मूडल रिलीज में कुछ न्यूनतम, कम से कम PHP संस्करण अपेक्षा, आवश्यकता और कई अनिवार्य पी. एच. पी. विस्तार होते हैं। प्रत्येक स्थापना और उन्नयन करना से पहले पूर्ण पर्अथवावरण जांच की जाती है। यदि आप नहीं जानकारी कि नअथवा संस्करण कैसे स्थापित करना है अथवा PHP विस्तार को सक्षम करना है तो कृपअथवा सर्वर प्रशासक से संपर्क करें।';
+$string['errorsinenvironment'] = 'पर्यावरण जांच विफल रही!';
+$string['installation'] = 'अधिष्ठापन';
+$string['langdownloaderror'] = 'दुर्भाग्य से "{$a}" भाषा डाउनलोड नहीं की जा सकी। अधिष्ठापन प्रक्रम अंग्रेजी में जारी रहेगी।';
+$string['memorylimithelp'] = 'आपके सर्वर के लिए PHP मेमोरी सीमा वर्तमान में {$a} पर सेट है।
+
+इससे Moodle को बाद में मेमोरी संबंधी समस्याएँ हो सकती हैं, विशेष रूप से
+
+यदि आपने कई मॉड्यूल सक्रिय किए हैं और/या कई उपयोगकर्ता हैं।
+
+हमारा सुझाव है कि यदि संभव हो तो आप PHP को उच्च सीमा, जैसे 40M, पर कॉन्फ़िगर करें।
+
+ऐसा करने के कई तरीके हैं जिन्हें आप आज़मा सकते हैं:
+
+- यदि संभव हो, तो PHP को --enable-memory-limit के साथ पुनः संकलित करें।
+
+इससे Moodle स्वयं मेमोरी सीमा निर्धारित कर सकेगा।
+- यदि आपके पास अपनी php.ini फ़ाइल तक पहुँच है, तो आप उसमें memory_limit
+सेटिंग को 40M जैसी किसी मान पर बदल सकते हैं।
यदि आपके पास पहुँच नहीं है, तो आप
+अपने व्यवस्थापक से यह करने के लिए कह सकते हैं।
+- कुछ PHP सर्वरों पर, आप Moodle निर्देशिका में एक .htaccess फ़ाइल बना सकते हैं,
+
+जिसमें यह पंक्ति हो:
+
+
php_value memory_limit 40M
+हालाँकि, कुछ सर्वरों पर यह सभी PHP पृष्ठों को काम करने से रोक देगा
+(पृष्ठों को देखते समय आपको त्रुटियाँ दिखाई देंगी), इसलिए आपको .htaccess फ़ाइल को हटाना होगा।
+
';
+$string['paths'] = 'रास्ते';
+$string['pathserrcreatedataroot'] = 'डेटा निर्देशिका ({$a-> dataroot}) इंस्टॉलर द्वारा नहीं बनाई जा सकती है।';
+$string['pathshead'] = 'रास्तों की पुष्टि';
+$string['pathsrodataroot'] = 'डेटारूट निर्देशिका लिखित नहीं है।';
+$string['pathsroparentdataroot'] = 'मूल निर्देशिका ({$a-> parent}) लिखने योग्य नहीं है। डेटा निर्देशिका ({$a-> dataroot}) इंस्टॉलर द्वारा नहीं बनाई जा सकती है।';
+$string['pathssubadmindir'] = 'बहुत कम वेबहोस्ट नियंत्रण कक्ष अथवा किसी चीज़ तक पहुँचने के लिए आपके लिए एक विशेष URL के रूप में प्रशासक का उपयोग करते हैं। दुर्भाग्य से यह मूडल प्रशासक पृष्ठों के लिए स्तर स्थान के साथ संघर्ष करता है। आप अपने इंस्टॉलेशन में प्रशासक निर्देशिका का नाम बदलकर और उस नए नाम को यहाँ रखकर इसे निश्चित करना सकते हैं। उदाहरण के लिए: moodleadmin यह मूडल में प्रशासक लिंक को निश्चित करनाेगा।';
+$string['pathssubdataroot'] = ' एक निर्देशिका जहाँ मूडल उपयोगकर्ताओं द्वारा अपलोड की गई सभी दाखिल करना सामग्री को संग्रहीत करेगा।
यह निर्देशिका वेब सर्वर उपयोगकर्ता (आमतौर पर \'www-data\', \'कोई नहीं\' अथवा \'अपाचे\') द्वारा पढ़ने योग्य और लिखने योग्य दोनों होनी चाहिए।
यह सीधे वेब पर सुलभ नहीं होना चाहिए।
यदि निर्देशिका वर्तमान में मौजूद नहीं है, तो अधिष्ठापन प्रक्रम इसे बनाने का प्रअथवास करेगी।
';
+$string['pathssubdirroot'] = ' मॉड्यूल कोड वाली निर्देशिका का पूरा मार्ग।
';
+$string['pathssubwwwroot'] = ' पूरा सम्बोधन जहाँ मूडल तक पहुँचा जाएगा i.e। वह सम्बोधन जिसे उपयोगकर्ता मूडल तक पहुँचने के लिए अपने ब्राउज़र के पते की पट्टी में दर्ज करेंगे।
कई पतों का उपयोग करके मूडल तक पहुँचना संभव नहीं है। यदि आपकी साइट कई पतों के माध्यम से सुलभ है तो सबसे आसान का चयन करें और अन्य पतों में से प्रत्येक के लिए एक स्थायी पुनर्निर्देश स्थापित करें।
यदि आपकी साइट इंटरनेट और आंतरिक नेटवर्क (जिसे कभी-कभी इंट्रानेट कहा जाता है) दोनों से सुलभ है, तो यहाँ सार्वजनिक पते का उपयोग करें।
यदि चालू, प्रचलित सम्बोधन सही नहीं है, तो कृपया अपने ब्राउज़र के पते की पट्टी में यूआरएल बदलें और स्थापना को फिर से शुरू करें।
';
+$string['pathsunsecuredataroot'] = 'डेटारूट स्थान सुरक्षित नहीं है';
+$string['pathswrongadmindir'] = 'प्रशासक निर्देशिका मौजूद नहीं है';
+$string['phpextension'] = '{$a} PHP विस्तार';
+$string['phpversion'] = 'PHP संस्करण';
+$string['phpversionhelp'] = 'Moodle के लिए PHP का कम से कम 5.6.5 या 7.1 संस्करण आवश्यक है (7.0.x में कुछ इंजन संबंधी सीमाएँ हैं)।
+आप वर्तमान में {$a} संस्करण चला रहे हैं।
+आपको PHP को अपग्रेड करना होगा या PHP के नए संस्करण वाले होस्ट पर जाना होगा।
';
+$string['welcomep20'] = 'आप इस पृष्ठ को इसलिए देख रहे हैं क्योंकि आपने अपने कंप्यूटर में {$a->packname} {$a->packversion} पैकेज को सफलतापूर्वक स्थापित और लॉन्च कर लिया है।
+बधाई हो!';
+$string['welcomep30'] = 'इस {$a->installername} रिलीज़ में निम्नलिखित एप्लिकेशन शामिल हैं:
+एक ऐसा वातावरण बनाने के लिए जिसमें Moodle काम करेगा, अर्थात्:';
+$string['welcomep40'] = 'इस पैकेज में Moodle {$a->moodlerelease} ({$a->moodleversion}) भी शामिल है।';
+$string['welcomep50'] = 'इस पैकेज में शामिल सभी अनुप्रयोगों का उपयोग उनके संबंधित लाइसेंसों द्वारा नियंत्रित होता है। संपूर्ण {$a->installername} पैकेज ओपन सोर्स है और इसे GPL लाइसेंस के अंतर्गत वितरित किया जाता है।';
+$string['welcomep60'] = 'निम्नलिखित पृष्ठों में दिए गए आसान चरणों का पालन करके आप अपने कंप्यूटर पर मूडल को कॉन्फ़िगर और सेट अप कर सकते हैं।
+आप डिफ़ॉल्ट सेटिंग्स को स्वीकार कर सकते हैं या चाहें तो उन्हें अपनी आवश्यकताओं के अनुसार बदल सकते हैं।';
+$string['welcomep70'] = 'मूडल के सेटअप के साथ जारी रखने के लिए नीचे दिए गए "अगले" बटन पर क्लिक करें।';
+$string['wwwroot'] = 'वेब सम्बोधन';
diff --git a/install/lang/uz/error.php b/install/lang/uz/error.php
index f9a7a55e570ac..9bbf6768efa50 100644
--- a/install/lang/uz/error.php
+++ b/install/lang/uz/error.php
@@ -43,3 +43,10 @@
$string['componentisuptodate'] = 'Komponent yangilangan holda';
$string['dmlexceptiononinstall'] = 'Ma’lumotlar bazasida xatolik yuz berdi [{$a->errorcode}].
{$a->debuginfo}
';
$string['downloadedfilecheckfailed'] = 'Yuklab olingan faylni tekshirish muvaffaqiyatsiz tugadi';
+$string['invalidmd5'] = 'Tekshiruv o‘zgaruvchisi noto‘g‘ri — qayta urinib ko‘ring';
+$string['missingrequiredfield'] = 'Ba’zi talab qilingan maydonlar mavjud emas';
+$string['remotedownloaderror'] = 'Komponentni serveringizga yuklab olish muvaffaqiyatsiz bo‘ldi. Iltimos, proksi sozlamalarini tekshiring; PHP cURL kengaytmasi tavsiya etiladi.
+Siz {$a->url} faylini qo‘lda yuklab olishingiz, serveringizdagi "{$a->dest}" ga nusxalashingiz va u yerda arxivdan chiqarishingiz kerak.
';
+$string['wrongdestpath'] = 'Noto‘g‘ri manzil yo‘li.';
+$string['wrongsourcebase'] = 'Noto‘g‘ri manba URL bazasi.';
+$string['wrongzipfilename'] = 'Noto‘g‘ri ZIP fayl nomi.';
diff --git a/install/lang/uz/install.php b/install/lang/uz/install.php
index fe487e90576d6..8e1c382c48259 100644
--- a/install/lang/uz/install.php
+++ b/install/lang/uz/install.php
@@ -31,10 +31,44 @@
$string['admindirname'] = 'Admin katalogi';
$string['availablelangs'] = 'Mavjud til paketlari';
+$string['chooselanguagehead'] = 'Tilni tanlash';
+$string['chooselanguagesub'] = 'Iltimos, o‘rnatish uchun tilni tanlang. Ushbu til saytning standart tili sifatida ham ishlatiladi, ammo keyinchalik o‘zgartirish mumkin.';
+$string['clialreadyconfigured'] = 'config.php konfiguratsiya fayli allaqachon mavjud. Iltimos, ushbu sayt uchun Moodle’ni o‘rnatish uchun admin/cli/install_database.php faylidan foydalaning.';
+$string['clialreadyinstalled'] = 'config.php konfiguratsiya fayli allaqachon mavjud. Iltimos, ushbu sayt uchun Moodle’ni yangilash uchun admin/cli/install_database.php faylidan foydalaning.';
+$string['cliinstallheader'] = 'Moodle {$a} buyruq qatori o‘rnatish dasturi';
+$string['clitablesexist'] = 'Ma’lumotlar bazasi jadvallari allaqachon mavjud; CLI orqali o‘rnatish davom ettirib bo‘lmaydi.';
+$string['databasehost'] = 'Ma’lumotlar bazasi mezboni';
+$string['databasename'] = 'Ma’lumotlar bazasi nomi';
+$string['databasetypehead'] = 'Ma’lumotlar bazasi drayverini tanlang';
$string['dataroot'] = 'Ma’lumotlar katalogi';
+$string['datarootpermission'] = 'Data kataloglari uchun ruxsatlar';
$string['dbprefix'] = 'Jadvallar prefiksi';
$string['dirroot'] = 'Moodle katalogi';
+$string['environmenthead'] = 'Muhitingiz tekshirilmoqda ...';
+$string['environmentsub2'] = 'Har bir Moodle versiyasi minimal PHP versiyasi talabi va majburiy PHP kengaytmalariga ega.
+To‘liq muhit tekshiruvi har bir o‘rnatish yoki yangilashdan oldin amalga oshiriladi. Agar yangi versiyani o‘rnatish yoki PHP kengaytmalarini yoqishni bilmasangiz, server administratoriga murojaat qiling.';
+$string['errorsinenvironment'] = 'Muhitni tekshirish muvaffaqiyatsiz tugadi!';
$string['installation'] = 'O\'rnatish';
+$string['langdownloaderror'] = 'Afsuski, "{$a}" tili yuklab bo‘lmadi. O‘rnatish jarayoni ingliz tilida davom ettiriladi.';
+$string['paths'] = 'O‘tkazib yuborishlar';
+$string['pathserrcreatedataroot'] = 'Ma’lumotlar papkasi ({$a->dataroot}) o‘rnatish dasturi tomonidan yaratilolmayapti.';
+$string['pathshead'] = 'O’tkazib yuborishlarni tasdiqlash';
+$string['pathsrodataroot'] = 'Dataroot papkasiga yozish mumkin emas.';
+$string['pathsroparentdataroot'] = 'Ota papkaga ({$a->parent}) yozish mumkin emas. Shu sababli, ma’lumotlar papkasi ({$a->dataroot}) o‘rnatish dasturi tomonidan yaratilolmayapti.';
+$string['pathssubadmindir'] = 'Faqat juda kam web-hostlar sizga boshqaruv paneliga kirish uchun /admin URL’ini maxsus tarzda ishlatadi. Afsuski, bu Moodle administrator sahifalarining standart joylashuvi bilan to‘qnashadi. Buni tuzatish uchun o‘rnatishingizdagi admin papkasining nomini o‘zgartiring va yangi nomni bu yerga kiriting. Masalan: moodleadmin. Bu Moodle’dagi admin havolalarini tuzatadi.';
+$string['pathssubdataroot'] = 'Moodle foydalanuvchilar tomonidan yuklangan barcha fayl kontentini saqlaydigan papka.
+Ushbu papka veb-server foydalanuvchisi (odatda \'www-data\', \'nobody\' yoki \'apache\') tomonidan o‘qilishi va yozilishi mumkin bo‘lishi kerak.
+U to‘g‘ridan-to‘g‘ri veb orqali kirish mumkin bo‘lmasligi lozim.
+Agar papka hozir mavjud bo‘lmasa, o‘rnatish jarayoni uni yaratishga harakat qiladi.
';
+$string['pathssubdirroot'] = 'Moodle kodi joylashgan papkaning to‘liq yo‘li.
';
+$string['pathssubwwwroot'] = 'Moodle’ga kirish uchun to‘liq manzil, ya’ni foydalanuvchilar Moodle’ga kirish uchun brauzer manzil satriga kiritadigan manzil.
+Moodle’ga bir nechta manzil orqali kirish mumkin emas. Agar saytingiz bir nechta manzil orqali mavjud bo‘lsa, eng osonini tanlang va boshqa manzillar uchun doimiy yo‘naltirishni sozlang.
+Agar saytingiz Internetdan va ichki tarmoqdan (ba’zan Intranet deb ataladi) mavjud bo‘lsa, bu yerda jamoat manzilini ishlating.
+Agar hozirgi manzil noto‘g‘ri bo‘lsa, brauzeringiz manzil satrida URL’ni o‘zgartiring va o‘rnatishni qayta boshlang.
';
+$string['pathsunsecuredataroot'] = 'Dataroot joylashuvi xavfsiz emas.';
+$string['pathswrongadmindir'] = 'Administrator papkasi mavjud emas.';
+$string['phpextension'] = '{$a} PHP kengaytmasi';
+$string['phpversion'] = 'PHP versiyasi';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Siz bu sahifani ko‘rayotganingizning sababi, siz kompyuteringizda {$a->packname} {$a->packversion} paketini muvaffaqiyatli o‘rnatdingiz va ishga tushirdingiz. Tabriklaymiz!';
$string['welcomep30'] = '{$a->installername} ning ushbu relizi Moodle ishlaydigan muhitni yaratish uchun ilovalarni o‘z ichiga oladi, ya’ni:';
diff --git a/lang/en/admin.php b/lang/en/admin.php
index 2aaadcd58bfe0..a245812db7279 100644
--- a/lang/en/admin.php
+++ b/lang/en/admin.php
@@ -825,16 +825,12 @@
Your account with username {$a->username} on server \'{$a->sitename}\'
was locked out after multiple invalid login attempts.
-To unlock the account immediately go to the following address
+To unlock the account immediately, please click the link below:
-{$a->link}
+Unlock account
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then copy and paste the address into the address
-line at the top of your web browser window.
-If you need help, please contact the site administrator,
+If you need help, please contact the site administrator.
{$a->admin}';
$string['lockoutemailsubject'] = 'Your account on {$a} was locked out';
$string['lockouterrorunlock'] = 'Invalid account unlock information supplied.';
@@ -972,6 +968,7 @@
$string['maxtimelimit_desc'] = 'To restrict the maximum PHP execution time that Moodle will allow without any output being displayed, enter a value in seconds here. 0 means that Moodle default restrictions are used. If you have a front-end server with its own time limit, set this value lower to receive PHP errors in logs. Does not apply to CLI scripts.';
$string['moodlebrandedapp'] = 'Branded Moodle app';
$string['moodlebrandedappreference'] = 'Alternatively, get a Branded Moodle app with your own custom branding.';
+$string['moodlenetremovalwarning'] = 'The MoodleNet service will be shut down on 20 April 2026. If you wish to continue using MoodleNet on your site, install the MoodleNet plugin from the Moodle plugins directory and connect it to a self-hosted MoodleNet instance. Following this, the MoodleNet profile ID field will be removed; please migrate that data if you are using it for other purposes.';
$string['noreplyaddress'] = 'No-reply address';
$string['noreplydomain'] = 'No-reply and domain';
$string['noreplydomaindetail'] = 'Settings for No-reply and configured domains';
diff --git a/lang/en/auth.php b/lang/en/auth.php
index f5963af4f54bf..3431e01795d41 100644
--- a/lang/en/auth.php
+++ b/lang/en/auth.php
@@ -79,10 +79,14 @@
$string['emailupdate'] = 'Email address update';
$string['emailupdatemessage'] = 'Hi {$a->firstname},
-You have requested a change of your email address for your account on {$a->site}. To confirm this change, please go to the following web address:
+You have requested a change of your email address for your account on {$a->site}.
-{$a->url}
-The confirmation link will expire in 10 minutes.
+To confirm this change, please click the link below:
+
+Confirm email change
+
+
+The confirmation link will expire in 10 minutes.
{$a->supportemail}';
$string['emailupdatesuccess'] = 'Email address of user {$a->fullname} was successfully updated to {$a->email}.';
diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index 2088a1ceaacdd..d2d885c62f7ac 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -649,17 +649,12 @@
$string['emailconfirm'] = 'Confirm your account';
$string['emailconfirmation'] = 'Hi {$a->firstname},
-A new account has been requested at \'{$a->sitename}\'
-using your email address.
+A new account has been requested at \'{$a->sitename}\' using your email address.
-To confirm your new account, please go to this web address:
+To confirm your new account, please click the link below:
-{$a->link}
+Confirm your account
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then cut and paste the address into the address
-line at the top of your web browser window.
If you need help, please contact the site administrator,
{$a->admin}';
@@ -706,20 +701,14 @@
$string['emailonlyallowed'] = 'This email cannot be used. Allowed email domains are: {$a}.';
$string['emailpasswordconfirmation'] = 'Hi {$a->firstname},
-Someone (probably you) has requested a new password for your
-account on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'.
-To confirm this and have a new password sent to you via email,
-go to the following web address:
+To confirm this and have a new password sent to you via email, please click the link below:
-{$a->link}
+Get a new password
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then cut and paste the address into the address
-line at the top of your web browser window.
-If you need help, please contact the site administrator,
+If you need help, please contact the site administrator.
{$a->admin}';
$string['emailpasswordconfirmationsubject'] = '{$a}: Change password confirmation';
$string['emailpasswordconfirmmaybesent'] = 'If you supplied a correct username or unique email address then an email should have been sent to you.
@@ -735,19 +724,14 @@
If you continue to have difficulty, contact the site administrator.';
$string['emailpasswordchangeinfo'] = 'Hi {$a->firstname},
-Someone (probably you) has requested a new password for your
-account \'{$a->username}\' on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account \'{$a->username}\' on \'{$a->sitename}\'.
-To change your password, please go to the following web address:
+To change your password, please click the link below:
-{$a->link}
+Change password
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then cut and paste the address into the address
-line at the top of your web browser window.
-If you need help, please contact the site administrator,
+If you need help, please contact the site administrator.
{$a->admin}';
$string['emailpasswordchangeinfodisabled'] = 'Hi {$a->firstname},
@@ -764,14 +748,17 @@
A password reset was requested for your account \'{$a->username}\' at {$a->sitename}.
-To confirm this request, and set a new password for your account, please go to the following web address:
-{$a->link}
+To confirm this request, and set a new password for your account, please click the link below:
+
+Reset password
+
(This link is valid for {$a->resetminutes} minutes from the time this reset was first requested.)
If this password reset was not requested by you, no action is needed.
-If you need help, please contact the site administrator, {$a->admin}';
+If you need help, please contact the site administrator.
+{$a->admin}';
$string['emailresetconfirmationsubject'] = '{$a}: Password reset request';
$string['emailresetconfirmsent'] = 'An email has been sent to your address at {$a}.
It contains easy instructions to confirm and complete this password change.
@@ -1245,6 +1232,7 @@
If someone else has already chosen your username then you\'ll have to try again using a different username.
';
$string['loginto'] = 'Log in to {$a}';
$string['loginagain'] = 'Log in again';
+$string['loginrequired'] = 'Login required';
$string['logoof'] = 'Logo of {$a}';
$string['logout'] = 'Log out';
$string['logoutconfirm'] = 'Do you really want to log out?';
@@ -1512,10 +1500,10 @@
(You will be prompted to change your password when you log in for the first time.)
-To start using \'{$a->sitename}\', log in at
- {$a->link}
-If you need help, contact the site administrator,
+Click to log in and start using \'{$a->sitename}\'.
+
+If you need help, contact the site administrator.
{$a->signoff}';
$string['newusers'] = 'New users';
$string['newwindow'] = 'New window';
diff --git a/lib/UPGRADING.md b/lib/UPGRADING.md
index 23079ac5f31fb..e874eac6ae8d9 100644
--- a/lib/UPGRADING.md
+++ b/lib/UPGRADING.md
@@ -1,5 +1,13 @@
# core (subsystem) Upgrade notes
+## 4.5.9
+
+### Changed
+
+- `\core\output\core_renderer::confirm()`'s `$displayoptions` parameter now also accepts a `headinglevel` option that developers can use to specify the heading level of the confirmation's heading. If not specified, the confirmation heading will be rendered in an `h4` tag.
+
+ For more information see [MDL-87694](https://tracker.moodle.org/browse/MDL-87694)
+
## 4.5.8
### Changed
diff --git a/lib/adminlib.php b/lib/adminlib.php
index 6f92c68a615a1..29e90acc16a26 100644
--- a/lib/adminlib.php
+++ b/lib/adminlib.php
@@ -10525,13 +10525,13 @@ protected function validate($data) {
return $data;
} else if (in_array(strtolower($data), $colornames)) {
return $data;
- } else if (preg_match('/rgb\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\)/i', $data)) {
+ } else if (preg_match('/^rgb\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\)$/i', $data)) {
return $data;
- } else if (preg_match('/rgba\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\, ?\d(\.\d)?\)/i', $data)) {
+ } else if (preg_match('/^rgba\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\, ?\d(\.\d)?\)$/i', $data)) {
return $data;
- } else if (preg_match('/hsl\(\d{0,3}\, ?\d{0,3}%, ?\d{0,3}%\)/i', $data)) {
+ } else if (preg_match('/^hsl\(\d{0,3}\, ?\d{0,3}%, ?\d{0,3}%\)$/i', $data)) {
return $data;
- } else if (preg_match('/hsla\(\d{0,3}\, ?\d{0,3}%,\d{0,3}%\, ?\d(\.\d)?\)/i', $data)) {
+ } else if (preg_match('/^hsla\(\d{0,3}\, ?\d{0,3}%, ?\d{0,3}%\, ?\d(\.\d)?\)$/i', $data)) {
return $data;
} else if (($data == 'transparent') || ($data == 'currentColor') || ($data == 'inherit')) {
return $data;
diff --git a/lib/amd/build/edit_switch.min.js b/lib/amd/build/edit_switch.min.js
index 908684047c153..903b0d5b6868b 100644
--- a/lib/amd/build/edit_switch.min.js
+++ b/lib/amd/build/edit_switch.min.js
@@ -1,11 +1,10 @@
-define("core/edit_switch",["exports","core/ajax","core/event_dispatcher","core/notification"],(function(_exports,_ajax,_event_dispatcher,_notification){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.eventTypes=void 0;
+define("core/edit_switch",["exports","core/ajax","core/event_dispatcher","core/notification","core/pending"],(function(_exports,_ajax,_event_dispatcher,_notification,_pending){var obj;
/**
* Controls the edit switch.
*
* @module core/edit_switch
* @copyright 2021 Bas Brands
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-const eventTypes={editModeSet:"core/edit_switch/editModeSet"};_exports.eventTypes=eventTypes;const notifyEditModeSet=(container,editMode)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.editModeSet,{editMode:editMode},container,{cancelable:!0});_exports.init=editingSwitchId=>{const editSwitch=document.getElementById(editingSwitchId);editSwitch.addEventListener("change",(()=>{var context,setmode;(context=editSwitch.dataset.context,setmode=editSwitch.checked,(0,_ajax.call)([{methodname:"core_change_editmode",args:{context:context,setmode:setmode}}])[0]).then((result=>{result.success?(editSwitch=>{editSwitch.checked?editSwitch.setAttribute("aria-checked",!0):editSwitch.setAttribute("aria-checked",!1),notifyEditModeSet(editSwitch,editSwitch.checked).defaultPrevented||(editSwitch.setAttribute("disabled",!0),window.location=editSwitch.dataset.pageurl)})(editSwitch):editSwitch.checked=!1})).catch(_notification.exception)}))}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.eventTypes=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const eventTypes={editModeSet:"core/edit_switch/editModeSet"};_exports.eventTypes=eventTypes;const notifyEditModeSet=(container,editMode)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.editModeSet,{editMode:editMode},container,{cancelable:!0});_exports.init=editingSwitchId=>{const editSwitch=document.getElementById(editingSwitchId);editSwitch.addEventListener("change",(()=>{const pendingPromise=new _pending.default("core/edit_switch:toggle");var context,setmode;(context=editSwitch.dataset.context,setmode=editSwitch.checked,(0,_ajax.call)([{methodname:"core_change_editmode",args:{context:context,setmode:setmode}}])[0]).then((result=>{if(result.success){const redirected=(editSwitch=>(editSwitch.checked?editSwitch.setAttribute("aria-checked",!0):editSwitch.setAttribute("aria-checked",!1),!notifyEditModeSet(editSwitch,editSwitch.checked).defaultPrevented&&(editSwitch.setAttribute("disabled",!0),window.location=editSwitch.dataset.pageurl,!0)))(editSwitch);redirected||pendingPromise.resolve()}else editSwitch.checked=!1,pendingPromise.resolve()})).catch((error=>{pendingPromise.resolve(),(0,_notification.exception)(error)}))}))}}));
//# sourceMappingURL=edit_switch.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/edit_switch.min.js.map b/lib/amd/build/edit_switch.min.js.map
index ac217b8891133..c5b0c449002a7 100644
--- a/lib/amd/build/edit_switch.min.js.map
+++ b/lib/amd/build/edit_switch.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"edit_switch.min.js","sources":["../src/edit_switch.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 * Controls the edit switch.\n *\n * @module core/edit_switch\n * @copyright 2021 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {exception as displayException} from 'core/notification';\n\n/**\n * Change the Edit mode.\n *\n * @param {number} context The contextid that editing is being set for\n * @param {bool} setmode Whether editing is set or not\n * @return {Promise} Resolved with an array file the stored file url.\n */\nconst setEditMode = (context, setmode) => fetchMany([{\n methodname: 'core_change_editmode',\n args: {\n context,\n setmode,\n },\n}])[0];\n\n/**\n * Toggle the edit switch\n *\n * @method\n * @protected\n * @param {HTMLElement} editSwitch\n */\nconst toggleEditSwitch = editSwitch => {\n if (editSwitch.checked) {\n editSwitch.setAttribute('aria-checked', true);\n } else {\n editSwitch.setAttribute('aria-checked', false);\n }\n\n const event = notifyEditModeSet(editSwitch, editSwitch.checked);\n if (!event.defaultPrevented) {\n editSwitch.setAttribute('disabled', true);\n window.location = editSwitch.dataset.pageurl;\n }\n};\n\n/**\n * Names of events for core/edit_switch.\n *\n * @static\n * @property {String} editModeSet See {@link event:core/edit_switch/editModeSet}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the edit mode toggled.\n *\n * @event core/edit_switch/editModeSet\n * @type {CustomEvent}\n * @property {HTMLElement} target The switch used to toggle the edit mode\n * @property {object} detail\n * @property {bool} detail.editMode\n */\n editModeSet: 'core/edit_switch/editModeSet',\n};\n\n/**\n * Dispatch the editModeSet event after changing the edit mode.\n *\n * This event is cancelable.\n *\n * The default action is to reload the page after toggling the edit mode.\n *\n * @method\n * @protected\n * @param {HTMLElement} container\n * @param {bool} editMode\n * @returns {CustomEvent}\n */\nconst notifyEditModeSet = (container, editMode) => dispatchEvent(\n eventTypes.editModeSet,\n {editMode},\n container,\n {cancelable: true}\n);\n\n/**\n * Add the eventlistener for the editswitch.\n *\n * @param {string} editingSwitchId The id of the editing switch to listen for\n */\nexport const init = editingSwitchId => {\n const editSwitch = document.getElementById(editingSwitchId);\n editSwitch.addEventListener('change', () => {\n setEditMode(editSwitch.dataset.context, editSwitch.checked)\n .then(result => {\n if (result.success) {\n toggleEditSwitch(editSwitch);\n } else {\n editSwitch.checked = false;\n }\n return;\n })\n .catch(displayException);\n });\n};\n"],"names":["eventTypes","editModeSet","notifyEditModeSet","container","editMode","cancelable","editingSwitchId","editSwitch","document","getElementById","addEventListener","context","setmode","dataset","checked","methodname","args","then","result","success","setAttribute","defaultPrevented","window","location","pageurl","toggleEditSwitch","catch","displayException"],"mappings":";;;;;;;;MAqEaA,WAAa,CAUtBC,YAAa,qEAgBXC,kBAAoB,CAACC,UAAWC,YAAa,mCAC/CJ,WAAWC,YACX,CAACG,SAAAA,UACDD,UACA,CAACE,YAAY,kBAQGC,wBACVC,WAAaC,SAASC,eAAeH,iBAC3CC,WAAWG,iBAAiB,UAAU,KA3EtB,IAACC,QAASC,SAATD,QA4EDJ,WAAWM,QAAQF,QA5ETC,QA4EkBL,WAAWO,SA5EjB,cAAU,CAAC,CACjDC,WAAY,uBACZC,KAAM,CACFL,QAAAA,QACAC,QAAAA,YAEJ,IAuEKK,MAAKC,SACEA,OAAOC,QA/DEZ,CAAAA,aACjBA,WAAWO,QACXP,WAAWa,aAAa,gBAAgB,GAExCb,WAAWa,aAAa,gBAAgB,GAG9BlB,kBAAkBK,WAAYA,WAAWO,SAC5CO,mBACPd,WAAWa,aAAa,YAAY,GACpCE,OAAOC,SAAWhB,WAAWM,QAAQW,UAsD7BC,CAAiBlB,YAEjBA,WAAWO,SAAU,KAI5BY,MAAMC"}
\ No newline at end of file
+{"version":3,"file":"edit_switch.min.js","sources":["../src/edit_switch.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 * Controls the edit switch.\n *\n * @module core/edit_switch\n * @copyright 2021 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {exception as displayException} from 'core/notification';\nimport Pending from \"core/pending\";\n\n/**\n * Change the Edit mode.\n *\n * @param {number} context The contextid that editing is being set for\n * @param {bool} setmode Whether editing is set or not\n * @return {Promise} Resolved with an array file the stored file url.\n */\nconst setEditMode = (context, setmode) => fetchMany([{\n methodname: 'core_change_editmode',\n args: {\n context,\n setmode,\n },\n}])[0];\n\n/**\n * Toggle the edit switch\n *\n * @method\n * @protected\n * @param {HTMLElement} editSwitch\n */\nconst toggleEditSwitch = editSwitch => {\n if (editSwitch.checked) {\n editSwitch.setAttribute('aria-checked', true);\n } else {\n editSwitch.setAttribute('aria-checked', false);\n }\n\n const event = notifyEditModeSet(editSwitch, editSwitch.checked);\n if (!event.defaultPrevented) {\n editSwitch.setAttribute('disabled', true);\n window.location = editSwitch.dataset.pageurl;\n return true;\n }\n return false;\n};\n\n/**\n * Names of events for core/edit_switch.\n *\n * @static\n * @property {String} editModeSet See {@link event:core/edit_switch/editModeSet}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the edit mode toggled.\n *\n * @event core/edit_switch/editModeSet\n * @type {CustomEvent}\n * @property {HTMLElement} target The switch used to toggle the edit mode\n * @property {object} detail\n * @property {bool} detail.editMode\n */\n editModeSet: 'core/edit_switch/editModeSet',\n};\n\n/**\n * Dispatch the editModeSet event after changing the edit mode.\n *\n * This event is cancelable.\n *\n * The default action is to reload the page after toggling the edit mode.\n *\n * @method\n * @protected\n * @param {HTMLElement} container\n * @param {bool} editMode\n * @returns {CustomEvent}\n */\nconst notifyEditModeSet = (container, editMode) => dispatchEvent(\n eventTypes.editModeSet,\n {editMode},\n container,\n {cancelable: true}\n);\n\n/**\n * Add the eventlistener for the editswitch.\n *\n * @param {string} editingSwitchId The id of the editing switch to listen for\n */\nexport const init = editingSwitchId => {\n const editSwitch = document.getElementById(editingSwitchId);\n editSwitch.addEventListener('change', () => {\n const pendingPromise = new Pending(\"core/edit_switch:toggle\");\n setEditMode(editSwitch.dataset.context, editSwitch.checked)\n .then((result) => {\n if (result.success) {\n const redirected = toggleEditSwitch(editSwitch);\n if (!redirected) {\n pendingPromise.resolve();\n }\n } else {\n editSwitch.checked = false;\n pendingPromise.resolve();\n }\n return;\n })\n .catch((error) => {\n pendingPromise.resolve();\n displayException(error);\n });\n });\n};\n"],"names":["eventTypes","editModeSet","notifyEditModeSet","container","editMode","cancelable","editingSwitchId","editSwitch","document","getElementById","addEventListener","pendingPromise","Pending","context","setmode","dataset","checked","methodname","args","then","result","success","redirected","setAttribute","defaultPrevented","window","location","pageurl","toggleEditSwitch","resolve","catch","error"],"mappings":";;;;;;;sKAwEaA,WAAa,CAUtBC,YAAa,qEAgBXC,kBAAoB,CAACC,UAAWC,YAAa,mCAC/CJ,WAAWC,YACX,CAACG,SAAAA,UACDD,UACA,CAACE,YAAY,kBAQGC,wBACVC,WAAaC,SAASC,eAAeH,iBAC3CC,WAAWG,iBAAiB,UAAU,WAC5BC,eAAiB,IAAIC,iBAAQ,2BA9EvB,IAACC,QAASC,SAATD,QA+EDN,WAAWQ,QAAQF,QA/ETC,QA+EkBP,WAAWS,SA/EjB,cAAU,CAAC,CACjDC,WAAY,uBACZC,KAAM,CACFL,QAAAA,QACAC,QAAAA,YAEJ,IA0EKK,MAAMC,YACCA,OAAOC,QAAS,OACVC,WAnEGf,CAAAA,aACjBA,WAAWS,QACXT,WAAWgB,aAAa,gBAAgB,GAExChB,WAAWgB,aAAa,gBAAgB,IAG9BrB,kBAAkBK,WAAYA,WAAWS,SAC5CQ,mBACPjB,WAAWgB,aAAa,YAAY,GACpCE,OAAOC,SAAWnB,WAAWQ,QAAQY,SAC9B,IAwDoBC,CAAiBrB,YAC/Be,YACDX,eAAekB,eAGnBtB,WAAWS,SAAU,EACrBL,eAAekB,aAItBC,OAAOC,QACJpB,eAAekB,sCACEE"}
\ No newline at end of file
diff --git a/lib/amd/build/paged_content_paging_bar.min.js b/lib/amd/build/paged_content_paging_bar.min.js
index fb8a59c8caee1..17ef534570cea 100644
--- a/lib/amd/build/paged_content_paging_bar.min.js
+++ b/lib/amd/build/paged_content_paging_bar.min.js
@@ -5,6 +5,6 @@
* @copyright 2018 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("core/paged_content_paging_bar",["jquery","core/custom_interaction_events","core/paged_content_events","core/str","core/pubsub","core/pending"],(function($,CustomEvents,PagedContentEvents,Str,PubSub,Pending){var SELECTORS_PAGE="[data-page]",SELECTORS_PAGE_ITEM='[data-region="page-item"]',SELECTORS_PAGE_LINK='[data-region="page-link"]',SELECTORS_FIRST_BUTTON='[data-control="first"]',SELECTORS_LAST_BUTTON='[data-control="last"]',SELECTORS_NEXT_BUTTON='[data-control="next"]',SELECTORS_PREVIOUS_BUTTON='[data-control="previous"]',SELECTORS_DOTS_BUTTONS="[data-dots]",SELECTORS_BEGINNING_DOTS_BUTTON='[data-dots="beginning"]',SELECTORS_ENDING_DOTS_BUTTON='[data-dots="ending"]',getPageByNumber=function(root,pageNumber){return root.find(SELECTORS_PAGE_ITEM+'[data-page-number="'+pageNumber+'"]')},setLastPageNumber=function(root,number){root.attr("data-last-page-number",number)},getLastPageNumber=function(root){return parseInt(root.attr("data-last-page-number"),10)},getActivePageNumber=function(root){return parseInt(root.attr("data-active-page-number"),10)},setActivePageNumber=function(root,number){root.attr("data-active-page-number",number)},getPageNumber=function(root,page){if(null!=page.attr("data-page"))return parseInt(page.attr("data-page-number"),10);var pageNumber=1,activePageNumber=null;switch(page.attr("data-control")){case"first":default:pageNumber=1;break;case"last":pageNumber=getLastPageNumber(root);break;case"next":activePageNumber=getActivePageNumber(root);var lastPage=getLastPageNumber(root);pageNumber=lastPage?activePageNumber&&activePageNumber1?activePageNumber-1:1}return parseInt(pageNumber,10)},show=function(root){root.removeClass("hidden")},hide=function(root){root.addClass("hidden")},disableNextControlButtons=function(root){var nextButton=root.find(SELECTORS_NEXT_BUTTON),lastButton=root.find(SELECTORS_LAST_BUTTON);nextButton.addClass("disabled"),nextButton.attr("aria-disabled",!0),lastButton.addClass("disabled"),lastButton.attr("aria-disabled",!0)},enableNextControlButtons=function(root){var nextButton=root.find(SELECTORS_NEXT_BUTTON),lastButton=root.find(SELECTORS_LAST_BUTTON);nextButton.removeClass("disabled"),nextButton.removeAttr("aria-disabled"),lastButton.removeClass("disabled"),lastButton.removeAttr("aria-disabled")},disablePreviousControlButtons=function(root){var previousButton=root.find(SELECTORS_PREVIOUS_BUTTON),firstButton=root.find(SELECTORS_FIRST_BUTTON);previousButton.addClass("disabled"),previousButton.attr("aria-disabled",!0),firstButton.addClass("disabled"),firstButton.attr("aria-disabled",!0)},adjustPagingBarSize=function(root){var activePageNumber=getActivePageNumber(root),lastPageNumber=getLastPageNumber(root),dotsButtons=root.find(SELECTORS_DOTS_BUTTONS),beginningDotsButton=root.find(SELECTORS_BEGINNING_DOTS_BUTTON),endingDotsButton=root.find(SELECTORS_ENDING_DOTS_BUTTON),pages=root.find(SELECTORS_PAGE),barSize=parseInt(root.attr("data-bar-size"),10);if(barSize&&lastPageNumber>barSize){var minpage=Math.max(activePageNumber-Math.round(barSize/2),1),maxpage=minpage+barSize-1;maxpage>=lastPageNumber&&(minpage=(maxpage=lastPageNumber)-barSize+1),minpage>1?(show(beginningDotsButton),minpage++):hide(beginningDotsButton),maxpage=minpage&&index+1<=maxpage&&show(page)}))}else hide(dotsButtons)},enablePreviousControlButtons=function(root){var previousButton=root.find(SELECTORS_PREVIOUS_BUTTON),firstButton=root.find(SELECTORS_FIRST_BUTTON);previousButton.removeClass("disabled"),previousButton.removeAttr("aria-disabled"),firstButton.removeClass("disabled"),firstButton.removeAttr("aria-disabled")},showPage=function(root,pageNumber,id){var pendingPromise=new Pending("core/paged_content_paging_bar:showPage"),lastPageNumber=getLastPageNumber(root),isSamePage=pageNumber==getActivePageNumber(root),limit=function(root){return parseInt(root.attr("data-items-per-page"),10)}(root),offset=(pageNumber-1)*limit;if(!isSamePage){root.find(SELECTORS_PAGE_ITEM).removeClass("active").removeAttr("aria-current");var page=getPageByNumber(root,pageNumber);page.addClass("active"),page.attr("aria-current",!0),setActivePageNumber(root,pageNumber),adjustPagingBarSize(root)}lastPageNumber&&pageNumber>=lastPageNumber?disableNextControlButtons(root):enableNextControlButtons(root),pageNumber>1?enablePreviousControlButtons(root):disablePreviousControlButtons(root),function(root){var pageAriaLabelComponents=function(root){return root.attr("data-aria-label-components-pagination-item").split(",").map((function(component){return component.trim()}))}(root),activePageAriaLabelComponents=function(root){return root.attr("data-aria-label-components-pagination-active-item").split(",").map((function(component){return component.trim()}))}(root),activePageNumber=getActivePageNumber(root),pageItems=root.find(SELECTORS_PAGE_ITEM),stringRequests=pageItems.toArray().map((function(index,page){page=$(page);var pageNumber=getPageNumber(root,page);return pageNumber===activePageNumber?{key:activePageAriaLabelComponents[0],component:activePageAriaLabelComponents[1],param:pageNumber}:{key:pageAriaLabelComponents[0],component:pageAriaLabelComponents[1],param:pageNumber}}));Str.get_strings(stringRequests).then((function(strings){return pageItems.each((function(index,page){page=$(page);var string=strings[index];page.attr("aria-label",string),page.find(SELECTORS_PAGE_LINK).attr("aria-label",string)})),strings})).catch((function(){}))}(root),PubSub.publish(id+PagedContentEvents.SHOW_PAGES,[{pageNumber:pageNumber,limit:limit,offset:offset}]),pendingPromise.resolve()};return{init:function(root,id){var pages=(root=$(root)).find(SELECTORS_PAGE);if(function(root,items){var lastPageNumber=0;setActivePageNumber(root,0),items.each((function(index,item){var pageNumber=index+1;(item=$(item)).attr("data-page-number",pageNumber),lastPageNumber++,item.hasClass("active")&&setActivePageNumber(root,pageNumber)})),setLastPageNumber(root,lastPageNumber)}(root,pages),function(root,id){var ignoreControlWhileLoading=root.attr("data-ignore-control-while-loading"),loading=!1;""==ignoreControlWhileLoading&&(ignoreControlWhileLoading=!0),CustomEvents.define(root,[CustomEvents.events.activate]),root.on(CustomEvents.events.activate,SELECTORS_PAGE_ITEM,(function(e,data){if(data.originalEvent.preventDefault(),data.originalEvent.stopPropagation(),!ignoreControlWhileLoading||!loading){var page=$(e.target).closest(SELECTORS_PAGE_ITEM);if(!page.hasClass("disabled")){var pageNumber=getPageNumber(root,page);showPage(root,pageNumber,id),loading=!0}}})),PubSub.subscribe(id+PagedContentEvents.ALL_ITEMS_LOADED,(function(pageNumber){loading=!1;var currentLastPage=getLastPageNumber(root);(!currentLastPage||pageNumber1?activePageNumber-1:1}return parseInt(pageNumber,10)},show=function(root){root.removeClass("hidden")},hide=function(root){root.addClass("hidden")},disableNextControlButtons=function(root){var nextButton=root.find(SELECTORS_NEXT_BUTTON),lastButton=root.find(SELECTORS_LAST_BUTTON);nextButton.addClass("disabled"),nextButton.attr("aria-disabled",!0),lastButton.addClass("disabled"),lastButton.attr("aria-disabled",!0)},enableNextControlButtons=function(root){var nextButton=root.find(SELECTORS_NEXT_BUTTON),lastButton=root.find(SELECTORS_LAST_BUTTON);nextButton.removeClass("disabled"),nextButton.removeAttr("aria-disabled"),lastButton.removeClass("disabled"),lastButton.removeAttr("aria-disabled")},disablePreviousControlButtons=function(root){var previousButton=root.find(SELECTORS_PREVIOUS_BUTTON),firstButton=root.find(SELECTORS_FIRST_BUTTON);previousButton.addClass("disabled"),previousButton.attr("aria-disabled",!0),firstButton.addClass("disabled"),firstButton.attr("aria-disabled",!0)},adjustPagingBarSize=function(root){var activePageNumber=getActivePageNumber(root),lastPageNumber=getLastPageNumber(root),dotsButtons=root.find(SELECTORS_DOTS_BUTTONS),beginningDotsButton=root.find(SELECTORS_BEGINNING_DOTS_BUTTON),endingDotsButton=root.find(SELECTORS_ENDING_DOTS_BUTTON),pages=root.find(SELECTORS_PAGE),barSize=parseInt(root.attr("data-bar-size"),10);if(barSize&&lastPageNumber>barSize){var minpage=Math.max(activePageNumber-Math.round(barSize/2),1),maxpage=minpage+barSize-1;maxpage>=lastPageNumber&&(minpage=(maxpage=lastPageNumber)-barSize+1),minpage>1?(show(beginningDotsButton),minpage++):hide(beginningDotsButton),maxpage=minpage&&index+1<=maxpage&&show(page)}))}else hide(dotsButtons)},enablePreviousControlButtons=function(root){var previousButton=root.find(SELECTORS_PREVIOUS_BUTTON),firstButton=root.find(SELECTORS_FIRST_BUTTON);previousButton.removeClass("disabled"),previousButton.removeAttr("aria-disabled"),firstButton.removeClass("disabled"),firstButton.removeAttr("aria-disabled")},showPage=function(root,pageNumber,id){var pendingPromise=new Pending("core/paged_content_paging_bar:showPage"),lastPageNumber=getLastPageNumber(root),isSamePage=pageNumber==getActivePageNumber(root),limit=function(root){return parseInt(root.attr("data-items-per-page"),10)}(root),offset=(pageNumber-1)*limit;if(!isSamePage){root.find(SELECTORS_PAGE_ITEM).removeClass("active").removeAttr("aria-current");var page=getPageByNumber(root,pageNumber);page.addClass("active"),page.attr("aria-current",!0),setActivePageNumber(root,pageNumber),adjustPagingBarSize(root)}lastPageNumber&&pageNumber>=lastPageNumber?disableNextControlButtons(root):enableNextControlButtons(root),pageNumber>1?enablePreviousControlButtons(root):disablePreviousControlButtons(root),function(root){var pageAriaLabelComponents=function(root){return root.attr("data-aria-label-components-pagination-item").split(",").map((function(component){return component.trim()}))}(root),activePageAriaLabelComponents=function(root){return root.attr("data-aria-label-components-pagination-active-item").split(",").map((function(component){return component.trim()}))}(root),activePageNumber=getActivePageNumber(root),pageItems=root.find(SELECTORS_PAGE_ITEM),stringRequests=pageItems.toArray().map((function(page){if(null==(page=$(page)).attr("data-page"))return{};var pageNumber=getPageNumber(root,page);return pageNumber===activePageNumber?{key:activePageAriaLabelComponents[0],component:activePageAriaLabelComponents[1],param:pageNumber}:{key:pageAriaLabelComponents[0],component:pageAriaLabelComponents[1],param:pageNumber}})).filter(Boolean);Str.get_strings(stringRequests).then((function(strings){return pageItems.each((function(index,page){page=$(page);var string=strings[index];page.attr("aria-label",string),page.find(SELECTORS_PAGE_LINK).attr("aria-label",string)})),strings})).catch((function(){}))}(root),PubSub.publish(id+PagedContentEvents.SHOW_PAGES,[{pageNumber:pageNumber,limit:limit,offset:offset}]),pendingPromise.resolve()};return{init:function(root,id){var pages=(root=$(root)).find(SELECTORS_PAGE);if(function(root,items){var lastPageNumber=0;setActivePageNumber(root,0),items.each((function(index,item){var pageNumber=index+1;(item=$(item)).attr("data-page-number",pageNumber),lastPageNumber++,item.hasClass("active")&&setActivePageNumber(root,pageNumber)})),setLastPageNumber(root,lastPageNumber)}(root,pages),function(root,id){var ignoreControlWhileLoading=root.attr("data-ignore-control-while-loading"),loading=!1;""==ignoreControlWhileLoading&&(ignoreControlWhileLoading=!0),CustomEvents.define(root,[CustomEvents.events.activate]),root.on(CustomEvents.events.activate,SELECTORS_PAGE_ITEM,(function(e,data){if(data.originalEvent.preventDefault(),data.originalEvent.stopPropagation(),!ignoreControlWhileLoading||!loading){var page=$(e.target).closest(SELECTORS_PAGE_ITEM);if(!page.hasClass("disabled")){var pageNumber=getPageNumber(root,page);showPage(root,pageNumber,id),loading=!0}}})),PubSub.subscribe(id+PagedContentEvents.ALL_ITEMS_LOADED,(function(pageNumber){loading=!1;var currentLastPage=getLastPageNumber(root);(!currentLastPage||pageNumber.\n\n/**\n * Javascript to enhance the paged content paging bar.\n *\n * @module core/paged_content_paging_bar\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/custom_interaction_events',\n 'core/paged_content_events',\n 'core/str',\n 'core/pubsub',\n 'core/pending',\n],\nfunction(\n $,\n CustomEvents,\n PagedContentEvents,\n Str,\n PubSub,\n Pending\n) {\n\n var SELECTORS = {\n ROOT: '[data-region=\"paging-bar\"]',\n PAGE: '[data-page]',\n PAGE_ITEM: '[data-region=\"page-item\"]',\n PAGE_LINK: '[data-region=\"page-link\"]',\n FIRST_BUTTON: '[data-control=\"first\"]',\n LAST_BUTTON: '[data-control=\"last\"]',\n NEXT_BUTTON: '[data-control=\"next\"]',\n PREVIOUS_BUTTON: '[data-control=\"previous\"]',\n DOTS_BUTTONS: '[data-dots]',\n BEGINNING_DOTS_BUTTON: '[data-dots=\"beginning\"]',\n ENDING_DOTS_BUTTON: '[data-dots=\"ending\"]',\n };\n\n /**\n * Get the page element by number.\n *\n * @param {object} root The root element.\n * @param {Number} pageNumber The page number.\n * @return {jQuery}\n */\n var getPageByNumber = function(root, pageNumber) {\n return root.find(SELECTORS.PAGE_ITEM + '[data-page-number=\"' + pageNumber + '\"]');\n };\n\n /**\n * Get the next button element.\n *\n * @param {object} root The root element.\n * @return {jQuery}\n */\n var getNextButton = function(root) {\n return root.find(SELECTORS.NEXT_BUTTON);\n };\n\n /**\n * Set the last page number after which no more pages\n * should be loaded.\n *\n * @param {object} root The root element.\n * @param {Number} number Page number.\n */\n var setLastPageNumber = function(root, number) {\n root.attr('data-last-page-number', number);\n };\n\n /**\n * Get the last page number.\n *\n * @param {object} root The root element.\n * @return {Number}\n */\n var getLastPageNumber = function(root) {\n return parseInt(root.attr('data-last-page-number'), 10);\n };\n\n /**\n * Get the active page number.\n *\n * @param {object} root The root element.\n * @returns {Number} The page number\n */\n var getActivePageNumber = function(root) {\n return parseInt(root.attr('data-active-page-number'), 10);\n };\n\n /**\n * Set the active page number.\n *\n * @param {object} root The root element.\n * @param {Number} number Page number.\n */\n var setActivePageNumber = function(root, number) {\n root.attr('data-active-page-number', number);\n };\n\n /**\n * Check if there is an active page number.\n *\n * @param {object} root The root element.\n * @returns {bool}\n */\n var hasActivePageNumber = function(root) {\n var number = getActivePageNumber(root);\n return !isNaN(number) && number != 0;\n };\n\n /**\n * Get the page number for a given page.\n *\n * @param {object} root The root element.\n * @param {object} page The page element.\n * @returns {Number} The page number\n */\n var getPageNumber = function(root, page) {\n if (page.attr('data-page') != undefined) {\n // If it's an actual page then we can just use the page number\n // attribute.\n return parseInt(page.attr('data-page-number'), 10);\n }\n\n var pageNumber = 1;\n var activePageNumber = null;\n\n switch (page.attr('data-control')) {\n case 'first':\n pageNumber = 1;\n break;\n\n case 'last':\n pageNumber = getLastPageNumber(root);\n break;\n\n case 'next':\n activePageNumber = getActivePageNumber(root);\n var lastPage = getLastPageNumber(root);\n if (!lastPage) {\n pageNumber = activePageNumber + 1;\n } else if (activePageNumber && activePageNumber < lastPage) {\n pageNumber = activePageNumber + 1;\n } else {\n pageNumber = lastPage;\n }\n break;\n\n case 'previous':\n activePageNumber = getActivePageNumber(root);\n if (activePageNumber && activePageNumber > 1) {\n pageNumber = activePageNumber - 1;\n } else {\n pageNumber = 1;\n }\n break;\n\n default:\n pageNumber = 1;\n break;\n }\n\n // Make sure we return an int not a string.\n return parseInt(pageNumber, 10);\n };\n\n /**\n * Get the limit of items for each page.\n *\n * @param {object} root The root element.\n * @returns {Number}\n */\n var getLimit = function(root) {\n return parseInt(root.attr('data-items-per-page'), 10);\n };\n\n /**\n * Set the limit of items for each page.\n *\n * @param {object} root The root element.\n * @param {Number} limit Items per page limit.\n */\n var setLimit = function(root, limit) {\n root.attr('data-items-per-page', limit);\n };\n\n /**\n * Show the paging bar.\n *\n * @param {object} root The root element.\n */\n var show = function(root) {\n root.removeClass('hidden');\n };\n\n /**\n * Hide the paging bar.\n *\n * @param {object} root The root element.\n */\n var hide = function(root) {\n root.addClass('hidden');\n };\n\n /**\n * Disable the next and last buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var disableNextControlButtons = function(root) {\n var nextButton = root.find(SELECTORS.NEXT_BUTTON);\n var lastButton = root.find(SELECTORS.LAST_BUTTON);\n\n nextButton.addClass('disabled');\n nextButton.attr('aria-disabled', true);\n lastButton.addClass('disabled');\n lastButton.attr('aria-disabled', true);\n };\n\n /**\n * Enable the next and last buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var enableNextControlButtons = function(root) {\n var nextButton = root.find(SELECTORS.NEXT_BUTTON);\n var lastButton = root.find(SELECTORS.LAST_BUTTON);\n\n nextButton.removeClass('disabled');\n nextButton.removeAttr('aria-disabled');\n lastButton.removeClass('disabled');\n lastButton.removeAttr('aria-disabled');\n };\n\n /**\n * Disable the previous and first buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var disablePreviousControlButtons = function(root) {\n var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);\n var firstButton = root.find(SELECTORS.FIRST_BUTTON);\n\n previousButton.addClass('disabled');\n previousButton.attr('aria-disabled', true);\n firstButton.addClass('disabled');\n firstButton.attr('aria-disabled', true);\n };\n\n /**\n * Adjusts the size of the paging bar and hides unnecessary pages.\n *\n * @param {object} root The root element.\n */\n var adjustPagingBarSize = function(root) {\n var activePageNumber = getActivePageNumber(root);\n var lastPageNumber = getLastPageNumber(root);\n\n var dotsButtons = root.find(SELECTORS.DOTS_BUTTONS);\n var beginningDotsButton = root.find(SELECTORS.BEGINNING_DOTS_BUTTON);\n var endingDotsButton = root.find(SELECTORS.ENDING_DOTS_BUTTON);\n\n var pages = root.find(SELECTORS.PAGE);\n var barSize = parseInt(root.attr('data-bar-size'), 10);\n\n if (barSize && lastPageNumber > barSize) {\n\n var minpage = Math.max(activePageNumber - Math.round(barSize / 2), 1);\n var maxpage = minpage + barSize - 1;\n\n if (maxpage >= lastPageNumber) {\n maxpage = lastPageNumber;\n minpage = maxpage - barSize + 1;\n }\n\n if (minpage > 1) {\n show(beginningDotsButton);\n minpage++;\n } else {\n hide(beginningDotsButton);\n }\n if (maxpage < lastPageNumber) {\n show(endingDotsButton);\n maxpage--;\n } else {\n hide(endingDotsButton);\n }\n dotsButtons.addClass('disabled');\n dotsButtons.attr('aria-disabled', true);\n\n hide(pages);\n\n pages.each(function(index, page) {\n page = $(page);\n if ((index + 1) >= minpage && (index + 1) <= maxpage) {\n show(page);\n }\n });\n\n } else {\n hide(dotsButtons);\n }\n };\n\n /**\n * Enable the previous and first buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var enablePreviousControlButtons = function(root) {\n var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);\n var firstButton = root.find(SELECTORS.FIRST_BUTTON);\n\n previousButton.removeClass('disabled');\n previousButton.removeAttr('aria-disabled');\n firstButton.removeClass('disabled');\n firstButton.removeAttr('aria-disabled');\n };\n\n /**\n * Get the components for a get_string request for the aria-label\n * on a page. The value is a comma separated string of key and\n * component.\n *\n * @param {object} root The root element.\n * @return {array} First element is the key, second is the component.\n */\n var getPageAriaLabelComponents = function(root) {\n var componentString = root.attr('data-aria-label-components-pagination-item');\n var components = componentString.split(',').map(function(component) {\n return component.trim();\n });\n return components;\n };\n\n /**\n * Get the components for a get_string request for the aria-label\n * on an active page. The value is a comma separated string of key and\n * component.\n *\n * @param {object} root The root element.\n * @return {array} First element is the key, second is the component.\n */\n var getActivePageAriaLabelComponents = function(root) {\n var componentString = root.attr('data-aria-label-components-pagination-active-item');\n var components = componentString.split(',').map(function(component) {\n return component.trim();\n });\n return components;\n };\n\n /**\n * Set page numbers on each of the given items. Page numbers are set\n * from 1..n (where n is the number of items).\n *\n * Sets the active page number to be the last page found with\n * an \"active\" class (if any).\n *\n * Sets the last page number.\n *\n * @param {object} root The root element.\n * @param {jQuery} items A jQuery list of items.\n */\n var generatePageNumbers = function(root, items) {\n var lastPageNumber = 0;\n setActivePageNumber(root, 0);\n\n items.each(function(index, item) {\n var pageNumber = index + 1;\n item = $(item);\n item.attr('data-page-number', pageNumber);\n lastPageNumber++;\n\n if (item.hasClass('active')) {\n setActivePageNumber(root, pageNumber);\n }\n });\n\n setLastPageNumber(root, lastPageNumber);\n };\n\n /**\n * Set the aria-labels on each of the page items in the paging bar.\n * This includes the next, previous, first, and last items.\n *\n * @param {object} root The root element.\n */\n var generateAriaLabels = function(root) {\n var pageAriaLabelComponents = getPageAriaLabelComponents(root);\n var activePageAriaLabelComponents = getActivePageAriaLabelComponents(root);\n var activePageNumber = getActivePageNumber(root);\n var pageItems = root.find(SELECTORS.PAGE_ITEM);\n // We want to request all of the strings at once rather than\n // one at a time.\n var stringRequests = pageItems.toArray().map(function(index, page) {\n page = $(page);\n var pageNumber = getPageNumber(root, page);\n\n if (pageNumber === activePageNumber) {\n return {\n key: activePageAriaLabelComponents[0],\n component: activePageAriaLabelComponents[1],\n param: pageNumber\n };\n } else {\n return {\n key: pageAriaLabelComponents[0],\n component: pageAriaLabelComponents[1],\n param: pageNumber\n };\n }\n });\n\n Str.get_strings(stringRequests).then(function(strings) {\n pageItems.each(function(index, page) {\n page = $(page);\n var string = strings[index];\n page.attr('aria-label', string);\n page.find(SELECTORS.PAGE_LINK).attr('aria-label', string);\n });\n\n return strings;\n })\n .catch(function() {\n // No need to interrupt the page if we can't load the aria lang strings.\n return;\n });\n };\n\n /**\n * Make the paging bar item for the given page number visible and fire\n * the SHOW_PAGES paged content event to tell any listening content to\n * update.\n *\n * @param {object} root The root element.\n * @param {Number} pageNumber The number for the page to show.\n * @param {string} id A uniqie id for this instance.\n */\n var showPage = function(root, pageNumber, id) {\n var pendingPromise = new Pending('core/paged_content_paging_bar:showPage');\n var lastPageNumber = getLastPageNumber(root);\n var isSamePage = pageNumber == getActivePageNumber(root);\n var limit = getLimit(root);\n var offset = (pageNumber - 1) * limit;\n\n if (!isSamePage) {\n // We only need to toggle the active class if the user didn't click\n // on the already active page.\n root.find(SELECTORS.PAGE_ITEM).removeClass('active').removeAttr('aria-current');\n var page = getPageByNumber(root, pageNumber);\n page.addClass('active');\n page.attr('aria-current', true);\n setActivePageNumber(root, pageNumber);\n\n adjustPagingBarSize(root);\n }\n\n // Make sure the control buttons are disabled as the user navigates\n // to either end of the limits.\n if (lastPageNumber && pageNumber >= lastPageNumber) {\n disableNextControlButtons(root);\n } else {\n enableNextControlButtons(root);\n }\n\n if (pageNumber > 1) {\n enablePreviousControlButtons(root);\n } else {\n disablePreviousControlButtons(root);\n }\n\n generateAriaLabels(root);\n\n // This event requires a payload that contains a list of all pages that\n // were activated. In the case of the paging bar we only show one page at\n // a time.\n PubSub.publish(id + PagedContentEvents.SHOW_PAGES, [{\n pageNumber: pageNumber,\n limit: limit,\n offset: offset\n }]);\n\n pendingPromise.resolve();\n };\n\n /**\n * Add event listeners for interactions with the paging bar as well as listening\n * for custom paged content events.\n *\n * Each event will trigger different logic to update parts of the paging bar's\n * display.\n *\n * @param {object} root The root element.\n * @param {string} id A uniqie id for this instance.\n */\n var registerEventListeners = function(root, id) {\n var ignoreControlWhileLoading = root.attr('data-ignore-control-while-loading');\n var loading = false;\n\n if (ignoreControlWhileLoading == \"\") {\n // Default to ignoring control while loading if not specified.\n ignoreControlWhileLoading = true;\n }\n\n CustomEvents.define(root, [\n CustomEvents.events.activate\n ]);\n\n root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) {\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n\n if (ignoreControlWhileLoading && loading) {\n // Do nothing if configured to ignore control while loading.\n return;\n }\n\n var page = $(e.target).closest(SELECTORS.PAGE_ITEM);\n\n if (!page.hasClass('disabled')) {\n var pageNumber = getPageNumber(root, page);\n showPage(root, pageNumber, id);\n loading = true;\n }\n });\n\n // This event is fired when all of the items have been loaded. Typically used\n // in an \"infinite\" pages context when we don't know the exact number of pages\n // ahead of time.\n PubSub.subscribe(id + PagedContentEvents.ALL_ITEMS_LOADED, function(pageNumber) {\n loading = false;\n var currentLastPage = getLastPageNumber(root);\n\n if (!currentLastPage || pageNumber < currentLastPage) {\n // Somehow the value we've got saved is higher than the new\n // value we just received. Perhaps events came out of order.\n // In any case, save the lowest value.\n setLastPageNumber(root, pageNumber);\n }\n\n if (pageNumber === 1 && root.attr('data-hide-control-on-single-page')) {\n // If all items were loaded on the first page then we can hide\n // the paging bar because there are no other pages to load.\n hide(root);\n disableNextControlButtons(root);\n disablePreviousControlButtons(root);\n } else {\n show(root);\n disableNextControlButtons(root);\n }\n });\n\n // This event is fired after all of the requested pages have been rendered.\n PubSub.subscribe(id + PagedContentEvents.PAGES_SHOWN, function() {\n // All pages have been shown so turn off the loading flag.\n loading = false;\n });\n\n // This is triggered when the paging limit is modified.\n PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function(limit) {\n // Update the limit.\n setLimit(root, limit);\n setLastPageNumber(root, 0);\n setActivePageNumber(root, 0);\n show(root);\n // Reload the data from page 1 again.\n showPage(root, 1, id);\n });\n };\n\n /**\n * Initialise the paging bar.\n * @param {object} root The root element.\n * @param {string} id A uniqie id for this instance.\n */\n var init = function(root, id) {\n root = $(root);\n var pages = root.find(SELECTORS.PAGE);\n generatePageNumbers(root, pages);\n registerEventListeners(root, id);\n\n if (hasActivePageNumber(root)) {\n var activePageNumber = getActivePageNumber(root);\n // If the the paging bar was rendered with an active page selected\n // then make sure we fired off the event to tell the content page to\n // show.\n getPageByNumber(root, activePageNumber).click();\n if (activePageNumber == 1) {\n // If the first page is active then disable the previous buttons.\n disablePreviousControlButtons(root);\n }\n } else {\n // There was no active page number so load the first page using\n // the next button. This allows the infinite pagination to work.\n getNextButton(root).click();\n }\n\n adjustPagingBarSize(root);\n };\n\n return {\n init: init,\n disableNextControlButtons: disableNextControlButtons,\n enableNextControlButtons: enableNextControlButtons,\n disablePreviousControlButtons: disablePreviousControlButtons,\n enablePreviousControlButtons: enablePreviousControlButtons,\n showPage: showPage,\n rootSelector: SELECTORS.ROOT,\n };\n});\n"],"names":["define","$","CustomEvents","PagedContentEvents","Str","PubSub","Pending","SELECTORS","getPageByNumber","root","pageNumber","find","setLastPageNumber","number","attr","getLastPageNumber","parseInt","getActivePageNumber","setActivePageNumber","getPageNumber","page","undefined","activePageNumber","lastPage","show","removeClass","hide","addClass","disableNextControlButtons","nextButton","lastButton","enableNextControlButtons","removeAttr","disablePreviousControlButtons","previousButton","firstButton","adjustPagingBarSize","lastPageNumber","dotsButtons","beginningDotsButton","endingDotsButton","pages","barSize","minpage","Math","max","round","maxpage","each","index","enablePreviousControlButtons","showPage","id","pendingPromise","isSamePage","limit","getLimit","offset","pageAriaLabelComponents","split","map","component","trim","getPageAriaLabelComponents","activePageAriaLabelComponents","getActivePageAriaLabelComponents","pageItems","stringRequests","toArray","key","param","get_strings","then","strings","string","catch","generateAriaLabels","publish","SHOW_PAGES","resolve","init","items","item","hasClass","generatePageNumbers","ignoreControlWhileLoading","loading","events","activate","on","e","data","originalEvent","preventDefault","stopPropagation","target","closest","subscribe","ALL_ITEMS_LOADED","currentLastPage","PAGES_SHOWN","SET_ITEMS_PER_PAGE_LIMIT","setLimit","registerEventListeners","isNaN","hasActivePageNumber","click","getNextButton","rootSelector"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"paged_content_paging_bar.min.js","sources":["../src/paged_content_paging_bar.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 * Javascript to enhance the paged content paging bar.\n *\n * @module core/paged_content_paging_bar\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/custom_interaction_events',\n 'core/paged_content_events',\n 'core/str',\n 'core/pubsub',\n 'core/pending',\n],\nfunction(\n $,\n CustomEvents,\n PagedContentEvents,\n Str,\n PubSub,\n Pending\n) {\n\n var SELECTORS = {\n ROOT: '[data-region=\"paging-bar\"]',\n PAGE: '[data-page]',\n PAGE_ITEM: '[data-region=\"page-item\"]',\n PAGE_LINK: '[data-region=\"page-link\"]',\n FIRST_BUTTON: '[data-control=\"first\"]',\n LAST_BUTTON: '[data-control=\"last\"]',\n NEXT_BUTTON: '[data-control=\"next\"]',\n PREVIOUS_BUTTON: '[data-control=\"previous\"]',\n DOTS_BUTTONS: '[data-dots]',\n BEGINNING_DOTS_BUTTON: '[data-dots=\"beginning\"]',\n ENDING_DOTS_BUTTON: '[data-dots=\"ending\"]',\n };\n\n /**\n * Get the page element by number.\n *\n * @param {object} root The root element.\n * @param {Number} pageNumber The page number.\n * @return {jQuery}\n */\n var getPageByNumber = function(root, pageNumber) {\n return root.find(SELECTORS.PAGE_ITEM + '[data-page-number=\"' + pageNumber + '\"]');\n };\n\n /**\n * Get the next button element.\n *\n * @param {object} root The root element.\n * @return {jQuery}\n */\n var getNextButton = function(root) {\n return root.find(SELECTORS.NEXT_BUTTON);\n };\n\n /**\n * Set the last page number after which no more pages\n * should be loaded.\n *\n * @param {object} root The root element.\n * @param {Number} number Page number.\n */\n var setLastPageNumber = function(root, number) {\n root.attr('data-last-page-number', number);\n };\n\n /**\n * Get the last page number.\n *\n * @param {object} root The root element.\n * @return {Number}\n */\n var getLastPageNumber = function(root) {\n return parseInt(root.attr('data-last-page-number'), 10);\n };\n\n /**\n * Get the active page number.\n *\n * @param {object} root The root element.\n * @returns {Number} The page number\n */\n var getActivePageNumber = function(root) {\n return parseInt(root.attr('data-active-page-number'), 10);\n };\n\n /**\n * Set the active page number.\n *\n * @param {object} root The root element.\n * @param {Number} number Page number.\n */\n var setActivePageNumber = function(root, number) {\n root.attr('data-active-page-number', number);\n };\n\n /**\n * Check if there is an active page number.\n *\n * @param {object} root The root element.\n * @returns {bool}\n */\n var hasActivePageNumber = function(root) {\n var number = getActivePageNumber(root);\n return !isNaN(number) && number != 0;\n };\n\n /**\n * Get the page number for a given page.\n *\n * @param {object} root The root element.\n * @param {object} page The page element.\n * @returns {Number} The page number\n */\n var getPageNumber = function(root, page) {\n if (page.attr('data-page') != undefined) {\n // If it's an actual page then we can just use the page number\n // attribute.\n return parseInt(page.attr('data-page-number'), 10);\n }\n\n var pageNumber = 1;\n var activePageNumber = null;\n\n switch (page.attr('data-control')) {\n case 'first':\n pageNumber = 1;\n break;\n\n case 'last':\n pageNumber = getLastPageNumber(root);\n break;\n\n case 'next':\n activePageNumber = getActivePageNumber(root);\n var lastPage = getLastPageNumber(root);\n if (!lastPage) {\n pageNumber = activePageNumber + 1;\n } else if (activePageNumber && activePageNumber < lastPage) {\n pageNumber = activePageNumber + 1;\n } else {\n pageNumber = lastPage;\n }\n break;\n\n case 'previous':\n activePageNumber = getActivePageNumber(root);\n if (activePageNumber && activePageNumber > 1) {\n pageNumber = activePageNumber - 1;\n } else {\n pageNumber = 1;\n }\n break;\n\n default:\n pageNumber = 1;\n break;\n }\n\n // Make sure we return an int not a string.\n return parseInt(pageNumber, 10);\n };\n\n /**\n * Get the limit of items for each page.\n *\n * @param {object} root The root element.\n * @returns {Number}\n */\n var getLimit = function(root) {\n return parseInt(root.attr('data-items-per-page'), 10);\n };\n\n /**\n * Set the limit of items for each page.\n *\n * @param {object} root The root element.\n * @param {Number} limit Items per page limit.\n */\n var setLimit = function(root, limit) {\n root.attr('data-items-per-page', limit);\n };\n\n /**\n * Show the paging bar.\n *\n * @param {object} root The root element.\n */\n var show = function(root) {\n root.removeClass('hidden');\n };\n\n /**\n * Hide the paging bar.\n *\n * @param {object} root The root element.\n */\n var hide = function(root) {\n root.addClass('hidden');\n };\n\n /**\n * Disable the next and last buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var disableNextControlButtons = function(root) {\n var nextButton = root.find(SELECTORS.NEXT_BUTTON);\n var lastButton = root.find(SELECTORS.LAST_BUTTON);\n\n nextButton.addClass('disabled');\n nextButton.attr('aria-disabled', true);\n lastButton.addClass('disabled');\n lastButton.attr('aria-disabled', true);\n };\n\n /**\n * Enable the next and last buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var enableNextControlButtons = function(root) {\n var nextButton = root.find(SELECTORS.NEXT_BUTTON);\n var lastButton = root.find(SELECTORS.LAST_BUTTON);\n\n nextButton.removeClass('disabled');\n nextButton.removeAttr('aria-disabled');\n lastButton.removeClass('disabled');\n lastButton.removeAttr('aria-disabled');\n };\n\n /**\n * Disable the previous and first buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var disablePreviousControlButtons = function(root) {\n var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);\n var firstButton = root.find(SELECTORS.FIRST_BUTTON);\n\n previousButton.addClass('disabled');\n previousButton.attr('aria-disabled', true);\n firstButton.addClass('disabled');\n firstButton.attr('aria-disabled', true);\n };\n\n /**\n * Adjusts the size of the paging bar and hides unnecessary pages.\n *\n * @param {object} root The root element.\n */\n var adjustPagingBarSize = function(root) {\n var activePageNumber = getActivePageNumber(root);\n var lastPageNumber = getLastPageNumber(root);\n\n var dotsButtons = root.find(SELECTORS.DOTS_BUTTONS);\n var beginningDotsButton = root.find(SELECTORS.BEGINNING_DOTS_BUTTON);\n var endingDotsButton = root.find(SELECTORS.ENDING_DOTS_BUTTON);\n\n var pages = root.find(SELECTORS.PAGE);\n var barSize = parseInt(root.attr('data-bar-size'), 10);\n\n if (barSize && lastPageNumber > barSize) {\n\n var minpage = Math.max(activePageNumber - Math.round(barSize / 2), 1);\n var maxpage = minpage + barSize - 1;\n\n if (maxpage >= lastPageNumber) {\n maxpage = lastPageNumber;\n minpage = maxpage - barSize + 1;\n }\n\n if (minpage > 1) {\n show(beginningDotsButton);\n minpage++;\n } else {\n hide(beginningDotsButton);\n }\n if (maxpage < lastPageNumber) {\n show(endingDotsButton);\n maxpage--;\n } else {\n hide(endingDotsButton);\n }\n dotsButtons.addClass('disabled');\n dotsButtons.attr('aria-disabled', true);\n\n hide(pages);\n\n pages.each(function(index, page) {\n page = $(page);\n if ((index + 1) >= minpage && (index + 1) <= maxpage) {\n show(page);\n }\n });\n\n } else {\n hide(dotsButtons);\n }\n };\n\n /**\n * Enable the previous and first buttons in the paging bar.\n *\n * @param {object} root The root element.\n */\n var enablePreviousControlButtons = function(root) {\n var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);\n var firstButton = root.find(SELECTORS.FIRST_BUTTON);\n\n previousButton.removeClass('disabled');\n previousButton.removeAttr('aria-disabled');\n firstButton.removeClass('disabled');\n firstButton.removeAttr('aria-disabled');\n };\n\n /**\n * Get the components for a get_string request for the aria-label\n * on a page. The value is a comma separated string of key and\n * component.\n *\n * @param {object} root The root element.\n * @return {array} First element is the key, second is the component.\n */\n var getPageAriaLabelComponents = function(root) {\n var componentString = root.attr('data-aria-label-components-pagination-item');\n var components = componentString.split(',').map(function(component) {\n return component.trim();\n });\n return components;\n };\n\n /**\n * Get the components for a get_string request for the aria-label\n * on an active page. The value is a comma separated string of key and\n * component.\n *\n * @param {object} root The root element.\n * @return {array} First element is the key, second is the component.\n */\n var getActivePageAriaLabelComponents = function(root) {\n var componentString = root.attr('data-aria-label-components-pagination-active-item');\n var components = componentString.split(',').map(function(component) {\n return component.trim();\n });\n return components;\n };\n\n /**\n * Set page numbers on each of the given items. Page numbers are set\n * from 1..n (where n is the number of items).\n *\n * Sets the active page number to be the last page found with\n * an \"active\" class (if any).\n *\n * Sets the last page number.\n *\n * @param {object} root The root element.\n * @param {jQuery} items A jQuery list of items.\n */\n var generatePageNumbers = function(root, items) {\n var lastPageNumber = 0;\n setActivePageNumber(root, 0);\n\n items.each(function(index, item) {\n var pageNumber = index + 1;\n item = $(item);\n item.attr('data-page-number', pageNumber);\n lastPageNumber++;\n\n if (item.hasClass('active')) {\n setActivePageNumber(root, pageNumber);\n }\n });\n\n setLastPageNumber(root, lastPageNumber);\n };\n\n /**\n * Set the aria-labels on each of the page items in the paging bar.\n * This includes the next, previous, first, and last items.\n *\n * @param {object} root The root element.\n */\n var generateAriaLabels = function(root) {\n var pageAriaLabelComponents = getPageAriaLabelComponents(root);\n var activePageAriaLabelComponents = getActivePageAriaLabelComponents(root);\n var activePageNumber = getActivePageNumber(root);\n var pageItems = root.find(SELECTORS.PAGE_ITEM);\n // We want to request all of the strings at once rather than\n // one at a time.\n var stringRequests = pageItems.toArray().map(function(page) {\n page = $(page);\n if (page.attr('data-page') == undefined) {\n return {};\n }\n var pageNumber = getPageNumber(root, page);\n\n if (pageNumber === activePageNumber) {\n return {\n key: activePageAriaLabelComponents[0],\n component: activePageAriaLabelComponents[1],\n param: pageNumber\n };\n } else {\n return {\n key: pageAriaLabelComponents[0],\n component: pageAriaLabelComponents[1],\n param: pageNumber\n };\n }\n }).filter(Boolean);\n\n Str.get_strings(stringRequests).then(function(strings) {\n pageItems.each(function(index, page) {\n page = $(page);\n var string = strings[index];\n page.attr('aria-label', string);\n page.find(SELECTORS.PAGE_LINK).attr('aria-label', string);\n });\n\n return strings;\n })\n .catch(function() {\n // No need to interrupt the page if we can't load the aria lang strings.\n return;\n });\n };\n\n /**\n * Make the paging bar item for the given page number visible and fire\n * the SHOW_PAGES paged content event to tell any listening content to\n * update.\n *\n * @param {object} root The root element.\n * @param {Number} pageNumber The number for the page to show.\n * @param {string} id A uniqie id for this instance.\n */\n var showPage = function(root, pageNumber, id) {\n var pendingPromise = new Pending('core/paged_content_paging_bar:showPage');\n var lastPageNumber = getLastPageNumber(root);\n var isSamePage = pageNumber == getActivePageNumber(root);\n var limit = getLimit(root);\n var offset = (pageNumber - 1) * limit;\n\n if (!isSamePage) {\n // We only need to toggle the active class if the user didn't click\n // on the already active page.\n root.find(SELECTORS.PAGE_ITEM).removeClass('active').removeAttr('aria-current');\n var page = getPageByNumber(root, pageNumber);\n page.addClass('active');\n page.attr('aria-current', true);\n setActivePageNumber(root, pageNumber);\n\n adjustPagingBarSize(root);\n }\n\n // Make sure the control buttons are disabled as the user navigates\n // to either end of the limits.\n if (lastPageNumber && pageNumber >= lastPageNumber) {\n disableNextControlButtons(root);\n } else {\n enableNextControlButtons(root);\n }\n\n if (pageNumber > 1) {\n enablePreviousControlButtons(root);\n } else {\n disablePreviousControlButtons(root);\n }\n\n generateAriaLabels(root);\n\n // This event requires a payload that contains a list of all pages that\n // were activated. In the case of the paging bar we only show one page at\n // a time.\n PubSub.publish(id + PagedContentEvents.SHOW_PAGES, [{\n pageNumber: pageNumber,\n limit: limit,\n offset: offset\n }]);\n\n pendingPromise.resolve();\n };\n\n /**\n * Add event listeners for interactions with the paging bar as well as listening\n * for custom paged content events.\n *\n * Each event will trigger different logic to update parts of the paging bar's\n * display.\n *\n * @param {object} root The root element.\n * @param {string} id A uniqie id for this instance.\n */\n var registerEventListeners = function(root, id) {\n var ignoreControlWhileLoading = root.attr('data-ignore-control-while-loading');\n var loading = false;\n\n if (ignoreControlWhileLoading == \"\") {\n // Default to ignoring control while loading if not specified.\n ignoreControlWhileLoading = true;\n }\n\n CustomEvents.define(root, [\n CustomEvents.events.activate\n ]);\n\n root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) {\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n\n if (ignoreControlWhileLoading && loading) {\n // Do nothing if configured to ignore control while loading.\n return;\n }\n\n var page = $(e.target).closest(SELECTORS.PAGE_ITEM);\n\n if (!page.hasClass('disabled')) {\n var pageNumber = getPageNumber(root, page);\n showPage(root, pageNumber, id);\n loading = true;\n }\n });\n\n // This event is fired when all of the items have been loaded. Typically used\n // in an \"infinite\" pages context when we don't know the exact number of pages\n // ahead of time.\n PubSub.subscribe(id + PagedContentEvents.ALL_ITEMS_LOADED, function(pageNumber) {\n loading = false;\n var currentLastPage = getLastPageNumber(root);\n\n if (!currentLastPage || pageNumber < currentLastPage) {\n // Somehow the value we've got saved is higher than the new\n // value we just received. Perhaps events came out of order.\n // In any case, save the lowest value.\n setLastPageNumber(root, pageNumber);\n }\n\n if (pageNumber === 1 && root.attr('data-hide-control-on-single-page')) {\n // If all items were loaded on the first page then we can hide\n // the paging bar because there are no other pages to load.\n hide(root);\n disableNextControlButtons(root);\n disablePreviousControlButtons(root);\n } else {\n show(root);\n disableNextControlButtons(root);\n }\n });\n\n // This event is fired after all of the requested pages have been rendered.\n PubSub.subscribe(id + PagedContentEvents.PAGES_SHOWN, function() {\n // All pages have been shown so turn off the loading flag.\n loading = false;\n });\n\n // This is triggered when the paging limit is modified.\n PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function(limit) {\n // Update the limit.\n setLimit(root, limit);\n setLastPageNumber(root, 0);\n setActivePageNumber(root, 0);\n show(root);\n // Reload the data from page 1 again.\n showPage(root, 1, id);\n });\n };\n\n /**\n * Initialise the paging bar.\n * @param {object} root The root element.\n * @param {string} id A uniqie id for this instance.\n */\n var init = function(root, id) {\n root = $(root);\n var pages = root.find(SELECTORS.PAGE);\n generatePageNumbers(root, pages);\n registerEventListeners(root, id);\n\n if (hasActivePageNumber(root)) {\n var activePageNumber = getActivePageNumber(root);\n // If the the paging bar was rendered with an active page selected\n // then make sure we fired off the event to tell the content page to\n // show.\n getPageByNumber(root, activePageNumber).click();\n if (activePageNumber == 1) {\n // If the first page is active then disable the previous buttons.\n disablePreviousControlButtons(root);\n }\n } else {\n // There was no active page number so load the first page using\n // the next button. This allows the infinite pagination to work.\n getNextButton(root).click();\n }\n\n adjustPagingBarSize(root);\n };\n\n return {\n init: init,\n disableNextControlButtons: disableNextControlButtons,\n enableNextControlButtons: enableNextControlButtons,\n disablePreviousControlButtons: disablePreviousControlButtons,\n enablePreviousControlButtons: enablePreviousControlButtons,\n showPage: showPage,\n rootSelector: SELECTORS.ROOT,\n };\n});\n"],"names":["define","$","CustomEvents","PagedContentEvents","Str","PubSub","Pending","SELECTORS","getPageByNumber","root","pageNumber","find","setLastPageNumber","number","attr","getLastPageNumber","parseInt","getActivePageNumber","setActivePageNumber","getPageNumber","page","undefined","activePageNumber","lastPage","show","removeClass","hide","addClass","disableNextControlButtons","nextButton","lastButton","enableNextControlButtons","removeAttr","disablePreviousControlButtons","previousButton","firstButton","adjustPagingBarSize","lastPageNumber","dotsButtons","beginningDotsButton","endingDotsButton","pages","barSize","minpage","Math","max","round","maxpage","each","index","enablePreviousControlButtons","showPage","id","pendingPromise","isSamePage","limit","getLimit","offset","pageAriaLabelComponents","split","map","component","trim","getPageAriaLabelComponents","activePageAriaLabelComponents","getActivePageAriaLabelComponents","pageItems","stringRequests","toArray","key","param","filter","Boolean","get_strings","then","strings","string","catch","generateAriaLabels","publish","SHOW_PAGES","resolve","init","items","item","hasClass","generatePageNumbers","ignoreControlWhileLoading","loading","events","activate","on","e","data","originalEvent","preventDefault","stopPropagation","target","closest","subscribe","ALL_ITEMS_LOADED","currentLastPage","PAGES_SHOWN","SET_ITEMS_PER_PAGE_LIMIT","setLimit","registerEventListeners","isNaN","hasActivePageNumber","click","getNextButton","rootSelector"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/amd/build/tree.min.js b/lib/amd/build/tree.min.js
index dc73db1d7f640..96b96212b1107 100644
--- a/lib/amd/build/tree.min.js
+++ b/lib/amd/build/tree.min.js
@@ -6,6 +6,6 @@
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("core/tree",["jquery"],(function($){var SELECTORS_ITEM="[role=treeitem]",SELECTORS_GROUP="[role=treeitem]:has([role=group]), [role=treeitem][aria-owns], [role=treeitem][data-requires-ajax=true]",SELECTORS_CLOSED_GROUP="[role=treeitem]:has([role=group])[aria-expanded=false], [role=treeitem][aria-owns][aria-expanded=false], [role=treeitem][data-requires-ajax=true][aria-expanded=false]",SELECTORS_FIRST_ITEM="[role=treeitem]:first",SELECTORS_VISIBLE_ITEM="[role=treeitem]:visible",SELECTORS_UNLOADED_AJAX_ITEM="[role=treeitem][data-requires-ajax=true][data-loaded=false][aria-expanded=true]",Tree=function(selector,selectCallback){this.treeRoot=$(selector),this.treeRoot.data("activeItem",null),this.selectCallback=selectCallback,this.keys={tab:9,enter:13,space:32,pageup:33,pagedown:34,end:35,home:36,left:37,up:38,right:39,down:40,asterisk:106},this.initialiseNodes(this.treeRoot),this.setActiveItem(this.treeRoot.find(SELECTORS_FIRST_ITEM)),this.refreshVisibleItemsCache(),this.bindEventHandlers()};return Tree.prototype.registerEnterCallback=function(callback){this.enterCallback=callback},Tree.prototype.refreshVisibleItemsCache=function(){this.treeRoot.data("visibleItems",this.treeRoot.find(SELECTORS_VISIBLE_ITEM))},Tree.prototype.getVisibleItems=function(){return this.treeRoot.data("visibleItems")},Tree.prototype.setActiveItem=function(item){var currentActive=this.treeRoot.data("activeItem");item!==currentActive&&(currentActive&&(currentActive.attr("tabindex","-1"),currentActive.attr("aria-selected","false")),item.attr("tabindex","0"),item.attr("aria-selected","true"),this.treeRoot.data("activeItem",item),"function"==typeof this.selectCallback&&this.selectCallback(item))},Tree.prototype.isGroupItem=function(item){return item.is(SELECTORS_GROUP)},Tree.prototype.getGroupFromItem=function(item){var ariaowns=this.treeRoot.find("#"+item.attr("aria-owns")),plain=item.children("[role=group]");return ariaowns.length>plain.length?ariaowns:plain},Tree.prototype.isGroupCollapsed=function(item){return"false"===item.attr("aria-expanded")},Tree.prototype.isGroupCollapsible=function(item){return"false"!==item.attr("data-collapsible")},Tree.prototype.initialiseNodes=function(node){this.removeAllFromTabOrder(node),this.setAriaSelectedFalseOnItems(node);var thisTree=this;node.find(SELECTORS_UNLOADED_AJAX_ITEM).each((function(){var unloadedNode=$(this);thisTree.collapseGroup(unloadedNode),thisTree.expandGroup(unloadedNode)}))},Tree.prototype.removeAllFromTabOrder=function(node){node.find("*").attr("tabindex","-1"),this.getGroupFromItem($(node)).find("*").attr("tabindex","-1")},Tree.prototype.setAriaSelectedFalseOnItems=function(node){node.find(SELECTORS_ITEM).attr("aria-selected","false")},Tree.prototype.expandAllGroups=function(){var thisTree=this;this.treeRoot.find(SELECTORS_CLOSED_GROUP).each((function(){var groupNode=$(this);thisTree.expandGroup($(this)).done((function(){thisTree.expandAllChildGroups(groupNode)}))}))},Tree.prototype.expandAllChildGroups=function(item){var thisTree=this;this.getGroupFromItem(item).find(SELECTORS_CLOSED_GROUP).each((function(){var groupNode=$(this);thisTree.expandGroup($(this)).done((function(){thisTree.expandAllChildGroups(groupNode)}))}))},Tree.prototype.expandGroup=function(item){var promise=$.Deferred();if("false"!==item.attr("data-expandable")&&this.isGroupCollapsed(item))if("true"===item.attr("data-requires-ajax")&&"true"!==item.attr("data-loaded")){item.attr("data-loaded",!1);var moduleName=item.closest("[data-ajax-loader]").attr("data-ajax-loader"),thisTree=this;const p=item.find("p");p.addClass("loading"),require([moduleName],(function(loader){loader.load(item).done((function(){item.attr("data-loaded",!0),thisTree.initialiseNodes(item),thisTree.finishExpandingGroup(item),p.removeClass("loading"),promise.resolve()}))}))}else this.finishExpandingGroup(item),promise.resolve();else promise.resolve();return promise},Tree.prototype.finishExpandingGroup=function(item){this.getGroupFromItem(item).removeAttr("aria-hidden"),item.attr("aria-expanded","true"),this.refreshVisibleItemsCache()},Tree.prototype.collapseGroup=function(item){this.isGroupCollapsible(item)&&!this.isGroupCollapsed(item)&&(this.getGroupFromItem(item).attr("aria-hidden","true"),item.attr("aria-expanded","false"),this.refreshVisibleItemsCache())},Tree.prototype.toggleGroup=function(item){"true"===item.attr("aria-expanded")?this.collapseGroup(item):this.expandGroup(item)},Tree.prototype.handleKeyDown=function(e){var _this$getVisibleItems,item=$(e.target),currentIndex=null===(_this$getVisibleItems=this.getVisibleItems())||void 0===_this$getVisibleItems?void 0:_this$getVisibleItems.index(item);if(!(e.altKey||e.ctrlKey||e.metaKey||e.shiftKey&&e.keyCode!=this.keys.tab))switch(e.keyCode){case this.keys.home:return this.getVisibleItems().first().focus(),void e.preventDefault();case this.keys.end:return this.getVisibleItems().last().focus(),void e.preventDefault();case this.keys.enter:var links=item.children("a").length?item.children("a"):item.children().not(SELECTORS_GROUP).find("a");return links.length?links.first().data("overrides-tree-activation-key-handler")?links.first().triggerHandler(e):"function"==typeof this.enterCallback?this.enterCallback(item):window.location.href=links.first().attr("href"):this.isGroupItem(item)&&this.toggleGroup(item,!0),void e.preventDefault();case this.keys.space:if(this.isGroupItem(item))this.toggleGroup(item,!0);else if(item.children("a").length){var firstLink=item.children("a").first();firstLink.data("overrides-tree-activation-key-handler")&&firstLink.triggerHandler(e)}return void e.preventDefault();case this.keys.left:var focusParent=function(tree){tree.getVisibleItems().filter((function(){return tree.getGroupFromItem($(this)).has(item).length})).focus()};return this.isGroupItem(item)?this.isGroupCollapsed(item)?focusParent(this):this.collapseGroup(item):focusParent(this),void e.preventDefault();case this.keys.right:return this.isGroupItem(item)&&(this.isGroupCollapsed(item)?this.expandGroup(item):this.getGroupFromItem(item).find(SELECTORS_ITEM).first().focus()),void e.preventDefault();case this.keys.up:if(currentIndex>0)this.getVisibleItems().eq(currentIndex-1).focus();return void e.preventDefault();case this.keys.down:if(currentIndexplain.length?ariaowns:plain},Tree.prototype.isGroupCollapsed=function(item){return"false"===item.attr("aria-expanded")},Tree.prototype.isGroupCollapsible=function(item){return"false"!==item.attr("data-collapsible")},Tree.prototype.initialiseNodes=function(node){this.removeAllFromTabOrder(node),this.setAriaSelectedFalseOnItems(node);var thisTree=this;node.find(SELECTORS_UNLOADED_AJAX_ITEM).each((function(){var unloadedNode=$(this);thisTree.collapseGroup(unloadedNode),thisTree.expandGroup(unloadedNode)}))},Tree.prototype.removeAllFromTabOrder=function(node){node.find("*").attr("tabindex","-1"),this.getGroupFromItem($(node)).find("*").attr("tabindex","-1")},Tree.prototype.setAriaSelectedFalseOnItems=function(node){node.find(SELECTORS_ITEM).attr("aria-selected","false")},Tree.prototype.expandAllGroups=function(){var thisTree=this;this.treeRoot.find(SELECTORS_CLOSED_GROUP).each((function(){var groupNode=$(this);thisTree.expandGroup($(this)).done((function(){thisTree.expandAllChildGroups(groupNode)}))}))},Tree.prototype.expandAllChildGroups=function(item){var thisTree=this;this.getGroupFromItem(item).find(SELECTORS_CLOSED_GROUP).each((function(){var groupNode=$(this);thisTree.expandGroup($(this)).done((function(){thisTree.expandAllChildGroups(groupNode)}))}))},Tree.prototype.expandGroup=function(item){var promise=$.Deferred();if("false"!==item.attr("data-expandable")&&this.isGroupCollapsed(item))if("true"===item.attr("data-requires-ajax")&&"true"!==item.attr("data-loaded")){item.attr("data-loaded",!1);var moduleName=item.closest("[data-ajax-loader]").attr("data-ajax-loader"),thisTree=this;const p=item.find("p");p.addClass("loading"),require([moduleName],(function(loader){loader.load(item).done((function(){item.attr("data-loaded",!0),thisTree.initialiseNodes(item),thisTree.finishExpandingGroup(item),p.removeClass("loading"),promise.resolve()}))}))}else this.finishExpandingGroup(item),promise.resolve();else promise.resolve();return promise},Tree.prototype.finishExpandingGroup=function(item){this.getGroupFromItem(item).removeAttr("aria-hidden"),item.attr("aria-expanded","true"),this.refreshVisibleItemsCache()},Tree.prototype.collapseGroup=function(item){this.isGroupCollapsible(item)&&!this.isGroupCollapsed(item)&&(this.getGroupFromItem(item).attr("aria-hidden","true"),item.attr("aria-expanded","false"),this.refreshVisibleItemsCache())},Tree.prototype.toggleGroup=function(item){"true"===item.attr("aria-expanded")?this.collapseGroup(item):this.expandGroup(item)},Tree.prototype.handleKeyDown=function(e){var _this$getVisibleItems,item=$(e.target),currentIndex=null===(_this$getVisibleItems=this.getVisibleItems())||void 0===_this$getVisibleItems?void 0:_this$getVisibleItems.index(item);if(e.altKey||e.ctrlKey||e.metaKey||e.shiftKey&&e.keyCode!=this.keys.tab)return;const rtl=window.right_to_left(),collapseKey=rtl?this.keys.right:this.keys.left,expandKey=rtl?this.keys.left:this.keys.right;switch(e.keyCode){case this.keys.home:return this.getVisibleItems().first().focus(),void e.preventDefault();case this.keys.end:return this.getVisibleItems().last().focus(),void e.preventDefault();case this.keys.enter:var links=item.children("a").length?item.children("a"):item.children().not(SELECTORS_GROUP).find("a");return links.length?links.first().data("overrides-tree-activation-key-handler")?links.first().triggerHandler(e):"function"==typeof this.enterCallback?this.enterCallback(item):window.location.href=links.first().attr("href"):this.isGroupItem(item)&&this.toggleGroup(item,!0),void e.preventDefault();case this.keys.space:if(this.isGroupItem(item))this.toggleGroup(item,!0);else if(item.children("a").length){var firstLink=item.children("a").first();firstLink.data("overrides-tree-activation-key-handler")&&firstLink.triggerHandler(e)}return void e.preventDefault();case collapseKey:var focusParent=function(tree){tree.getVisibleItems().filter((function(){return tree.getGroupFromItem($(this)).has(item).length})).focus()};return this.isGroupItem(item)?this.isGroupCollapsed(item)?focusParent(this):this.collapseGroup(item):focusParent(this),void e.preventDefault();case expandKey:return this.isGroupItem(item)&&(this.isGroupCollapsed(item)?this.expandGroup(item):this.getGroupFromItem(item).find(SELECTORS_ITEM).first().focus()),void e.preventDefault();case this.keys.up:if(currentIndex>0)this.getVisibleItems().eq(currentIndex-1).focus();return void e.preventDefault();case this.keys.down:if(currentIndex.\n\n/**\n * Implement an accessible aria tree widget, from a nested unordered list.\n * Based on http://oaa-accessibility.org/example/41/.\n *\n * @module core/tree\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery'], function($) {\n // Private variables and functions.\n var SELECTORS = {\n ITEM: '[role=treeitem]',\n GROUP: '[role=treeitem]:has([role=group]), [role=treeitem][aria-owns], [role=treeitem][data-requires-ajax=true]',\n CLOSED_GROUP: '[role=treeitem]:has([role=group])[aria-expanded=false], [role=treeitem][aria-owns][aria-expanded=false], ' +\n '[role=treeitem][data-requires-ajax=true][aria-expanded=false]',\n FIRST_ITEM: '[role=treeitem]:first',\n VISIBLE_ITEM: '[role=treeitem]:visible',\n UNLOADED_AJAX_ITEM: '[role=treeitem][data-requires-ajax=true][data-loaded=false][aria-expanded=true]'\n };\n\n /**\n * Constructor.\n *\n * @param {String} selector\n * @param {function} selectCallback Called when the active node is changed.\n */\n var Tree = function(selector, selectCallback) {\n this.treeRoot = $(selector);\n\n this.treeRoot.data('activeItem', null);\n this.selectCallback = selectCallback;\n this.keys = {\n tab: 9,\n enter: 13,\n space: 32,\n pageup: 33,\n pagedown: 34,\n end: 35,\n home: 36,\n left: 37,\n up: 38,\n right: 39,\n down: 40,\n asterisk: 106\n };\n\n // Apply the standard default initialisation for all nodes, starting with the tree root.\n this.initialiseNodes(this.treeRoot);\n // Make the first item the active item for the tree so that it is added to the tab order.\n this.setActiveItem(this.treeRoot.find(SELECTORS.FIRST_ITEM));\n // Create the cache of the visible items.\n this.refreshVisibleItemsCache();\n // Create the event handlers for the tree.\n this.bindEventHandlers();\n };\n\n Tree.prototype.registerEnterCallback = function(callback) {\n this.enterCallback = callback;\n };\n\n /**\n * Find all visible tree items and save a cache of them on the tree object.\n *\n * @method refreshVisibleItemsCache\n */\n Tree.prototype.refreshVisibleItemsCache = function() {\n this.treeRoot.data('visibleItems', this.treeRoot.find(SELECTORS.VISIBLE_ITEM));\n };\n\n /**\n * Get all visible tree items.\n *\n * @method getVisibleItems\n * @return {Object} visible items\n */\n Tree.prototype.getVisibleItems = function() {\n return this.treeRoot.data('visibleItems');\n };\n\n /**\n * Mark the given item as active within the tree and fire the callback for when the active item is set.\n *\n * @method setActiveItem\n * @param {object} item jquery object representing an item on the tree.\n */\n Tree.prototype.setActiveItem = function(item) {\n var currentActive = this.treeRoot.data('activeItem');\n if (item === currentActive) {\n return;\n }\n\n // Remove previous active from tab order.\n if (currentActive) {\n currentActive.attr('tabindex', '-1');\n currentActive.attr('aria-selected', 'false');\n }\n item.attr('tabindex', '0');\n item.attr('aria-selected', 'true');\n\n // Set the new active item.\n this.treeRoot.data('activeItem', item);\n\n if (typeof this.selectCallback === 'function') {\n this.selectCallback(item);\n }\n };\n\n /**\n * Determines if the given item is a group item (contains child tree items) in the tree.\n *\n * @method isGroupItem\n * @param {object} item jquery object representing an item on the tree.\n * @returns {bool}\n */\n Tree.prototype.isGroupItem = function(item) {\n return item.is(SELECTORS.GROUP);\n };\n\n /**\n * Determines if the given item is a group item (contains child tree items) in the tree.\n *\n * @method isGroupItem\n * @param {object} item jquery object representing an item on the tree.\n * @returns {bool}\n */\n Tree.prototype.getGroupFromItem = function(item) {\n var ariaowns = this.treeRoot.find('#' + item.attr('aria-owns'));\n var plain = item.children('[role=group]');\n if (ariaowns.length > plain.length) {\n return ariaowns;\n } else {\n return plain;\n }\n };\n\n /**\n * Determines if the given group item (contains child tree items) is collapsed.\n *\n * @method isGroupCollapsed\n * @param {object} item jquery object representing a group item on the tree.\n * @returns {bool}\n */\n Tree.prototype.isGroupCollapsed = function(item) {\n return item.attr('aria-expanded') === 'false';\n };\n\n /**\n * Determines if the given group item (contains child tree items) can be collapsed.\n *\n * @method isGroupCollapsible\n * @param {object} item jquery object representing a group item on the tree.\n * @returns {bool}\n */\n Tree.prototype.isGroupCollapsible = function(item) {\n return item.attr('data-collapsible') !== 'false';\n };\n\n /**\n * Performs the tree initialisation for all child items from the given node,\n * such as removing everything from the tab order and setting aria selected\n * on items.\n *\n * @method initialiseNodes\n * @param {object} node jquery object representing a node.\n */\n Tree.prototype.initialiseNodes = function(node) {\n this.removeAllFromTabOrder(node);\n this.setAriaSelectedFalseOnItems(node);\n\n // Get all ajax nodes that have been rendered as expanded but haven't loaded the child items yet.\n var thisTree = this;\n node.find(SELECTORS.UNLOADED_AJAX_ITEM).each(function() {\n var unloadedNode = $(this);\n // Collapse and then expand to trigger the ajax loading.\n thisTree.collapseGroup(unloadedNode);\n thisTree.expandGroup(unloadedNode);\n });\n };\n\n /**\n * Removes all child DOM elements of the given node from the tab order.\n *\n * @method removeAllFromTabOrder\n * @param {object} node jquery object representing a node.\n */\n Tree.prototype.removeAllFromTabOrder = function(node) {\n node.find('*').attr('tabindex', '-1');\n this.getGroupFromItem($(node)).find('*').attr('tabindex', '-1');\n };\n\n /**\n * Find all child tree items from the given node and set the aria selected attribute to false.\n *\n * @method setAriaSelectedFalseOnItems\n * @param {object} node jquery object representing a node.\n */\n Tree.prototype.setAriaSelectedFalseOnItems = function(node) {\n node.find(SELECTORS.ITEM).attr('aria-selected', 'false');\n };\n\n /**\n * Expand all group nodes within the tree.\n *\n * @method expandAllGroups\n */\n Tree.prototype.expandAllGroups = function() {\n var thisTree = this;\n\n this.treeRoot.find(SELECTORS.CLOSED_GROUP).each(function() {\n var groupNode = $(this);\n\n thisTree.expandGroup($(this)).done(function() {\n thisTree.expandAllChildGroups(groupNode);\n });\n });\n };\n\n /**\n * Find all child group nodes from the given node and expand them.\n *\n * @method expandAllChildGroups\n * @param {Object} item is the jquery id of the group.\n */\n Tree.prototype.expandAllChildGroups = function(item) {\n var thisTree = this;\n\n this.getGroupFromItem(item).find(SELECTORS.CLOSED_GROUP).each(function() {\n var groupNode = $(this);\n\n thisTree.expandGroup($(this)).done(function() {\n thisTree.expandAllChildGroups(groupNode);\n });\n });\n };\n\n /**\n * Expand a collapsed group.\n *\n * Handles expanding nodes that are ajax loaded (marked with a data-requires-ajax attribute).\n *\n * @method expandGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n * @return {Object} a promise that is resolved when the group has been expanded.\n */\n Tree.prototype.expandGroup = function(item) {\n var promise = $.Deferred();\n // Ignore nodes that are explicitly maked as not expandable or are already expanded.\n if (item.attr('data-expandable') !== 'false' && this.isGroupCollapsed(item)) {\n // If this node requires ajax load and we haven't already loaded it.\n if (item.attr('data-requires-ajax') === 'true' && item.attr('data-loaded') !== 'true') {\n item.attr('data-loaded', false);\n // Get the closes ajax loading module specificed in the tree.\n var moduleName = item.closest('[data-ajax-loader]').attr('data-ajax-loader');\n var thisTree = this;\n // Flag this node as loading.\n const p = item.find('p');\n p.addClass('loading');\n // Require the ajax module (must be AMD) and try to load the items.\n require([moduleName], function(loader) {\n // All ajax module must implement a \"load\" method.\n loader.load(item).done(function() {\n item.attr('data-loaded', true);\n\n // Set defaults on the newly constructed part of the tree.\n thisTree.initialiseNodes(item);\n thisTree.finishExpandingGroup(item);\n // Make sure no child elements of the item we just loaded are tabbable.\n p.removeClass('loading');\n promise.resolve();\n });\n });\n } else {\n this.finishExpandingGroup(item);\n promise.resolve();\n }\n } else {\n promise.resolve();\n }\n return promise;\n };\n\n /**\n * Perform the necessary DOM changes to display a group item.\n *\n * @method finishExpandingGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n */\n Tree.prototype.finishExpandingGroup = function(item) {\n // Expand the group.\n var group = this.getGroupFromItem(item);\n group.removeAttr('aria-hidden');\n item.attr('aria-expanded', 'true');\n\n // Update the list of visible items.\n this.refreshVisibleItemsCache();\n };\n\n /**\n * Collapse an expanded group.\n *\n * @method collapseGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n */\n Tree.prototype.collapseGroup = function(item) {\n // If the item is not collapsible or already collapsed then do nothing.\n if (!this.isGroupCollapsible(item) || this.isGroupCollapsed(item)) {\n return;\n }\n\n // Collapse the group.\n var group = this.getGroupFromItem(item);\n group.attr('aria-hidden', 'true');\n item.attr('aria-expanded', 'false');\n\n // Update the list of visible items.\n this.refreshVisibleItemsCache();\n };\n\n /**\n * Expand or collapse a group.\n *\n * @method toggleGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n */\n Tree.prototype.toggleGroup = function(item) {\n if (item.attr('aria-expanded') === 'true') {\n this.collapseGroup(item);\n } else {\n this.expandGroup(item);\n }\n };\n\n /**\n * Handle a key down event - ie navigate the tree.\n *\n * @method handleKeyDown\n * @param {Event} e The event.\n */\n // This function should be simplified. In the meantime..\n // eslint-disable-next-line complexity\n Tree.prototype.handleKeyDown = function(e) {\n var item = $(e.target);\n var currentIndex = this.getVisibleItems()?.index(item);\n\n if ((e.altKey || e.ctrlKey || e.metaKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {\n // Do nothing.\n return;\n }\n\n switch (e.keyCode) {\n case this.keys.home: {\n // Jump to first item in tree.\n this.getVisibleItems().first().focus();\n\n e.preventDefault();\n return;\n }\n case this.keys.end: {\n // Jump to last visible item.\n this.getVisibleItems().last().focus();\n\n e.preventDefault();\n return;\n }\n case this.keys.enter: {\n var links = item.children('a').length ? item.children('a') : item.children().not(SELECTORS.GROUP).find('a');\n if (links.length) {\n if (links.first().data('overrides-tree-activation-key-handler')) {\n // If the link overrides handling of activation keys, let it do so.\n links.first().triggerHandler(e);\n } else if (typeof this.enterCallback === 'function') {\n // Use callback if there is one.\n this.enterCallback(item);\n } else {\n window.location.href = links.first().attr('href');\n }\n } else if (this.isGroupItem(item)) {\n this.toggleGroup(item, true);\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.space: {\n if (this.isGroupItem(item)) {\n this.toggleGroup(item, true);\n } else if (item.children('a').length) {\n var firstLink = item.children('a').first();\n\n if (firstLink.data('overrides-tree-activation-key-handler')) {\n firstLink.triggerHandler(e);\n }\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.left: {\n var focusParent = function(tree) {\n // Get the immediate visible parent group item that contains this element.\n tree.getVisibleItems().filter(function() {\n return tree.getGroupFromItem($(this)).has(item).length;\n }).focus();\n };\n\n // If this is a group item then collapse it and focus the parent group\n // in accordance with the aria spec.\n if (this.isGroupItem(item)) {\n if (this.isGroupCollapsed(item)) {\n focusParent(this);\n } else {\n this.collapseGroup(item);\n }\n } else {\n focusParent(this);\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.right: {\n // If this is a group item then expand it and focus the first child item\n // in accordance with the aria spec.\n if (this.isGroupItem(item)) {\n if (this.isGroupCollapsed(item)) {\n this.expandGroup(item);\n } else {\n // Move to the first item in the child group.\n this.getGroupFromItem(item).find(SELECTORS.ITEM).first().focus();\n }\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.up: {\n\n if (currentIndex > 0) {\n var prev = this.getVisibleItems().eq(currentIndex - 1);\n\n prev.focus();\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.down: {\n\n if (currentIndex < this.getVisibleItems().length - 1) {\n var next = this.getVisibleItems().eq(currentIndex + 1);\n\n next.focus();\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.asterisk: {\n // Expand all groups.\n this.expandAllGroups();\n e.preventDefault();\n return;\n }\n }\n };\n\n /**\n * Handle an item click.\n *\n * @param {Event} event the click event\n * @param {jQuery} item the item clicked\n */\n Tree.prototype.handleItemClick = function(event, item) {\n // Update the active item.\n item.focus();\n\n // If the item is a group node.\n if (this.isGroupItem(item)) {\n this.toggleGroup(item);\n }\n };\n\n /**\n * Handle a click (select).\n *\n * @method handleClick\n * @param {Event} event The event.\n */\n Tree.prototype.handleClick = function(event) {\n if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {\n // Do nothing.\n return;\n }\n\n // Get the closest tree item from the event target.\n var item = $(event.target).closest('[role=\"treeitem\"]');\n if (!item.is(event.currentTarget)) {\n return;\n }\n\n this.handleItemClick(event, item);\n };\n\n /**\n * Handle a focus event.\n *\n * @method handleFocus\n * @param {Event} e The event.\n */\n Tree.prototype.handleFocus = function(e) {\n this.setActiveItem($(e.target));\n };\n\n /**\n * Bind the event listeners we require.\n *\n * @method bindEventHandlers\n */\n Tree.prototype.bindEventHandlers = function() {\n // Bind event handlers to the tree items. Use event delegates to allow\n // for dynamically loaded parts of the tree.\n this.treeRoot.on({\n click: this.handleClick.bind(this),\n keydown: this.handleKeyDown.bind(this),\n focus: this.handleFocus.bind(this),\n }, SELECTORS.ITEM);\n };\n\n return /** @alias module:core/tree */ Tree;\n});\n"],"names":["define","$","SELECTORS","Tree","selector","selectCallback","treeRoot","data","keys","tab","enter","space","pageup","pagedown","end","home","left","up","right","down","asterisk","initialiseNodes","this","setActiveItem","find","refreshVisibleItemsCache","bindEventHandlers","prototype","registerEnterCallback","callback","enterCallback","getVisibleItems","item","currentActive","attr","isGroupItem","is","getGroupFromItem","ariaowns","plain","children","length","isGroupCollapsed","isGroupCollapsible","node","removeAllFromTabOrder","setAriaSelectedFalseOnItems","thisTree","each","unloadedNode","collapseGroup","expandGroup","expandAllGroups","groupNode","done","expandAllChildGroups","promise","Deferred","moduleName","closest","p","addClass","require","loader","load","finishExpandingGroup","removeClass","resolve","removeAttr","toggleGroup","handleKeyDown","e","target","currentIndex","_this$getVisibleItems","index","altKey","ctrlKey","metaKey","shiftKey","keyCode","first","focus","preventDefault","last","links","not","triggerHandler","window","location","href","firstLink","focusParent","tree","filter","has","eq","handleItemClick","event","handleClick","currentTarget","handleFocus","on","click","bind","keydown"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"tree.min.js","sources":["../src/tree.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 * Implement an accessible aria tree widget, from a nested unordered list.\n * Based on http://oaa-accessibility.org/example/41/.\n *\n * @module core/tree\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery'], function($) {\n // Private variables and functions.\n var SELECTORS = {\n ITEM: '[role=treeitem]',\n GROUP: '[role=treeitem]:has([role=group]), [role=treeitem][aria-owns], [role=treeitem][data-requires-ajax=true]',\n CLOSED_GROUP: '[role=treeitem]:has([role=group])[aria-expanded=false], [role=treeitem][aria-owns][aria-expanded=false], ' +\n '[role=treeitem][data-requires-ajax=true][aria-expanded=false]',\n FIRST_ITEM: '[role=treeitem]:first',\n VISIBLE_ITEM: '[role=treeitem]:visible',\n UNLOADED_AJAX_ITEM: '[role=treeitem][data-requires-ajax=true][data-loaded=false][aria-expanded=true]'\n };\n\n /**\n * Constructor.\n *\n * @param {String} selector\n * @param {function} selectCallback Called when the active node is changed.\n */\n var Tree = function(selector, selectCallback) {\n this.treeRoot = $(selector);\n\n this.treeRoot.data('activeItem', null);\n this.selectCallback = selectCallback;\n this.keys = {\n tab: 9,\n enter: 13,\n space: 32,\n pageup: 33,\n pagedown: 34,\n end: 35,\n home: 36,\n left: 37,\n up: 38,\n right: 39,\n down: 40,\n asterisk: 106\n };\n\n // Apply the standard default initialisation for all nodes, starting with the tree root.\n this.initialiseNodes(this.treeRoot);\n // Make the first item the active item for the tree so that it is added to the tab order.\n this.setActiveItem(this.treeRoot.find(SELECTORS.FIRST_ITEM));\n // Create the cache of the visible items.\n this.refreshVisibleItemsCache();\n // Create the event handlers for the tree.\n this.bindEventHandlers();\n };\n\n Tree.prototype.registerEnterCallback = function(callback) {\n this.enterCallback = callback;\n };\n\n /**\n * Find all visible tree items and save a cache of them on the tree object.\n *\n * @method refreshVisibleItemsCache\n */\n Tree.prototype.refreshVisibleItemsCache = function() {\n this.treeRoot.data('visibleItems', this.treeRoot.find(SELECTORS.VISIBLE_ITEM));\n };\n\n /**\n * Get all visible tree items.\n *\n * @method getVisibleItems\n * @return {Object} visible items\n */\n Tree.prototype.getVisibleItems = function() {\n return this.treeRoot.data('visibleItems');\n };\n\n /**\n * Mark the given item as active within the tree and fire the callback for when the active item is set.\n *\n * @method setActiveItem\n * @param {object} item jquery object representing an item on the tree.\n */\n Tree.prototype.setActiveItem = function(item) {\n var currentActive = this.treeRoot.data('activeItem');\n if (item === currentActive) {\n return;\n }\n\n // Remove previous active from tab order.\n if (currentActive) {\n currentActive.attr('tabindex', '-1');\n currentActive.attr('aria-selected', 'false');\n }\n item.attr('tabindex', '0');\n item.attr('aria-selected', 'true');\n\n // Set the new active item.\n this.treeRoot.data('activeItem', item);\n\n if (typeof this.selectCallback === 'function') {\n this.selectCallback(item);\n }\n };\n\n /**\n * Determines if the given item is a group item (contains child tree items) in the tree.\n *\n * @method isGroupItem\n * @param {object} item jquery object representing an item on the tree.\n * @returns {bool}\n */\n Tree.prototype.isGroupItem = function(item) {\n return item.is(SELECTORS.GROUP);\n };\n\n /**\n * Determines if the given item is a group item (contains child tree items) in the tree.\n *\n * @method isGroupItem\n * @param {object} item jquery object representing an item on the tree.\n * @returns {bool}\n */\n Tree.prototype.getGroupFromItem = function(item) {\n var ariaowns = this.treeRoot.find('#' + item.attr('aria-owns'));\n var plain = item.children('[role=group]');\n if (ariaowns.length > plain.length) {\n return ariaowns;\n } else {\n return plain;\n }\n };\n\n /**\n * Determines if the given group item (contains child tree items) is collapsed.\n *\n * @method isGroupCollapsed\n * @param {object} item jquery object representing a group item on the tree.\n * @returns {bool}\n */\n Tree.prototype.isGroupCollapsed = function(item) {\n return item.attr('aria-expanded') === 'false';\n };\n\n /**\n * Determines if the given group item (contains child tree items) can be collapsed.\n *\n * @method isGroupCollapsible\n * @param {object} item jquery object representing a group item on the tree.\n * @returns {bool}\n */\n Tree.prototype.isGroupCollapsible = function(item) {\n return item.attr('data-collapsible') !== 'false';\n };\n\n /**\n * Performs the tree initialisation for all child items from the given node,\n * such as removing everything from the tab order and setting aria selected\n * on items.\n *\n * @method initialiseNodes\n * @param {object} node jquery object representing a node.\n */\n Tree.prototype.initialiseNodes = function(node) {\n this.removeAllFromTabOrder(node);\n this.setAriaSelectedFalseOnItems(node);\n\n // Get all ajax nodes that have been rendered as expanded but haven't loaded the child items yet.\n var thisTree = this;\n node.find(SELECTORS.UNLOADED_AJAX_ITEM).each(function() {\n var unloadedNode = $(this);\n // Collapse and then expand to trigger the ajax loading.\n thisTree.collapseGroup(unloadedNode);\n thisTree.expandGroup(unloadedNode);\n });\n };\n\n /**\n * Removes all child DOM elements of the given node from the tab order.\n *\n * @method removeAllFromTabOrder\n * @param {object} node jquery object representing a node.\n */\n Tree.prototype.removeAllFromTabOrder = function(node) {\n node.find('*').attr('tabindex', '-1');\n this.getGroupFromItem($(node)).find('*').attr('tabindex', '-1');\n };\n\n /**\n * Find all child tree items from the given node and set the aria selected attribute to false.\n *\n * @method setAriaSelectedFalseOnItems\n * @param {object} node jquery object representing a node.\n */\n Tree.prototype.setAriaSelectedFalseOnItems = function(node) {\n node.find(SELECTORS.ITEM).attr('aria-selected', 'false');\n };\n\n /**\n * Expand all group nodes within the tree.\n *\n * @method expandAllGroups\n */\n Tree.prototype.expandAllGroups = function() {\n var thisTree = this;\n\n this.treeRoot.find(SELECTORS.CLOSED_GROUP).each(function() {\n var groupNode = $(this);\n\n thisTree.expandGroup($(this)).done(function() {\n thisTree.expandAllChildGroups(groupNode);\n });\n });\n };\n\n /**\n * Find all child group nodes from the given node and expand them.\n *\n * @method expandAllChildGroups\n * @param {Object} item is the jquery id of the group.\n */\n Tree.prototype.expandAllChildGroups = function(item) {\n var thisTree = this;\n\n this.getGroupFromItem(item).find(SELECTORS.CLOSED_GROUP).each(function() {\n var groupNode = $(this);\n\n thisTree.expandGroup($(this)).done(function() {\n thisTree.expandAllChildGroups(groupNode);\n });\n });\n };\n\n /**\n * Expand a collapsed group.\n *\n * Handles expanding nodes that are ajax loaded (marked with a data-requires-ajax attribute).\n *\n * @method expandGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n * @return {Object} a promise that is resolved when the group has been expanded.\n */\n Tree.prototype.expandGroup = function(item) {\n var promise = $.Deferred();\n // Ignore nodes that are explicitly maked as not expandable or are already expanded.\n if (item.attr('data-expandable') !== 'false' && this.isGroupCollapsed(item)) {\n // If this node requires ajax load and we haven't already loaded it.\n if (item.attr('data-requires-ajax') === 'true' && item.attr('data-loaded') !== 'true') {\n item.attr('data-loaded', false);\n // Get the closes ajax loading module specificed in the tree.\n var moduleName = item.closest('[data-ajax-loader]').attr('data-ajax-loader');\n var thisTree = this;\n // Flag this node as loading.\n const p = item.find('p');\n p.addClass('loading');\n // Require the ajax module (must be AMD) and try to load the items.\n require([moduleName], function(loader) {\n // All ajax module must implement a \"load\" method.\n loader.load(item).done(function() {\n item.attr('data-loaded', true);\n\n // Set defaults on the newly constructed part of the tree.\n thisTree.initialiseNodes(item);\n thisTree.finishExpandingGroup(item);\n // Make sure no child elements of the item we just loaded are tabbable.\n p.removeClass('loading');\n promise.resolve();\n });\n });\n } else {\n this.finishExpandingGroup(item);\n promise.resolve();\n }\n } else {\n promise.resolve();\n }\n return promise;\n };\n\n /**\n * Perform the necessary DOM changes to display a group item.\n *\n * @method finishExpandingGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n */\n Tree.prototype.finishExpandingGroup = function(item) {\n // Expand the group.\n var group = this.getGroupFromItem(item);\n group.removeAttr('aria-hidden');\n item.attr('aria-expanded', 'true');\n\n // Update the list of visible items.\n this.refreshVisibleItemsCache();\n };\n\n /**\n * Collapse an expanded group.\n *\n * @method collapseGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n */\n Tree.prototype.collapseGroup = function(item) {\n // If the item is not collapsible or already collapsed then do nothing.\n if (!this.isGroupCollapsible(item) || this.isGroupCollapsed(item)) {\n return;\n }\n\n // Collapse the group.\n var group = this.getGroupFromItem(item);\n group.attr('aria-hidden', 'true');\n item.attr('aria-expanded', 'false');\n\n // Update the list of visible items.\n this.refreshVisibleItemsCache();\n };\n\n /**\n * Expand or collapse a group.\n *\n * @method toggleGroup\n * @param {Object} item is the jquery id of the parent item of the group.\n */\n Tree.prototype.toggleGroup = function(item) {\n if (item.attr('aria-expanded') === 'true') {\n this.collapseGroup(item);\n } else {\n this.expandGroup(item);\n }\n };\n\n /**\n * Handle a key down event - ie navigate the tree.\n *\n * @method handleKeyDown\n * @param {Event} e The event.\n */\n // This function should be simplified. In the meantime..\n // eslint-disable-next-line complexity\n Tree.prototype.handleKeyDown = function(e) {\n var item = $(e.target);\n var currentIndex = this.getVisibleItems()?.index(item);\n\n if ((e.altKey || e.ctrlKey || e.metaKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {\n // Do nothing.\n return;\n }\n\n // Detect RTL mode and swap left/right arrow keys accordingly.\n const rtl = window.right_to_left();\n const collapseKey = rtl ? this.keys.right : this.keys.left;\n const expandKey = rtl ? this.keys.left : this.keys.right;\n\n switch (e.keyCode) {\n case this.keys.home: {\n // Jump to first item in tree.\n this.getVisibleItems().first().focus();\n\n e.preventDefault();\n return;\n }\n case this.keys.end: {\n // Jump to last visible item.\n this.getVisibleItems().last().focus();\n\n e.preventDefault();\n return;\n }\n case this.keys.enter: {\n var links = item.children('a').length ? item.children('a') : item.children().not(SELECTORS.GROUP).find('a');\n if (links.length) {\n if (links.first().data('overrides-tree-activation-key-handler')) {\n // If the link overrides handling of activation keys, let it do so.\n links.first().triggerHandler(e);\n } else if (typeof this.enterCallback === 'function') {\n // Use callback if there is one.\n this.enterCallback(item);\n } else {\n window.location.href = links.first().attr('href');\n }\n } else if (this.isGroupItem(item)) {\n this.toggleGroup(item, true);\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.space: {\n if (this.isGroupItem(item)) {\n this.toggleGroup(item, true);\n } else if (item.children('a').length) {\n var firstLink = item.children('a').first();\n\n if (firstLink.data('overrides-tree-activation-key-handler')) {\n firstLink.triggerHandler(e);\n }\n }\n\n e.preventDefault();\n return;\n }\n case collapseKey: {\n var focusParent = function(tree) {\n // Get the immediate visible parent group item that contains this element.\n tree.getVisibleItems().filter(function() {\n return tree.getGroupFromItem($(this)).has(item).length;\n }).focus();\n };\n\n // If this is a group item then collapse it and focus the parent group\n // in accordance with the aria spec.\n if (this.isGroupItem(item)) {\n if (this.isGroupCollapsed(item)) {\n focusParent(this);\n } else {\n this.collapseGroup(item);\n }\n } else {\n focusParent(this);\n }\n\n e.preventDefault();\n return;\n }\n case expandKey: {\n // If this is a group item then expand it and focus the first child item\n // in accordance with the aria spec.\n if (this.isGroupItem(item)) {\n if (this.isGroupCollapsed(item)) {\n this.expandGroup(item);\n } else {\n // Move to the first item in the child group.\n this.getGroupFromItem(item).find(SELECTORS.ITEM).first().focus();\n }\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.up: {\n\n if (currentIndex > 0) {\n var prev = this.getVisibleItems().eq(currentIndex - 1);\n\n prev.focus();\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.down: {\n\n if (currentIndex < this.getVisibleItems().length - 1) {\n var next = this.getVisibleItems().eq(currentIndex + 1);\n\n next.focus();\n }\n\n e.preventDefault();\n return;\n }\n case this.keys.asterisk: {\n // Expand all groups.\n this.expandAllGroups();\n e.preventDefault();\n return;\n }\n }\n };\n\n /**\n * Handle an item click.\n *\n * @param {Event} event the click event\n * @param {jQuery} item the item clicked\n */\n Tree.prototype.handleItemClick = function(event, item) {\n // Update the active item.\n item.focus();\n\n // If the item is a group node.\n if (this.isGroupItem(item)) {\n this.toggleGroup(item);\n }\n };\n\n /**\n * Handle a click (select).\n *\n * @method handleClick\n * @param {Event} event The event.\n */\n Tree.prototype.handleClick = function(event) {\n if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {\n // Do nothing.\n return;\n }\n\n // Get the closest tree item from the event target.\n var item = $(event.target).closest('[role=\"treeitem\"]');\n if (!item.is(event.currentTarget)) {\n return;\n }\n\n this.handleItemClick(event, item);\n };\n\n /**\n * Handle a focus event.\n *\n * @method handleFocus\n * @param {Event} e The event.\n */\n Tree.prototype.handleFocus = function(e) {\n this.setActiveItem($(e.target));\n };\n\n /**\n * Bind the event listeners we require.\n *\n * @method bindEventHandlers\n */\n Tree.prototype.bindEventHandlers = function() {\n // Bind event handlers to the tree items. Use event delegates to allow\n // for dynamically loaded parts of the tree.\n this.treeRoot.on({\n click: this.handleClick.bind(this),\n keydown: this.handleKeyDown.bind(this),\n focus: this.handleFocus.bind(this),\n }, SELECTORS.ITEM);\n };\n\n return /** @alias module:core/tree */ Tree;\n});\n"],"names":["define","$","SELECTORS","Tree","selector","selectCallback","treeRoot","data","keys","tab","enter","space","pageup","pagedown","end","home","left","up","right","down","asterisk","initialiseNodes","this","setActiveItem","find","refreshVisibleItemsCache","bindEventHandlers","prototype","registerEnterCallback","callback","enterCallback","getVisibleItems","item","currentActive","attr","isGroupItem","is","getGroupFromItem","ariaowns","plain","children","length","isGroupCollapsed","isGroupCollapsible","node","removeAllFromTabOrder","setAriaSelectedFalseOnItems","thisTree","each","unloadedNode","collapseGroup","expandGroup","expandAllGroups","groupNode","done","expandAllChildGroups","promise","Deferred","moduleName","closest","p","addClass","require","loader","load","finishExpandingGroup","removeClass","resolve","removeAttr","toggleGroup","handleKeyDown","e","target","currentIndex","_this$getVisibleItems","index","altKey","ctrlKey","metaKey","shiftKey","keyCode","rtl","window","right_to_left","collapseKey","expandKey","first","focus","preventDefault","last","links","not","triggerHandler","location","href","firstLink","focusParent","tree","filter","has","eq","handleItemClick","event","handleClick","currentTarget","handleFocus","on","click","bind","keydown"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/amd/src/edit_switch.js b/lib/amd/src/edit_switch.js
index 0c545e053a0d2..746be157ba2c4 100644
--- a/lib/amd/src/edit_switch.js
+++ b/lib/amd/src/edit_switch.js
@@ -24,6 +24,7 @@
import {call as fetchMany} from 'core/ajax';
import {dispatchEvent} from 'core/event_dispatcher';
import {exception as displayException} from 'core/notification';
+import Pending from "core/pending";
/**
* Change the Edit mode.
@@ -58,7 +59,9 @@ const toggleEditSwitch = editSwitch => {
if (!event.defaultPrevented) {
editSwitch.setAttribute('disabled', true);
window.location = editSwitch.dataset.pageurl;
+ return true;
}
+ return false;
};
/**
@@ -108,15 +111,23 @@ const notifyEditModeSet = (container, editMode) => dispatchEvent(
export const init = editingSwitchId => {
const editSwitch = document.getElementById(editingSwitchId);
editSwitch.addEventListener('change', () => {
+ const pendingPromise = new Pending("core/edit_switch:toggle");
setEditMode(editSwitch.dataset.context, editSwitch.checked)
- .then(result => {
+ .then((result) => {
if (result.success) {
- toggleEditSwitch(editSwitch);
+ const redirected = toggleEditSwitch(editSwitch);
+ if (!redirected) {
+ pendingPromise.resolve();
+ }
} else {
editSwitch.checked = false;
+ pendingPromise.resolve();
}
return;
})
- .catch(displayException);
+ .catch((error) => {
+ pendingPromise.resolve();
+ displayException(error);
+ });
});
};
diff --git a/lib/amd/src/paged_content_paging_bar.js b/lib/amd/src/paged_content_paging_bar.js
index 9bc656f62c9d4..53d70fa28db55 100644
--- a/lib/amd/src/paged_content_paging_bar.js
+++ b/lib/amd/src/paged_content_paging_bar.js
@@ -408,8 +408,11 @@ function(
var pageItems = root.find(SELECTORS.PAGE_ITEM);
// We want to request all of the strings at once rather than
// one at a time.
- var stringRequests = pageItems.toArray().map(function(index, page) {
+ var stringRequests = pageItems.toArray().map(function(page) {
page = $(page);
+ if (page.attr('data-page') == undefined) {
+ return {};
+ }
var pageNumber = getPageNumber(root, page);
if (pageNumber === activePageNumber) {
@@ -425,7 +428,7 @@ function(
param: pageNumber
};
}
- });
+ }).filter(Boolean);
Str.get_strings(stringRequests).then(function(strings) {
pageItems.each(function(index, page) {
diff --git a/lib/amd/src/tree.js b/lib/amd/src/tree.js
index 8b7b1261cc3ba..8190f56ab54b4 100644
--- a/lib/amd/src/tree.js
+++ b/lib/amd/src/tree.js
@@ -362,6 +362,11 @@ define(['jquery'], function($) {
return;
}
+ // Detect RTL mode and swap left/right arrow keys accordingly.
+ const rtl = window.right_to_left();
+ const collapseKey = rtl ? this.keys.right : this.keys.left;
+ const expandKey = rtl ? this.keys.left : this.keys.right;
+
switch (e.keyCode) {
case this.keys.home: {
// Jump to first item in tree.
@@ -410,7 +415,7 @@ define(['jquery'], function($) {
e.preventDefault();
return;
}
- case this.keys.left: {
+ case collapseKey: {
var focusParent = function(tree) {
// Get the immediate visible parent group item that contains this element.
tree.getVisibleItems().filter(function() {
@@ -433,7 +438,7 @@ define(['jquery'], function($) {
e.preventDefault();
return;
}
- case this.keys.right: {
+ case expandKey: {
// If this is a group item then expand it and focus the first child item
// in accordance with the aria spec.
if (this.isGroupItem(item)) {
diff --git a/lib/behat/classes/partial_named_selector.php b/lib/behat/classes/partial_named_selector.php
index 6363d443bcd60..44e4882c2ca4c 100644
--- a/lib/behat/classes/partial_named_selector.php
+++ b/lib/behat/classes/partial_named_selector.php
@@ -127,6 +127,7 @@ public function __construct() {
'table' => 'table',
'table_row' => 'table_row',
'text' => 'text',
+ 'toast_message' => 'toast_message',
'xpath_element' => 'xpath_element',
'form_row' => 'form_row',
'autocomplete_selection' => 'autocomplete_selection',
@@ -279,6 +280,9 @@ public function __construct() {
XPATH
, 'text' => << << <<content;
} else {
$data['nodearray'] = (array) $this->content;
+ // If there is no node array to render then return an empty array.
+ if (empty($data['nodearray'])) {
+ return [];
+ }
}
$data['moremenuid'] = uniqid();
diff --git a/lib/classes/output/context_header.php b/lib/classes/output/context_header.php
index b3030ced3afcd..7327669401b14 100644
--- a/lib/classes/output/context_header.php
+++ b/lib/classes/output/context_header.php
@@ -111,8 +111,11 @@ protected function format_button_images() {
*/
public function export_for_template(renderer_base $output): array {
// Heading.
+ $heading = '';
$headingtext = isset($this->heading) ? $this->heading : $output->get_page()->heading;
- $heading = $output->heading($headingtext, $this->headinglevel, "h2 mb-0");
+ if ('' !== $headingtext) {
+ $heading = $output->heading($headingtext, $this->headinglevel, "h2 mb-0");
+ }
// Buttons.
if (isset($this->additionalbuttons)) {
diff --git a/lib/classes/output/core_renderer.php b/lib/classes/output/core_renderer.php
index 2e643ba45cce8..b2c93da8646ea 100644
--- a/lib/classes/output/core_renderer.php
+++ b/lib/classes/output/core_renderer.php
@@ -1697,23 +1697,35 @@ public function action_icon(
return $this->action_link($url, $text . $icon, $action, $attributes);
}
- /**
- * Print a message along with button choices for Continue/Cancel
- *
- * If a string or moodle_url is given instead of a single_button, method defaults to post.
- *
- * @param string $message The question to ask the user
- * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer. Can also be a moodle_url or string URL
- * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer. Can also be a moodle_url or string URL
- * @param array $displayoptions optional extra display options
- * @return string HTML fragment
- */
+ /**
+ * Print a message along with button choices for Continue/Cancel
+ *
+ * If a string or moodle_url is given instead of a single_button, method defaults to post.
+ *
+ * @param string $message The question to ask the user
+ * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer.
+ * Can also be a moodle_url or string URL
+ * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer.
+ * Can also be a moodle_url or string URL
+ * @param array $displayoptions Display options (Optional).
+ * Possible options:
+ * - confirmtitle: The title to display above the message
+ * - continuestr: The label to use for the continue button (if $continue is not a single_button)
+ * - cancelstr: The label to use for the cancel button (if $cancel is not a single_button)
+ * - headinglevel: The heading level to use for the title (1-6). Default is 4.
+ * - type: The button type to use for the continue button (if $continue is not a single_button). Default is BUTTON_PRIMARY.
+ * @return string HTML fragment
+ */
public function confirm($message, $continue, $cancel, array $displayoptions = []) {
// Check existing displayoptions.
$displayoptions['confirmtitle'] = $displayoptions['confirmtitle'] ?? get_string('confirm');
$displayoptions['continuestr'] = $displayoptions['continuestr'] ?? get_string('continue');
$displayoptions['cancelstr'] = $displayoptions['cancelstr'] ?? get_string('cancel');
+ $headinglevel = $displayoptions['headinglevel'] ?? 4;
+ if ($headinglevel < 1 || $headinglevel > 6) {
+ throw new coding_exception('The headinglevel option to $OUTPUT->confirm() must be between 1 and 6.');
+ }
if ($continue instanceof single_button) {
// Continue button should be primary if set to secondary type as it is the fefault.
@@ -1758,7 +1770,7 @@ public function confirm($message, $continue, $cancel, array $displayoptions = []
$output = $this->box_start('generalbox modal modal-dialog modal-in-page show', 'notice', $attributes);
$output .= $this->box_start('modal-content', 'modal-content');
$output .= $this->box_start('modal-header px-3', 'modal-header');
- $output .= html_writer::tag('h4', $displayoptions['confirmtitle']);
+ $output .= html_writer::tag('h' . $headinglevel, $displayoptions['confirmtitle'], ['class' => 'h4']);
$output .= $this->box_end();
$attributes = [
'role' => 'alert',
diff --git a/lib/db/upgradelib.php b/lib/db/upgradelib.php
index 876b05dfb2e7c..e259b5f6b39a6 100644
--- a/lib/db/upgradelib.php
+++ b/lib/db/upgradelib.php
@@ -1274,62 +1274,56 @@ function upgrade_block_delete_instances(
): void {
global $DB;
- $deleteblockinstances = function (string $instanceselect, array $instanceparams) use ($DB) {
- $deletesql = <<delete_records_subquery('context', 'id', 'cid', $deletesql, array_merge($instanceparams, [
- 'contextlevel' => CONTEXT_BLOCK,
- ]));
-
- $deletesql = <<delete_records_subquery('block_positions', 'id', 'bpid', $deletesql, $instanceparams);
-
- $blockhidden = $DB->sql_concat("'block'", 'bi.id', "'hidden'");
- $blockdocked = $DB->sql_concat("'docked_block_instance_'", 'bi.id');
- $deletesql = <<delete_records_subquery('user_preferences', 'id', 'pid', $deletesql, $instanceparams);
+ $deleteblockinstances = function (array $blockids) use ($DB) {
+ [$insql, $inparams] = $DB->get_in_or_equal($blockids, SQL_PARAMS_NAMED, 'blockid');
+ $contextparams = array_merge(['contextlevel' => CONTEXT_BLOCK], $inparams);
+ $DB->delete_records_select('context', "contextlevel = :contextlevel AND instanceid {$insql}", $contextparams);
- $deletesql = <<delete_records_subquery('block_instances', 'id', 'bid', $deletesql, $instanceparams);
+ $DB->delete_records_select('block_positions', "blockinstanceid {$insql}", $inparams);
+
+ $DB->delete_records_select('block_instances', "id {$insql}", $inparams);
+ };
+
+ $deletepreferences = function (array $prefids) use ($DB) {
+ [$insql, $params] = $DB->get_in_or_equal($prefids);
+ $DB->delete_records_select('user_preferences', "id {$insql}", $params);
+ };
+
+ $targetids = [];
+ $batchsize = 1000;
+
+ $collectids = function ($recordset) use (&$targetids) {
+ foreach ($recordset as $record) {
+ $targetids[(int)$record->id] = true;
+ }
+ $recordset->close();
};
- // Delete the default indexsys version of the block.
+ $collectpreferences = function ($recordset, $targetids) {
+ $prefstodelete = [];
+ foreach ($recordset as $pref) {
+ $blockid = (int)str_replace(['docked_block_instance_', 'block', 'hidden'], '', $pref->name);
+ if (isset($targetids[$blockid])) {
+ $prefstodelete[] = $pref->id;
+ }
+ }
+ $recordset->close();
+ return $prefstodelete;
+ };
+
+ // Collect block IDs from the default indexsys version of the block.
$subpagepattern = $DB->get_record('my_pages', [
'userid' => null,
'name' => $pagename,
'private' => MY_PAGE_PRIVATE,
], 'id', IGNORE_MULTIPLE)->id;
- $instanceselect = <<get_recordset('block_instances', [
'blockname' => $blockname,
'pagetypepattern' => $pagetypepattern,
'subpagepattern' => $subpagepattern,
- ];
- $deleteblockinstances($instanceselect, $params);
+ ], '', 'id');
+ $collectids($recordset);
// The subpagepattern is a string.
// In all core blocks it contains a string represnetation of an integer, but it is theoretically possible for a
@@ -1339,30 +1333,54 @@ function upgrade_block_delete_instances(
// Look for any and all instances of the block in customised /my pages.
$subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false);
- $instanceselect = <<get_recordset_sql($customblockssql, [
'blockname' => $blockname,
'pagetypepattern' => $pagetypepattern,
'pagename' => $pagename,
'private' => MY_PAGE_PRIVATE,
- ];
+ ]);
+ $collectids($recordset);
+
+ if (empty($targetids)) {
+ return;
+ }
+
+ // Collect preference IDs associated with these blocks.
+ $dockedlike = $DB->sql_like('name', ':docked', false);
+ $hiddenlike = $DB->sql_like('name', ':hidden', false);
+ $prefssql = <<get_recordset_sql($prefssql, [
+ 'docked' => 'docked_block_instance_%',
+ 'hidden' => 'block%hidden',
+ ]);
+ $prefstodelete = $collectpreferences($recordset, $targetids);
- $deleteblockinstances($instanceselect, $params);
+ // Delete preferences in batches.
+ foreach (array_chunk($prefstodelete, $batchsize) as $batch) {
+ $deletepreferences($batch);
+ }
+
+ // Delete block instances and related records in batches.
+ foreach (array_chunk(array_keys($targetids), $batchsize) as $batch) {
+ $deleteblockinstances($batch);
+ }
}
/**
diff --git a/lib/lti1p3/readme_moodle.txt b/lib/lti1p3/readme_moodle.txt
index ddb6b0778b4e4..d92c09b1cb613 100644
--- a/lib/lti1p3/readme_moodle.txt
+++ b/lib/lti1p3/readme_moodle.txt
@@ -2,6 +2,7 @@ LTI 1.3 Tool Library import instructions
This library is a patched for use in Moodle - it requires the following changes be applied on top of the packback upstream base:
1. Removal of phpseclib dependency (replaces a single call with openssl equivalent)
+2. The fix included in MDL-87789, ensuring optional lineitem properties are omitted if not set.
To upgrade to a new version of this library:
1. Clone the latest version of the upstream library from github:
diff --git a/lib/lti1p3/src/DeepLinkResources/Resource.php b/lib/lti1p3/src/DeepLinkResources/Resource.php
index 5cbb4b2be9a97..ecfbac6bdff9b 100644
--- a/lib/lti1p3/src/DeepLinkResources/Resource.php
+++ b/lib/lti1p3/src/DeepLinkResources/Resource.php
@@ -50,7 +50,7 @@ public function getArray(): array
if (isset($this->line_item)) {
$resource['lineItem'] = [
'scoreMaximum' => $this->line_item->getScoreMaximum(),
- 'label' => $this->line_item->getLabel(),
+ ...(!is_null($this->line_item->getLabel()) ? ['label' => $this->line_item->getLabel()] : []),
];
}
diff --git a/lib/templates/paging_bar.mustache b/lib/templates/paging_bar.mustache
index 845cef6b6cf5f..b659404f51d0c 100644
--- a/lib/templates/paging_bar.mustache
+++ b/lib/templates/paging_bar.mustache
@@ -51,7 +51,7 @@
}}
{{#haspages}}