--- /dev/null
+// 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 += '§ion=' + 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'
+ }
+ });
+};
--- /dev/null
+// 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();
+};
--- /dev/null
+// 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];
+};
--- /dev/null
+// 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',
+ },
+};
);
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;
+ }
+
}
*/
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);
}
/**
return '';
}
- $this->page->requires->js_call_amd('core_course/modchooser', 'init', [$courseid]);
+ $this->page->requires->js_call_amd('core_course/activitychooser', 'init', [$courseid]);
return '';
}
'class' => 'section-modchooser-link btn btn-link',
'data-action' => 'open-chooser',
'data-sectionid' => $section,
- 'disabled' => true
)
);
$modchooser.= html_writer::end_tag('div');
--- /dev/null
+{{!
+ 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>
--- /dev/null
+{{!
+ 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>
--- /dev/null
+{{!
+ 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>
+++ /dev/null
-{{!
- 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}}
$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']);
+ }
}
+++ /dev/null
-{
- "name": "moodle-course-modchooser",
- "builds": {
- "moodle-course-modchooser": {
- "jsfiles": [
- "modchooser.js"
- ]
- }
- }
-}
+++ /dev/null
-/**
- * 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') + '§ion=' + 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);
-};
+++ /dev/null
-{
- "moodle-course-modchooser": {
- "requires": [
- "moodle-core-chooserdialogue",
- "moodle-course-coursebase"
- ]
- }
-}
$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}\'';
$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';
'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',
padding-top: 1px;
}
-.chooserdialogue-course-modchooser .modicon .icon {
- width: 24px;
- height: 24px;
- font-size: 24px;
-}
@include media-breakpoint-down(xs) {
.jsenabled .choosercontainer #chooseform .alloptions {
}
}
+/**
+ * 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;
/* course.less */
/* COURSE CONTENT */
-.section-modchooser-link img {
- margin-right: 0.5rem;
- width: 16px;
- height: 16px;
-}
.section_add_menus {
text-align: right;
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%; }
.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;
/* course.less */
/* COURSE CONTENT */
-.section-modchooser-link img {
- margin-right: 0.5rem;
- width: 16px;
- height: 16px; }
-
.section_add_menus {
text-align: right;
clear: both; }
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%; }
.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;
/* course.less */
/* COURSE CONTENT */
-.section-modchooser-link img {
- margin-right: 0.5rem;
- width: 16px;
- height: 16px; }
-
.section_add_menus {
text-align: right;
clear: both; }
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