MDL-67264 core_course: Activity chooser new feature
authorMathew May <mathewm@hotmail.co.nz>
Mon, 3 Feb 2020 01:35:11 +0000 (09:35 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Wed, 12 Feb 2020 04:35:03 +0000 (12:35 +0800)
Roll in the base for the new activity chooser
It renders all modules into a modal
Gives the user to add from either the base or help screens
All checked by accessability tools with great coverage
Adds minimal overhead to the course edit setup time

Co-authored-by: Mathew May <mathewm@hotmail.co.nz>
Co-authored-by: Mihail Geshoski <mihail@moodle.com>
32 files changed:
course/amd/build/activitychooser.min.js [new file with mode: 0644]
course/amd/build/activitychooser.min.js.map [new file with mode: 0644]
course/amd/build/local/activitychooser/dialogue.min.js [new file with mode: 0644]
course/amd/build/local/activitychooser/dialogue.min.js.map [new file with mode: 0644]
course/amd/build/local/activitychooser/repository.min.js [new file with mode: 0644]
course/amd/build/local/activitychooser/repository.min.js.map [new file with mode: 0644]
course/amd/build/local/activitychooser/selectors.min.js [new file with mode: 0644]
course/amd/build/local/activitychooser/selectors.min.js.map [new file with mode: 0644]
course/amd/src/activitychooser.js [new file with mode: 0644]
course/amd/src/local/activitychooser/dialogue.js [new file with mode: 0644]
course/amd/src/local/activitychooser/repository.js [new file with mode: 0644]
course/amd/src/local/activitychooser/selectors.js [new file with mode: 0644]
course/externallib.php
course/renderer.php
course/templates/chooser.mustache [new file with mode: 0644]
course/templates/chooser_help.mustache [new file with mode: 0644]
course/templates/chooser_item.mustache [new file with mode: 0644]
course/templates/modchooser.mustache [deleted file]
course/tests/externallib_test.php
course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js [deleted file]
course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js [deleted file]
course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js [deleted file]
course/yui/src/modchooser/build.json [deleted file]
course/yui/src/modchooser/js/modchooser.js [deleted file]
course/yui/src/modchooser/meta/modchooser.json [deleted file]
lang/en/moodle.php
lib/db/services.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
version.php

diff --git a/course/amd/build/activitychooser.min.js b/course/amd/build/activitychooser.min.js
new file mode 100644 (file)
index 0000000..39ab5cf
Binary files /dev/null and b/course/amd/build/activitychooser.min.js differ
diff --git a/course/amd/build/activitychooser.min.js.map b/course/amd/build/activitychooser.min.js.map
new file mode 100644 (file)
index 0000000..77cf4e6
Binary files /dev/null and b/course/amd/build/activitychooser.min.js.map differ
diff --git a/course/amd/build/local/activitychooser/dialogue.min.js b/course/amd/build/local/activitychooser/dialogue.min.js
new file mode 100644 (file)
index 0000000..e1d3ec5
Binary files /dev/null and b/course/amd/build/local/activitychooser/dialogue.min.js differ
diff --git a/course/amd/build/local/activitychooser/dialogue.min.js.map b/course/amd/build/local/activitychooser/dialogue.min.js.map
new file mode 100644 (file)
index 0000000..d2c6bcd
Binary files /dev/null and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
diff --git a/course/amd/build/local/activitychooser/repository.min.js b/course/amd/build/local/activitychooser/repository.min.js
new file mode 100644 (file)
index 0000000..da5b3c7
Binary files /dev/null and b/course/amd/build/local/activitychooser/repository.min.js differ
diff --git a/course/amd/build/local/activitychooser/repository.min.js.map b/course/amd/build/local/activitychooser/repository.min.js.map
new file mode 100644 (file)
index 0000000..67b1590
Binary files /dev/null and b/course/amd/build/local/activitychooser/repository.min.js.map differ
diff --git a/course/amd/build/local/activitychooser/selectors.min.js b/course/amd/build/local/activitychooser/selectors.min.js
new file mode 100644 (file)
index 0000000..d709bb4
Binary files /dev/null and b/course/amd/build/local/activitychooser/selectors.min.js differ
diff --git a/course/amd/build/local/activitychooser/selectors.min.js.map b/course/amd/build/local/activitychooser/selectors.min.js.map
new file mode 100644 (file)
index 0000000..98e3367
Binary files /dev/null and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
diff --git a/course/amd/src/activitychooser.js b/course/amd/src/activitychooser.js
new file mode 100644 (file)
index 0000000..6f6613b
--- /dev/null
@@ -0,0 +1,148 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A type of dialogue used as for choosing modules in a course.
+ *
+ * @module     core_course/activitychooser
+ * @package    core_course
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';
+import * as Repository from 'core_course/local/activitychooser/repository';
+import selectors from 'core_course/local/activitychooser/selectors';
+import CustomEvents from 'core/custom_interaction_events';
+import * as Templates from 'core/templates';
+import * as ModalFactory from 'core/modal_factory';
+import {get_string as getString} from 'core/str';
+import Pending from 'core/pending';
+
+/**
+ * Set up the activity chooser.
+ *
+ * @method init
+ * @param {Number} courseId Course ID to use later on in fetchModules()
+ */
+export const init = courseId => {
+    const pendingPromise = new Pending();
+
+    registerListenerEvents(courseId);
+
+    pendingPromise.resolve();
+};
+
+/**
+ * Once a selection has been made make the modal & module information and pass it along
+ *
+ * @method registerListenerEvents
+ * @param {Number} courseId
+ */
+const registerListenerEvents = (courseId) => {
+    const events = [
+        'click',
+        CustomEvents.events.activate,
+        CustomEvents.events.keyboardActivate
+    ];
+
+    const fetchModuleData = (() => {
+        let innerPromise = null;
+
+        return () => {
+            if (!innerPromise) {
+                innerPromise = new Promise((resolve) => {
+                    resolve(Repository.activityModules(courseId));
+                });
+            }
+
+            return innerPromise;
+        };
+    })();
+
+    CustomEvents.define(document, events);
+
+    // Display module chooser event listeners.
+    events.forEach((event) => {
+        document.addEventListener(event, async(e) => {
+            if (e.target.closest(selectors.elements.sectionmodchooser)) {
+                const caller = e.target.closest(selectors.elements.sectionmodchooser);
+                const builtModuleData = sectionIdMapper(await fetchModuleData(), caller.dataset.sectionid);
+                const sectionModal = await modalBuilder(builtModuleData);
+
+                ChooserDialogue.displayChooser(caller, sectionModal, builtModuleData);
+            }
+        });
+    });
+};
+
+/**
+ * Given the web service data and an ID we want to make a deep copy
+ * of the WS data then add on the section ID to the addoption URL
+ *
+ * @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
+ * @return {Array} [modules] with URL's built
+ */
+const sectionIdMapper = (webServiceData, id) => {
+    // We need to take a fresh deep copy of the original data as an object is a reference type.
+    const newData = JSON.parse(JSON.stringify(webServiceData));
+    newData.allmodules.forEach((module) => {
+        module.urls.addoption += '&section=' + id;
+    });
+    return newData.allmodules;
+};
+
+/**
+ * Build a modal for each section ID and store it into a map for quick access
+ *
+ * @method modalBuilder
+ * @param {Map} data our map of section ID's & modules to generate modals for
+ * @return {Object} TODO
+ */
+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
+ *
+ * @method templateDataBuilder
+ * @param {Array} data our modules to manipulate into a Templatable object
+ * @return {Object} Our built object ready to render out
+ */
+const templateDataBuilder = (data) => {
+    return {
+        'default': data,
+    };
+};
+
+/**
+ * Given an object we want to prebuild a modal ready to store into a map
+ *
+ * @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
+ */
+const buildModal = data => {
+    return ModalFactory.create({
+        type: ModalFactory.types.DEFAULT,
+        title: getString('addresourceoractivity'),
+        body: Templates.render('core_course/chooser', data),
+        large: true,
+        templateContext: {
+            classes: 'modchooser'
+        }
+    });
+};
diff --git a/course/amd/src/local/activitychooser/dialogue.js b/course/amd/src/local/activitychooser/dialogue.js
new file mode 100644 (file)
index 0000000..4be4341
--- /dev/null
@@ -0,0 +1,287 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A type of dialogue used as for choosing options.
+ *
+ * @module     core_course/local/chooser/dialogue
+ * @package    core
+ * @copyright  2019 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+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 {addIconToContainer} from 'core/loadingicon';
+
+/**
+ * Given an event from the main module 'page' navigate to it's help section via a carousel.
+ *
+ * @method showModuleHelp
+ * @param {jQuery} carousel Our initialized carousel to manipulate
+ * @param {Object} moduleData Data of the module to carousel to
+ */
+const showModuleHelp = (carousel, moduleData) => {
+    const help = carousel.find(selectors.regions.help)[0];
+    help.innerHTML = '';
+
+    // Add a spinner.
+    const spinnerPromise = addIconToContainer(help);
+
+    // Used later...
+    let transitionPromiseResolver = null;
+    const transitionPromise = new Promise(resolve => {
+        transitionPromiseResolver = resolve;
+    });
+
+    // Build up the html & js ready to place into the help section.
+    const contentPromise = Templates.renderForPromise('core_course/chooser_help', moduleData);
+
+    // Wait for the content to be ready, and for the transition to be complet.
+    Promise.all([contentPromise, spinnerPromise, transitionPromise])
+        .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))
+        .then(() => {
+            help.querySelector(selectors.regions.chooserSummary.description).focus();
+            return help;
+        })
+        .catch(Notification.exception);
+
+    // Move to the next slide, and resolve the transition promise when it's done.
+    carousel.one('slid.bs.carousel', () => {
+        transitionPromiseResolver();
+    });
+    // Trigger the transition between 'pages'.
+    carousel.carousel('next');
+};
+
+/**
+ * Register chooser related event listeners.
+ *
+ * @method registerListenerEvents
+ * @param {Promise} modal Our modal that we are working with
+ * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
+ */
+const registerListenerEvents = (modal, mappedModules) => {
+    const bodyClickListener = e => {
+        if (e.target.closest(selectors.actions.optionActions.showSummary)) {
+            const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
+
+            const module = e.target.closest(selectors.regions.chooserOption.container);
+            const moduleName = module.dataset.modname;
+            const moduleData = mappedModules.get(moduleName);
+            showModuleHelp(carousel, moduleData);
+        }
+
+        // From the help screen go back to the module overview.
+        if (e.target.matches(selectors.actions.closeOption)) {
+            const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
+
+            // Trigger the transition between 'pages'.
+            carousel.carousel('prev');
+            carousel.on('slid.bs.carousel', () => {
+                const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
+                const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));
+                caller.focus();
+            });
+        }
+    };
+
+    modal.getBodyPromise()
+
+    // The return value of getBodyPromise is a jquery object containing the body NodeElement.
+    .then(body => body[0])
+
+    // Set up the carousel.
+    .then(body => {
+        $(body.querySelector(selectors.regions.carousel))
+            .carousel({
+                interval: false,
+                pause: true,
+                keyboard: false
+            });
+
+        return body;
+    })
+
+    // Add the listener for clicks on the body.
+    .then(body => {
+        body.addEventListener('click', bodyClickListener);
+        return body;
+    })
+
+    // Register event listeners related to the keyboard navigation controls.
+    .then(body => {
+        initKeyboardNavigation(body, mappedModules);
+        return body;
+    })
+    .catch();
+
+};
+
+/**
+ * Initialise the keyboard navigation controls for the chooser.
+ *
+ * @method initKeyboardNavigation
+ * @param {NodeElement} body Our modal that we are working with
+ * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
+ */
+const initKeyboardNavigation = (body, mappedModules) => {
+
+    const chooserOptions = body.querySelectorAll(selectors.regions.chooserOption.container);
+
+    Array.from(chooserOptions).forEach((element) => {
+        return element.addEventListener('keyup', (e) => {
+            const chooserOptions = document.querySelector(selectors.regions.chooserOptions);
+
+            // Check for enter/ space triggers for showing the help.
+            if (e.keyCode === enter || e.keyCode === space) {
+                if (e.target.matches(selectors.actions.optionActions.showSummary)) {
+                    e.preventDefault();
+                    const module = e.target.closest(selectors.regions.chooserOption.container);
+                    const moduleName = module.dataset.modname;
+                    const moduleData = mappedModules.get(moduleName);
+                    const carousel = $(body.querySelector(selectors.regions.carousel));
+                    carousel.carousel({
+                        interval: false,
+                        pause: true,
+                        keyboard: false
+                    });
+                    showModuleHelp(carousel, moduleData);
+                }
+            }
+
+            // Next.
+            if (e.keyCode === arrowRight) {
+                e.preventDefault();
+                const currentOption = e.target.closest(selectors.regions.chooserOption.container);
+                const nextOption = currentOption.nextElementSibling;
+                const firstOption = chooserOptions.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 = chooserOptions.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 = chooserOptions.firstElementChild;
+                focusChooserOption(firstOption, currentOption);
+            }
+
+            if (e.keyCode === end) {
+                e.preventDefault();
+                const currentOption = e.target.closest(selectors.regions.chooserOption.container);
+                const lastOption = chooserOptions.lastElementChild;
+                focusChooserOption(lastOption, currentOption);
+            }
+        });
+    });
+};
+
+/**
+ * Focus on a chooser option element and remove the previous chooser element from the focus order
+ *
+ * @method focusChooserOption
+ * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus
+ * @param {HTMLElement} previousChooserOption The previous focused option element
+ */
+const focusChooserOption = (currentChooserOption, previousChooserOption = false) => {
+    if (previousChooserOption !== false) {
+        const previousChooserOptionLink = previousChooserOption.querySelector(selectors.actions.addChooser);
+        const previousChooserOptionHelp = previousChooserOption.querySelector(selectors.actions.optionActions.showSummary);
+        // Set tabindex to -1 to remove the previous chooser option element from the focus order.
+        previousChooserOption.tabIndex = -1;
+        previousChooserOptionLink.tabIndex = -1;
+        previousChooserOptionHelp.tabIndex = -1;
+    }
+
+    const currentChooserOptionLink = currentChooserOption.querySelector(selectors.actions.addChooser);
+    const currentChooserOptionHelp = currentChooserOption.querySelector(selectors.actions.optionActions.showSummary);
+    // Set tabindex to 0 to add current chooser option element to the focus order.
+    currentChooserOption.tabIndex = 0;
+    currentChooserOptionLink.tabIndex = 0;
+    currentChooserOptionHelp.tabIndex = 0;
+    // Focus the current chooser option element.
+    currentChooserOption.focus();
+};
+
+/**
+ * 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 {String}
+ */
+const clickErrorHandler = (item, fallback) => {
+    if (item !== null) {
+        return item;
+    } else {
+        return fallback;
+    }
+};
+
+/**
+ * Display the module chooser.
+ *
+ * @method displayChooser
+ * @param {HTMLElement} origin The calling button
+ * @param {Object} modal Our created modal for the section
+ * @param {Array} sectionModules An array of all of the built module information
+ */
+export const displayChooser = (origin, modal, sectionModules) => {
+
+    // 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) => {
+        mappedModules.set(module.modulename, module);
+    });
+
+    // Register event listeners.
+    registerListenerEvents(modal, mappedModules);
+
+    // 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.
+    modal.getRoot().on(ModalEvents.shown, () => {
+        modal.getModal()[0].tabIndex = -1;
+
+        modal.getBodyPromise()
+        .then(body => {
+            const firstChooserOption = body[0].querySelector(selectors.regions.chooserOption.container);
+            focusChooserOption(firstChooserOption);
+
+            return;
+        })
+        .catch(Notification.exception);
+    });
+
+    modal.show();
+};
diff --git a/course/amd/src/local/activitychooser/repository.js b/course/amd/src/local/activitychooser/repository.js
new file mode 100644 (file)
index 0000000..1e6f1a5
--- /dev/null
@@ -0,0 +1,40 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ *
+ * @module     core_course/repository
+ * @package    core_course
+ * @copyright  2019 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import ajax from 'core/ajax';
+
+/**
+ * Fetch all the information on modules we'll need in the activity chooser.
+ *
+ * @method activityModules
+ * @param {Number} courseid What course to fetch the modules for
+ * @return {object} jQuery promise
+ */
+export const activityModules = (courseid) => {
+    const request = {
+        methodname: 'core_course_get_activity_picker_info',
+        args: {
+            courseid: courseid,
+        },
+    };
+    return ajax.call([request])[0];
+};
diff --git a/course/amd/src/local/activitychooser/selectors.js b/course/amd/src/local/activitychooser/selectors.js
new file mode 100644 (file)
index 0000000..adeb07f
--- /dev/null
@@ -0,0 +1,70 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Define all of the selectors we will be using on the grading interface.
+ *
+ * @module     core_course/local/chooser/selectors
+ * @package    core_course
+ * @copyright  2019 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * A small helper function to build queryable data selectors.
+ * @method getDataSelector
+ * @param {String} name
+ * @param {String} value
+ * @return {string}
+ */
+const getDataSelector = (name, value) => {
+    return `[data-${name}="${value}"]`;
+};
+
+export default {
+    regions: {
+        chooser: getDataSelector('region', 'chooser-container'),
+        chooserOptions: getDataSelector('region', 'chooser-options-container'),
+        chooserOption: {
+            container: getDataSelector('region', 'chooser-option-container'),
+            actions: getDataSelector('region', 'chooser-option-actions-container'),
+            info: getDataSelector('region', 'chooser-option-info-container'),
+        },
+        chooserSummary: {
+            container: getDataSelector('region', 'chooser-option-summary-container'),
+            content: getDataSelector('region', 'chooser-option-summary-content-container'),
+            description: getDataSelector('region', 'summary-description'),
+            actions: getDataSelector('region', 'chooser-option-summary-actions-container'),
+        },
+        carousel: getDataSelector('region', 'carousel'),
+        help: getDataSelector('region', 'help'),
+        modules: getDataSelector('region', 'modules'),
+        getModuleSelector: modname => `[role="menuitem"][data-modname="${modname}"]`
+    },
+    actions: {
+        optionActions: {
+            showSummary: getDataSelector('action', 'show-option-summary'),
+        },
+        addChooser: getDataSelector('action', 'add-chooser-option'),
+        closeOption: getDataSelector('action', 'close-chooser-option-summary'),
+        hide: getDataSelector('action', 'hide')
+    },
+    elements: {
+        section: '.section',
+        sectionmodchooser: 'button.section-modchooser-link',
+        sitemenu: '.block_site_main_menu',
+        sitetopic: 'div.sitetopic',
+    },
+};
index b39acfe..c92ad97 100644 (file)
@@ -4140,4 +4140,86 @@ class core_course_external extends external_api {
         );
         return new external_single_structure($userfields);
     }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     */
+    public static function fetch_modules_activity_chooser_returns() {
+        return new external_single_structure([
+            'allmodules' => new external_multiple_structure(
+                new external_single_structure([
+                    'label' => new external_value(PARAM_TEXT, 'Human readable module name', VALUE_OPTIONAL),
+                    'modulename' => new external_value(PARAM_TEXT, 'Module name', VALUE_OPTIONAL),
+                    'description' => new external_value(PARAM_RAW, 'Help panel information', VALUE_OPTIONAL),
+                    'urls' => new external_single_structure([
+                        'addoption' => new external_value(PARAM_URL, 'The edit link for the module', VALUE_OPTIONAL),
+                    ]),
+                    'icon' => new external_single_structure([
+                        'attributes' => new external_multiple_structure(
+                            new external_single_structure([
+                                'name' => new external_value(PARAM_RAW, 'HTML attr', VALUE_OPTIONAL),
+                                'value' => new external_value(PARAM_RAW, 'Value of the HTML attr', VALUE_OPTIONAL),
+                            ])
+                        ),
+                        'extraclasses' => new external_value(PARAM_RAW, 'Anything extra the module defines', VALUE_OPTIONAL),
+                    ]),
+                ])
+            ),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function fetch_modules_activity_chooser_parameters() {
+        return new external_function_parameters([
+            'courseid' => new external_value(PARAM_INT, 'ID of the course', VALUE_REQUIRED),
+        ]);
+    }
+
+    /**
+     * Given a course ID fetch all accessible modules for that course
+     *
+     * @param int $courseid The course we want to fetch the modules for
+     * @return array Contains array of modules and their metadata
+     * @throws moodle_exception
+     */
+    public static function fetch_modules_activity_chooser(int $courseid) {
+        global $DB, $OUTPUT;
+        [
+            'courseid' => $courseid,
+        ] = self::validate_parameters(self::fetch_modules_activity_chooser_parameters(), [
+            'courseid' => $courseid,
+        ]);
+        $warnings = array();
+
+        // Validate the course context.
+        $coursecontext = context_course::instance($courseid);
+        self::validate_context($coursecontext);
+        // Check to see if user can add menus and there are modules to add.
+        if (!has_capability('moodle/course:manageactivities', $coursecontext)
+            || !($modnames = get_module_types_names()) || empty($modnames)) {
+            return '';
+        }
+
+        $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+        // Retrieve all modules with associated metadata.
+        $modules = get_module_metadata($course, $modnames, null);
+        $related = [
+            'context' => $coursecontext
+        ];
+        // Export the module chooser data.
+        $modchooserdata = new \core_course\external\course_module_chooser_exporter($modules, $related);
+
+        $result = [];
+        $result['allmodules'] = $modchooserdata->export($OUTPUT)->options;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
 }
index b02df93..83a329a 100644 (file)
@@ -142,11 +142,8 @@ class core_course_renderer extends plugin_renderer_base {
      */
     public function course_modchooser($modules, $course) {
         debugging('course_modchooser() is deprecated. Please use course_activitychooser() instead.', DEBUG_DEVELOPER);
-        if (!$this->page->requires->should_create_one_time_item_now('core_course_modchooser')) {
-            return '';
-        }
-        $modchooser = new \core_course\output\modchooser($course, $modules);
-        return $this->render($modchooser);
+
+        return $this->course_activitychooser($course->id);
     }
 
     /**
@@ -161,7 +158,7 @@ class core_course_renderer extends plugin_renderer_base {
             return '';
         }
 
-        $this->page->requires->js_call_amd('core_course/modchooser', 'init', [$courseid]);
+        $this->page->requires->js_call_amd('core_course/activitychooser', 'init', [$courseid]);
 
         return '';
     }
@@ -345,7 +342,6 @@ class core_course_renderer extends plugin_renderer_base {
                     'class' => 'section-modchooser-link btn btn-link',
                     'data-action' => 'open-chooser',
                     'data-sectionid' => $section,
-                    'disabled' => true
                 )
             );
             $modchooser.= html_writer::end_tag('div');
diff --git a/course/templates/chooser.mustache b/course/templates/chooser.mustache
new file mode 100644 (file)
index 0000000..c503c9b
--- /dev/null
@@ -0,0 +1,48 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_course/chooser
+
+    Chooser dialog template.
+
+    Example context (json):
+    {
+        "title": "Chooser title",
+        "options": {
+            "label": "Option name",
+            "description": "Option description",
+            "urls": {
+                "addoption": "http://addoptionurl.com"
+            },
+            "icon": "<img class='icon' src='http://urltooptionicon'>"
+        }
+    }
+}}
+<div data-region="carousel" class="carousel slide">
+    <div class="carousel-inner" aria-live="polite">
+        <div class="carousel-item active" data-region="modules">
+            <div class="modchoosercontainer" data-region="chooser-container" aria-label="{{#str}} activitymodules {{/str}}">
+                <div class="optionscontainer d-flex flex-wrap mw-100 p-3 position-relative" role="menubar" data-region="chooser-options-container">
+                    {{#default}}
+                        {{>core_course/chooser_item}}
+                    {{/default}}
+                </div>
+            </div>
+        </div>
+        <div class="carousel-item" data-region="help"></div>
+    </div>
+</div>
diff --git a/course/templates/chooser_help.mustache b/course/templates/chooser_help.mustache
new file mode 100644 (file)
index 0000000..b63502b
--- /dev/null
@@ -0,0 +1,54 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_course/chooser_help
+
+    Chooser help / more information template.
+
+    Example context (json):
+    {
+        "label": "Option name",
+        "description": "Option description",
+        "urls": {
+            "addoption": "http://addoptionurl.com"
+        },
+        "icon": "<img class='icon' src='http://urltooptionicon'>"
+    }
+}}
+<div class="optionsummary" tabindex="-1" data-region="chooser-option-summary-container" aria-labelledby="optionsummary_label" aria-describedby="optionsumary_desc">
+    <div class="content text-left mb-5 px-5 py-4" data-region="chooser-option-summary-content-container">
+        <div class="heading mb-4">
+            <h5 id="optionsummary_label">
+                {{#icon}}
+                    {{>core/pix_icon}}
+                {{/icon}}
+                {{label}}
+            </h5>
+        </div>
+        <div id="optionsumary_desc" class="description" data-region="summary-description" tabindex="0">
+            {{{description}}}
+        </div>
+    </div>
+    <div class="actions fixed-bottom w-100 d-flex justify-content-between position-absolute py-3 px-4" data-region="chooser-option-summary-actions-container">
+        <button data-action="close-chooser-option-summary" class="closeoptionsummary btn btn-secondary" tabindex="0" data-modname="{{modulename}}">
+            {{#str}} back {{/str}}
+        </button>
+        <a href="{{urls.addoption}}" aria-label="{{#str}} addnew, moodle, {{label}} {{/str}}" data-action="add-chooser-option" class="addoption btn btn-primary" tabindex="0">
+            {{#str}} add {{/str}}
+        </a>
+    </div>
+</div>
diff --git a/course/templates/chooser_item.mustache b/course/templates/chooser_item.mustache
new file mode 100644 (file)
index 0000000..f850756
--- /dev/null
@@ -0,0 +1,49 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_course/chooser_item
+
+    Chooser item template.
+
+    Example context (json):
+    {
+        "label": "Option name",
+        "description": "Option description",
+        "urls": {
+            "addoption": "http://addoptionurl.com"
+        },
+        "icon": "<img class='icon' src='http://urltooptionicon'>"
+    }
+}}
+<div role="menuitem" tabindex="-1" aria-label="{{label}}" class="option d-block text-center py-3 px-2" data-region="chooser-option-container" data-modname="{{modulename}}">
+    <div class="optioninfo w-100" data-region="chooser-option-info-container">
+        <a class="d-block" href="{{urls.addoption}}" aria-label="{{#str}} addnew, moodle, {{label}} {{/str}}" tabindex="-1" data-action="add-chooser-option">
+            <span class="optionicon d-block">
+                {{#icon}}
+                    {{>core/pix_icon}}
+                {{/icon}}
+            </span>
+            <span class="optionname d-block">{{label}}</span>
+        </a>
+        <div class="optionactions btn-group" role="group" aria-label="{{#str}} actionsfor, moodle, {{label}}{{/str}}" data-region="chooser-option-actions-container">
+            <button class="btn btn-icon icon-no-margin icon-size-3 m-0 optionaction" data-action="show-option-summary" tabindex="-1" aria-label="{{#str}} moreinfo {{/str}}">
+                <span aria-hidden="true">{{#pix}} docs, core {{/pix}}</span>
+                <span class="sr-only">{{#str}} moreinfo {{/str}}</span>
+            </button>
+        </div>
+    </div>
+</div>
diff --git a/course/templates/modchooser.mustache b/course/templates/modchooser.mustache
deleted file mode 100644 (file)
index 88cf99c..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-{{!
-    This file is part of Moodle - http://moodle.org/
-
-    Moodle is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    Moodle is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-}}
-{{!
-    Course module chooser.
-}}
-{{> core/chooser }}
-{{#js}}
-require([
-    'core/yui',
-    'core/str'
-], function(Y, Str) {
-    Str.get_strings([
-        { key: 'addresourceoractivity', component: 'moodle' },
-        { key: 'close', component: 'editor' },
-    ]).then(function(add, close) {
-        Y.use('moodle-course-modchooser', function() {
-            M.course.init_chooser({
-                courseid: {{courseid}},
-                closeButtonTitle: close
-            });
-        });
-    });
-});
-{{/js}}
index 67611e5..b2f1671 100644 (file)
@@ -3049,4 +3049,39 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(2, count($users['users']));
         $this->assertEquals($expectedusers, $users);
     }
+
+    /**
+     * Test fetch_modules_activity_chooser
+     */
+    public function test_fetch_modules_activity_chooser() {
+        global $OUTPUT;
+
+        $this->resetAfterTest(true);
+
+        // Log in as Admin.
+        $this->setAdminUser();
+
+        $course1  = self::getDataGenerator()->create_course();
+
+        // Fetch course modules.
+        $result = core_course_external::fetch_modules_activity_chooser($course1->id);
+        $result = external_api::clean_returnvalue(core_course_external::fetch_modules_activity_chooser_returns(), $result);
+        // Check for 0 warnings.
+        $this->assertEquals(0, count($result['warnings']));
+        // Check we have the right number of standard modules.
+        $this->assertEquals(21, count($result['allmodules']));
+
+        $coursecontext = context_course::instance($course1->id);
+        $modnames = get_module_types_names();
+        $modules = get_module_metadata($course1, $modnames, null);
+        $related = [
+            'context' => $coursecontext
+        ];
+        // Export the module chooser data.
+        $modchooserdata = new \core_course\external\course_module_chooser_exporter($modules, $related);
+        $formatteddata = $modchooserdata->export($OUTPUT)->options;
+
+        // Check if the webservice returns exactly what the exporter defines.
+        $this->assertEquals($formatteddata, $result['allmodules']);
+    }
 }
diff --git a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js b/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js
deleted file mode 100644 (file)
index ad86243..0000000
Binary files a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js and /dev/null differ
diff --git a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js b/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js
deleted file mode 100644 (file)
index 785cf35..0000000
Binary files a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js and /dev/null differ
diff --git a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js b/course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js
deleted file mode 100644 (file)
index ad86243..0000000
Binary files a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js and /dev/null differ
diff --git a/course/yui/src/modchooser/build.json b/course/yui/src/modchooser/build.json
deleted file mode 100644 (file)
index 569ab7b..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "name": "moodle-course-modchooser",
-  "builds": {
-    "moodle-course-modchooser": {
-      "jsfiles": [
-        "modchooser.js"
-      ]
-    }
-  }
-}
diff --git a/course/yui/src/modchooser/js/modchooser.js b/course/yui/src/modchooser/js/modchooser.js
deleted file mode 100644 (file)
index 989adf7..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * The activity chooser dialogue for courses.
- *
- * @module moodle-course-modchooser
- */
-
-var CSS = {
-    PAGECONTENT: 'body',
-    SECTION: null,
-    SECTIONMODCHOOSER: 'button.section-modchooser-link',
-    SITEMENU: '.block_site_main_menu',
-    SITETOPIC: 'div.sitetopic'
-};
-
-var MODCHOOSERNAME = 'course-modchooser';
-
-/**
- * The activity chooser dialogue for courses.
- *
- * @constructor
- * @class M.course.modchooser
- * @extends M.core.chooserdialogue
- */
-var MODCHOOSER = function() {
-    MODCHOOSER.superclass.constructor.apply(this, arguments);
-};
-
-Y.extend(MODCHOOSER, M.core.chooserdialogue, {
-    /**
-     * The current section ID.
-     *
-     * @property sectionid
-     * @private
-     * @type Number
-     * @default null
-     */
-    sectionid: null,
-
-    /**
-     * Set up the activity chooser.
-     *
-     * @method initializer
-     */
-    initializer: function() {
-        var sectionclass = M.course.format.get_sectionwrapperclass();
-        if (sectionclass) {
-            CSS.SECTION = '.' + sectionclass;
-        }
-        var dialogue = Y.one('.chooserdialoguebody');
-        var header = Y.one('.choosertitle');
-        var params = {};
-        this.setup_chooser_dialogue(dialogue, header, params);
-
-        // Initialize existing sections and register for dynamically created sections
-        this.setup_for_section();
-        M.course.coursebase.register_module(this);
-    },
-
-    /**
-     * Update any section areas within the scope of the specified
-     * selector with AJAX equivalents
-     *
-     * @method setup_for_section
-     * @param baseselector The selector to limit scope to
-     */
-    setup_for_section: function(baseselector) {
-        if (!baseselector) {
-            baseselector = CSS.PAGECONTENT;
-        }
-
-        // Setup for site topics
-        Y.one(baseselector).all(CSS.SITETOPIC).each(function(section) {
-            this._setup_for_section(section);
-        }, this);
-
-        // Setup for standard course topics
-        if (CSS.SECTION) {
-            Y.one(baseselector).all(CSS.SECTION).each(function(section) {
-                this._setup_for_section(section);
-            }, this);
-        }
-
-        // Setup for the block site menu
-        Y.one(baseselector).all(CSS.SITEMENU).each(function(section) {
-            this._setup_for_section(section);
-        }, this);
-    },
-
-    /**
-     * Update any section areas within the scope of the specified
-     * selector with AJAX equivalents
-     *
-     * @method _setup_for_section
-     * @private
-     * @param baseselector The selector to limit scope to
-     */
-    _setup_for_section: function(section) {
-        var chooserspan = section.one(CSS.SECTIONMODCHOOSER);
-        if (!chooserspan) {
-            return;
-        }
-        var chooserlink = Y.Node.create("<a href='#' />");
-        chooserspan.get('children').each(function(node) {
-            chooserlink.appendChild(node);
-        });
-        chooserspan.insertBefore(chooserlink);
-        chooserlink.on('click', this.display_mod_chooser, this);
-    },
-    /**
-     * Display the module chooser
-     *
-     * @method display_mod_chooser
-     * @param {EventFacade} e Triggering Event
-     */
-    display_mod_chooser: function(e) {
-        // Set the section for this version of the dialogue
-        if (e.target.ancestor(CSS.SITETOPIC)) {
-            // The site topic has a sectionid of 1
-            this.sectionid = 1;
-        } else if (e.target.ancestor(CSS.SECTION)) {
-            var section = e.target.ancestor(CSS.SECTION);
-            this.sectionid = section.get('id').replace('section-', '');
-        } else if (e.target.ancestor(CSS.SITEMENU)) {
-            // The block site menu has a sectionid of 0
-            this.sectionid = 0;
-        }
-        this.display_chooser(e);
-    },
-
-    /**
-     * Helper function to set the value of a hidden radio button when a
-     * selection is made.
-     *
-     * @method option_selected
-     * @param {String} thisoption The selected option value
-     * @private
-     */
-    option_selected: function(thisoption) {
-        // Add the sectionid to the URL.
-        this.hiddenRadioValue.setAttrs({
-            name: 'jump',
-            value: thisoption.get('value') + '&section=' + this.sectionid
-        });
-    }
-},
-{
-    NAME: MODCHOOSERNAME,
-    ATTRS: {
-        /**
-         * The maximum height (in pixels) of the activity chooser.
-         *
-         * @attribute maxheight
-         * @type Number
-         * @default 800
-         */
-        maxheight: {
-            value: 800
-        }
-    }
-});
-M.course = M.course || {};
-M.course.init_chooser = function(config) {
-    return new MODCHOOSER(config);
-};
diff --git a/course/yui/src/modchooser/meta/modchooser.json b/course/yui/src/modchooser/meta/modchooser.json
deleted file mode 100644 (file)
index 2ef461b..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "moodle-course-modchooser": {
-    "requires": [
-        "moodle-core-chooserdialogue",
-        "moodle-course-coursebase"
-    ]
-  }
-}
index 6c17f58..5010450 100644 (file)
@@ -43,6 +43,7 @@ $string['activityselect'] = 'Select this activity to be moved elsewhere';
 $string['activitysince'] = 'Activity since {$a}';
 $string['activitytypetitle'] = '{$a->activity} - {$a->type}';
 $string['activityweighted'] = 'Activity per user';
+$string['actionsfor'] = 'Actions for {$a}';
 $string['add'] = 'Add';
 $string['addactivity'] = 'Add an activity...';
 $string['addactivitytosection'] = 'Add an activity to section \'{$a}\'';
@@ -58,6 +59,7 @@ $string['addedtogroupnot'] = 'Not added to group "{$a}"';
 $string['addedtogroupnotenrolled'] = 'Not added to group "{$a}", because not enrolled in course';
 $string['addfilehere'] = 'Add file(s) here';
 $string['addinganew'] = 'Adding a new {$a}';
+$string['addnew'] = 'Add a new {$a}';
 $string['addinganewto'] = 'Adding a new {$a->what} to {$a->to}';
 $string['addingdatatoexisting'] = 'Adding data to existing';
 $string['additionalnames'] = 'Additional names';
index de2a200..50422b2 100644 (file)
@@ -629,6 +629,14 @@ $functions = array(
         'type' => 'read',
         'ajax' => true,
     ),
+    'core_course_get_activity_picker_info' => array(
+        'classname' => 'core_course_external',
+        'methodname' => 'fetch_modules_activity_chooser',
+        'classpath' => 'course/externallib.php',
+        'description' => 'Fetch all the module information for the activity picker',
+        'type' => 'read',
+        'ajax' => true,
+    ),
     'core_enrol_get_course_enrolment_methods' => array(
         'classname' => 'core_enrol_external',
         'methodname' => 'get_course_enrolment_methods',
index 2c3ad22..8fd7052 100644 (file)
@@ -1489,11 +1489,6 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     padding-top: 1px;
 }
 
-.chooserdialogue-course-modchooser .modicon .icon {
-    width: 24px;
-    height: 24px;
-    font-size: 24px;
-}
 
 @include media-breakpoint-down(xs) {
     .jsenabled .choosercontainer #chooseform .alloptions {
@@ -1506,6 +1501,124 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     }
 }
 
+/**
+ * Module chooser dialogue (moodle-core-chooserdialogue)
+ *
+ * This CSS belong to the chooser dialogue which should work both with, and
+ * without javascript enabled
+ */
+.modchooser .modal-body {
+    padding: 0;
+    height: 590px;
+    overflow-y: auto;
+
+    .loading-icon {
+        opacity: 1;
+        .icon {
+            display: block;
+            font-size: 3em;
+            height: 1em;
+            width: 1em;
+            margin: 5em auto;
+        }
+    }
+}
+
+.modchoosercontainer.noscroll {
+    overflow-y: hidden;
+}
+
+.modchoosercontainer .optionscontainer {
+    overflow-x: hidden;
+    .option {
+        // Six items per line.
+        flex-basis: 16%;
+        .optionactions {
+            .optionaction {
+                cursor: pointer;
+                margin: 0.2rem;
+                color: $gray-600;
+                i {
+                    margin: 0;
+                }
+            }
+        }
+        .optioninfo {
+            a {
+                color: $gray-700;
+                &:hover {
+                    text-decoration: none;
+                }
+                .optionname {
+                    margin-top: 0.5em;
+                }
+                .optionicon {
+                    .icon {
+                        margin: 0;
+                        padding: 0;
+                        width: 32px;
+                        height: 32px;
+                        font-size: 32px;
+                    }
+                }
+            }
+        }
+    }
+}
+
+.modchooser .modal-body .optionsummary {
+    background-color: $white;
+    overflow-x: hidden;
+    overflow-y: auto;
+    line-height: 2em;
+    height: 590px;
+
+    .content {
+        overflow-y: auto;
+        .heading {
+            .icon {
+                height: 32px;
+                width: 32px;
+                font-size: 32px;
+                padding: 0;
+            }
+        }
+    }
+
+    .actions {
+        border-top: 1px solid $gray-300;
+        background: $white;
+    }
+}
+
+@include media-breakpoint-down(lg) {
+    .modchoosercontainer .optionscontainer .option {
+        // Five items per line.
+        flex-basis: 20%;
+    }
+}
+
+@include media-breakpoint-down(xs) {
+    .path-course-view .modal-dialog.modal-lg,
+    .path-course-view .modal-content,
+    .modchooser .modal-body,
+    .modchooser .modal-body .carousel,
+    .modchooser .modal-body .carousel-inner,
+    .modchooser .modal-body .carousel-item,
+    .modchooser .modal-body .optionsummary,
+    .modchoosercontainer,
+    .optionscontainer {
+        height: 100%;
+    }
+    .path-course-view .modal-dialog.modal-lg {
+        margin: 0;
+    }
+    .modchoosercontainer .optionscontainer .option {
+        // Four items per line.
+        flex-basis: 25%;
+    }
+}
+
 /* Form element: listing */
 .formlistingradio {
     padding-bottom: 25px;
index d068d11..57c0b19 100644 (file)
@@ -1,10 +1,5 @@
 /* course.less */
 /* COURSE CONTENT */
-.section-modchooser-link img {
-    margin-right: 0.5rem;
-    width: 16px;
-    height: 16px;
-}
 
 .section_add_menus {
     text-align: right;
index bc138fa..330360e 100644 (file)
@@ -10637,11 +10637,6 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   margin-top: -1px;
   padding-top: 1px; }
 
-.chooserdialogue-course-modchooser .modicon .icon {
-  width: 24px;
-  height: 24px;
-  font-size: 24px; }
-
 @media (max-width: 575.98px) {
   .jsenabled .choosercontainer #chooseform .alloptions {
     max-width: 100%; }
@@ -10649,6 +10644,88 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   .jsenabled .choosercontainer #chooseform .typesummary {
     position: static; } }
 
+/**
+ * Module chooser dialogue (moodle-core-chooserdialogue)
+ *
+ * This CSS belong to the chooser dialogue which should work both with, and
+ * without javascript enabled
+ */
+.modchooser .modal-body {
+  padding: 0;
+  height: 590px;
+  overflow-y: auto; }
+  .modchooser .modal-body .loading-icon {
+    opacity: 1; }
+    .modchooser .modal-body .loading-icon .icon {
+      display: block;
+      font-size: 3em;
+      height: 1em;
+      width: 1em;
+      margin: 5em auto; }
+
+.modchoosercontainer.noscroll {
+  overflow-y: hidden; }
+
+.modchoosercontainer .optionscontainer {
+  overflow-x: hidden; }
+  .modchoosercontainer .optionscontainer .option {
+    flex-basis: 16%; }
+    .modchoosercontainer .optionscontainer .option .optionactions .optionaction {
+      cursor: pointer;
+      margin: 0.2rem;
+      color: #868e96; }
+      .modchoosercontainer .optionscontainer .option .optionactions .optionaction i {
+        margin: 0; }
+    .modchoosercontainer .optionscontainer .option .optioninfo a {
+      color: #495057; }
+      .modchoosercontainer .optionscontainer .option .optioninfo a:hover {
+        text-decoration: none; }
+      .modchoosercontainer .optionscontainer .option .optioninfo a .optionname {
+        margin-top: 0.5em; }
+      .modchoosercontainer .optionscontainer .option .optioninfo a .optionicon .icon {
+        margin: 0;
+        padding: 0;
+        width: 32px;
+        height: 32px;
+        font-size: 32px; }
+
+.modchooser .modal-body .optionsummary {
+  background-color: #fff;
+  overflow-x: hidden;
+  overflow-y: auto;
+  line-height: 2em;
+  height: 590px; }
+  .modchooser .modal-body .optionsummary .content {
+    overflow-y: auto; }
+    .modchooser .modal-body .optionsummary .content .heading .icon {
+      height: 32px;
+      width: 32px;
+      font-size: 32px;
+      padding: 0; }
+  .modchooser .modal-body .optionsummary .actions {
+    border-top: 1px solid #dee2e6;
+    background: #fff; }
+
+@media (max-width: 1199.98px) {
+  .modchoosercontainer .optionscontainer .option {
+    flex-basis: 20%; } }
+
+@media (max-width: 575.98px) {
+  .path-course-view .modal-dialog.modal-lg,
+  .path-course-view .modal-content,
+  .modchooser .modal-body,
+  .modchooser .modal-body .carousel,
+  .modchooser .modal-body .carousel-inner,
+  .modchooser .modal-body .carousel-item,
+  .modchooser .modal-body .optionsummary,
+  .modchoosercontainer,
+  .optionscontainer {
+    height: 100%; }
+  .path-course-view .modal-dialog.modal-lg {
+    margin: 0; }
+  .modchoosercontainer .optionscontainer .option {
+    flex-basis: 25%; } }
+
 /* Form element: listing */
 .formlistingradio {
   padding-bottom: 25px;
@@ -12421,11 +12498,6 @@ table.calendartable caption {
 
 /* course.less */
 /* COURSE CONTENT */
-.section-modchooser-link img {
-  margin-right: 0.5rem;
-  width: 16px;
-  height: 16px; }
-
 .section_add_menus {
   text-align: right;
   clear: both; }
index fd31a4e..4802c14 100644 (file)
@@ -10844,11 +10844,6 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   margin-top: -1px;
   padding-top: 1px; }
 
-.chooserdialogue-course-modchooser .modicon .icon {
-  width: 24px;
-  height: 24px;
-  font-size: 24px; }
-
 @media (max-width: 575.98px) {
   .jsenabled .choosercontainer #chooseform .alloptions {
     max-width: 100%; }
@@ -10856,6 +10851,88 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   .jsenabled .choosercontainer #chooseform .typesummary {
     position: static; } }
 
+/**
+ * Module chooser dialogue (moodle-core-chooserdialogue)
+ *
+ * This CSS belong to the chooser dialogue which should work both with, and
+ * without javascript enabled
+ */
+.modchooser .modal-body {
+  padding: 0;
+  height: 590px;
+  overflow-y: auto; }
+  .modchooser .modal-body .loading-icon {
+    opacity: 1; }
+    .modchooser .modal-body .loading-icon .icon {
+      display: block;
+      font-size: 3em;
+      height: 1em;
+      width: 1em;
+      margin: 5em auto; }
+
+.modchoosercontainer.noscroll {
+  overflow-y: hidden; }
+
+.modchoosercontainer .optionscontainer {
+  overflow-x: hidden; }
+  .modchoosercontainer .optionscontainer .option {
+    flex-basis: 16%; }
+    .modchoosercontainer .optionscontainer .option .optionactions .optionaction {
+      cursor: pointer;
+      margin: 0.2rem;
+      color: #868e96; }
+      .modchoosercontainer .optionscontainer .option .optionactions .optionaction i {
+        margin: 0; }
+    .modchoosercontainer .optionscontainer .option .optioninfo a {
+      color: #495057; }
+      .modchoosercontainer .optionscontainer .option .optioninfo a:hover {
+        text-decoration: none; }
+      .modchoosercontainer .optionscontainer .option .optioninfo a .optionname {
+        margin-top: 0.5em; }
+      .modchoosercontainer .optionscontainer .option .optioninfo a .optionicon .icon {
+        margin: 0;
+        padding: 0;
+        width: 32px;
+        height: 32px;
+        font-size: 32px; }
+
+.modchooser .modal-body .optionsummary {
+  background-color: #fff;
+  overflow-x: hidden;
+  overflow-y: auto;
+  line-height: 2em;
+  height: 590px; }
+  .modchooser .modal-body .optionsummary .content {
+    overflow-y: auto; }
+    .modchooser .modal-body .optionsummary .content .heading .icon {
+      height: 32px;
+      width: 32px;
+      font-size: 32px;
+      padding: 0; }
+  .modchooser .modal-body .optionsummary .actions {
+    border-top: 1px solid #dee2e6;
+    background: #fff; }
+
+@media (max-width: 1199.98px) {
+  .modchoosercontainer .optionscontainer .option {
+    flex-basis: 20%; } }
+
+@media (max-width: 575.98px) {
+  .path-course-view .modal-dialog.modal-lg,
+  .path-course-view .modal-content,
+  .modchooser .modal-body,
+  .modchooser .modal-body .carousel,
+  .modchooser .modal-body .carousel-inner,
+  .modchooser .modal-body .carousel-item,
+  .modchooser .modal-body .optionsummary,
+  .modchoosercontainer,
+  .optionscontainer {
+    height: 100%; }
+  .path-course-view .modal-dialog.modal-lg {
+    margin: 0; }
+  .modchoosercontainer .optionscontainer .option {
+    flex-basis: 25%; } }
+
 /* Form element: listing */
 .formlistingradio {
   padding-bottom: 25px;
@@ -12633,11 +12710,6 @@ table.calendartable caption {
 
 /* course.less */
 /* COURSE CONTENT */
-.section-modchooser-link img {
-  margin-right: 0.5rem;
-  width: 16px;
-  height: 16px; }
-
 .section_add_menus {
   text-align: right;
   clear: both; }
index 8f04316..fa4b3f0 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020020700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020020700.02;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '3.9dev (Build: 20200207)'; // Human-friendly version name