MDL-68529 course: Refactor chooser to include loading

This commit is contained in:
Mathew May 2020-04-29 17:14:38 +08:00
parent 36e5a07a59
commit f2d033a2bb
12 changed files with 103 additions and 72 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -78,7 +78,6 @@ const registerListenerEvents = (courseId) => {
events.forEach((event) => {
document.addEventListener(event, async(e) => {
if (e.target.closest(selectors.elements.sectionmodchooser)) {
const data = await fetchModuleData();
// We need to know who called this.
// Standard courses use the ID in the main section info.
const sectionDiv = e.target.closest(selectors.elements.section);
@ -86,11 +85,31 @@ const registerListenerEvents = (courseId) => {
const button = e.target.closest(selectors.elements.sectionmodchooser);
// If we don't have a section ID use the fallback ID.
const caller = sectionDiv || button;
const favouriteFunction = partiallyAppliedFavouriteManager(data, caller.dataset.sectionid);
const builtModuleData = sectionIdMapper(data, caller.dataset.sectionid);
const sectionModal = await modalBuilder(builtModuleData);
ChooserDialogue.displayChooser(caller, sectionModal, builtModuleData, favouriteFunction);
// We want to show the modal instantly but loading whilst waiting for our data.
let bodyPromiseResolver;
const bodyPromise = new Promise(resolve => {
bodyPromiseResolver = resolve;
});
const sectionModal = buildModal(bodyPromise);
// Now we have a modal we should start fetching data.
const data = await fetchModuleData();
// Apply the section id to all the module instance links.
const builtModuleData = sectionIdMapper(data, caller.dataset.sectionid);
ChooserDialogue.displayChooser(
sectionModal,
builtModuleData,
partiallyAppliedFavouriteManager(data, caller.dataset.sectionid),
);
bodyPromiseResolver(await Templates.render(
'core_course/activitychooser',
templateDataBuilder(builtModuleData)
));
}
});
});
@ -102,7 +121,7 @@ const registerListenerEvents = (courseId) => {
*
* @method sectionIdMapper
* @param {Object} webServiceData Our original data from the Web service call
* @param {Array} id The ID of the section we need to append to the links
* @param {Number} id The ID of the section we need to append to the links
* @return {Array} [modules] with URL's built
*/
const sectionIdMapper = (webServiceData, id) => {
@ -114,15 +133,6 @@ const sectionIdMapper = (webServiceData, id) => {
return newData.content_items;
};
/**
* Build a modal on demand to save page load times
*
* @method modalBuilder
* @param {Array} data our array of modules with section ID's applied in the URL field
* @return {Object} Our modal that we are going to show the user
*/
const modalBuilder = data => buildModal(templateDataBuilder(data));
/**
* Given an array of modules we want to figure out where & how to place them into our template object
*
@ -158,18 +168,22 @@ const templateDataBuilder = (data) => {
* Given an object we want to build a modal ready to show
*
* @method buildModal
* @param {Object} data The template data which contains arrays of modules
* @return {Object} The modal for the calling section with everything already set up
* @param {Promise} bodyPromise
* @return {Object} The modal ready to display immediately and render body in later.
*/
const buildModal = data => {
const buildModal = bodyPromise => {
return ModalFactory.create({
type: ModalFactory.types.DEFAULT,
title: getString('addresourceoractivity'),
body: Templates.render('core_course/activitychooser', data),
body: bodyPromise,
large: true,
templateContext: {
classes: 'modchooser'
}
})
.then(modal => {
modal.show();
return modal;
});
};
@ -240,6 +254,7 @@ const partiallyAppliedFavouriteManager = (moduleData, sectionId) => {
if (favourite) {
result.favourite = true;
// eslint-disable-next-line camelcase
newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);
const builtFaves = sectionIdMapper(newFaves, sectionId);

@ -42,6 +42,7 @@ import {debounce} from 'core/utils';
const showModuleHelp = (carousel, moduleData) => {
const help = carousel.find(selectors.regions.help)[0];
help.innerHTML = '';
help.classList.add('m-auto');
// Add a spinner.
const spinnerPromise = addIconToContainer(help);
@ -483,6 +484,37 @@ const searchModules = (modules, searchTerm) => {
return searchResults;
};
/**
* Set up our tabindex information across the chooser.
*
* @method setupKeyboardAccessibility
* @param {Promise} modal Our created modal for the section
* @param {Map} mappedModules A map of all of the built module information
*/
const setupKeyboardAccessibility = (modal, mappedModules) => {
modal.getModal()[0].tabIndex = -1;
modal.getBodyPromise().then(body => {
$(selectors.elements.tab).on('shown.bs.tab', (e) => {
const activeSectionId = e.target.getAttribute("href");
const activeSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = activeSectionChooserOptions
.querySelector(selectors.regions.chooserOption.container);
const prevActiveSectionId = e.relatedTarget.getAttribute("href");
const prevActiveSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));
// Disable the focus of every chooser option in the previous active section.
disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
// Enable the focus of the first chooser option in the current active section.
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions);
});
return;
}).catch(Notification.exception);
};
/**
* Disable the focus of all chooser options in a specific container (section).
*
@ -500,13 +532,11 @@ const disableFocusAllChooserOptions = (sectionChooserOptions) => {
* Display the module chooser.
*
* @method displayChooser
* @param {HTMLElement} origin The calling button
* @param {Object} modal Our created modal for the section
* @param {Promise} modalPromise Our created modal for the section
* @param {Array} sectionModules An array of all of the built module information
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
*/
export const displayChooser = (origin, modal, sectionModules, partialFavourite) => {
export const displayChooser = (modalPromise, sectionModules, partialFavourite) => {
// Make a map so we can quickly fetch a specific module's object for either rendering or searching.
const mappedModules = new Map();
sectionModules.forEach((module) => {
@ -514,39 +544,18 @@ export const displayChooser = (origin, modal, sectionModules, partialFavourite)
});
// Register event listeners.
registerListenerEvents(modal, mappedModules, partialFavourite);
modalPromise.then(modal => {
registerListenerEvents(modal, mappedModules, partialFavourite);
// We want to focus on the action select when the dialog is closed.
modal.getRoot().on(ModalEvents.hidden, () => {
modal.destroy();
});
// We want to focus on the first chooser option element as soon as the modal is opened.
setupKeyboardAccessibility(modal, mappedModules);
// We want to focus on the first chooser option element as soon as the modal is opened.
modal.getRoot().on(ModalEvents.shown, () => {
modal.getModal()[0].tabIndex = -1;
// We want to focus on the action select when the dialog is closed.
modal.getRoot().on(ModalEvents.hidden, () => {
modal.destroy();
});
modal.getBodyPromise()
.then(body => {
$(selectors.elements.tab).on('shown.bs.tab', (e) => {
const activeSectionId = e.target.getAttribute("href");
const activeSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = activeSectionChooserOptions
.querySelector(selectors.regions.chooserOption.container);
const prevActiveSectionId = e.relatedTarget.getAttribute("href");
const prevActiveSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));
// Disable the focus of every chooser option in the previous active section.
disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
// Enable the focus of the first chooser option in the current active section.
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions);
});
return;
})
.catch(Notification.exception);
});
modal.show();
return modal;
})
.catch();
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -322,6 +322,9 @@ define([
var contentPromise = null;
body.css('overflow', 'hidden');
// Ensure that the `value` is a jQuery Promise.
value = $.when(value);
if (value.state() == 'pending') {
// We're still waiting for the body promise to resolve so
// let's show a loading icon.

@ -1511,7 +1511,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
*/
.modchooser .modal-body {
padding: 0;
height: 640px;
min-height: 640px;
overflow-y: auto;
.loading-icon {
@ -1521,9 +1521,11 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
font-size: 3em;
height: 1em;
width: 1em;
margin: 5em auto;
}
}
.carousel-item .loading-icon .icon {
margin: 5em auto;
}
}
.modchoosercontainer.noscroll {
@ -1582,7 +1584,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
background-color: $white;
overflow-x: hidden;
overflow-y: auto;
height: 640px;
min-height: 640px;
.content {
overflow-y: auto;

@ -10653,7 +10653,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
*/
.modchooser .modal-body {
padding: 0;
height: 640px;
min-height: 640px;
overflow-y: auto; }
.modchooser .modal-body .loading-icon {
opacity: 1; }
@ -10661,8 +10661,9 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
display: block;
font-size: 3em;
height: 1em;
width: 1em;
margin: 5em auto; }
width: 1em; }
.modchooser .modal-body .carousel-item .loading-icon .icon {
margin: 5em auto; }
.modchoosercontainer.noscroll {
overflow-y: hidden; }
@ -10708,7 +10709,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
background-color: #fff;
overflow-x: hidden;
overflow-y: auto;
height: 640px; }
min-height: 640px; }
.modchooser .modal-body .optionsummary .content {
overflow-y: auto; }
.modchooser .modal-body .optionsummary .content .heading .icon {

@ -10861,7 +10861,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
*/
.modchooser .modal-body {
padding: 0;
height: 640px;
min-height: 640px;
overflow-y: auto; }
.modchooser .modal-body .loading-icon {
opacity: 1; }
@ -10869,8 +10869,9 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
display: block;
font-size: 3em;
height: 1em;
width: 1em;
margin: 5em auto; }
width: 1em; }
.modchooser .modal-body .carousel-item .loading-icon .icon {
margin: 5em auto; }
.modchoosercontainer.noscroll {
overflow-y: hidden; }
@ -10916,7 +10917,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
background-color: #fff;
overflow-x: hidden;
overflow-y: auto;
height: 640px; }
min-height: 640px; }
.modchooser .modal-body .optionsummary .content {
overflow-y: auto; }
.modchooser .modal-body .optionsummary .content .heading .icon {