MDL-68529 course: Refactor chooser to include loading
authorMathew May <mathewm@hotmail.co.nz>
Wed, 29 Apr 2020 09:14:38 +0000 (17:14 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Tue, 12 May 2020 08:59:54 +0000 (16:59 +0800)
12 files changed:
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/src/modal.js
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css

index d9441d4..2d78a9d 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 3348e19..33594e0 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index 92a8164..af6b17a 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 6a85405..b0b8374 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index d3b83d0..a66560e 100644 (file)
@@ -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);
+
+                // 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);
-                const sectionModal = await modalBuilder(builtModuleData);
 
-                ChooserDialogue.displayChooser(caller, sectionModal, builtModuleData, favouriteFunction);
+                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);
index 6a476cf..42a19b3 100644 (file)
@@ -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;
-
-        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);
-    });
+        // We want to focus on the action select when the dialog is closed.
+        modal.getRoot().on(ModalEvents.hidden, () => {
+            modal.destroy();
+        });
 
-    modal.show();
+        return modal;
+    })
+    .catch();
 };
index 642c2ed..972b779 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index cb5d00f..c8ac6e4 100644 (file)
Binary files a/lib/amd/build/modal.min.js.map and b/lib/amd/build/modal.min.js.map differ
index 154f974..d04924f 100644 (file)
@@ -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.
index b6f91f1..9ad3d5b 100644 (file)
@@ -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;
index 74d9792..76689d4 100644 (file)
@@ -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 {
index 42a7c53..da0106c 100644 (file)
@@ -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 {