1 // This file is part of Moodle - http://moodle.org/
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 * A type of dialogue used as for choosing options.
19 * @module core_course/local/chooser/dialogue
21 * @copyright 2019 Mihail Geshoski <mihail@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 import $ from 'jquery';
26 import * as ModalEvents from 'core/modal_events';
27 import selectors from 'core_course/local/activitychooser/selectors';
28 import * as Templates from 'core/templates';
29 import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
30 import {addIconToContainer} from 'core/loadingicon';
33 * Given an event from the main module 'page' navigate to it's help section via a carousel.
35 * @method showModuleHelp
36 * @param {jQuery} carousel Our initialized carousel to manipulate
37 * @param {Object} moduleData Data of the module to carousel to
39 const showModuleHelp = (carousel, moduleData) => {
40 const help = carousel.find(selectors.regions.help)[0];
44 const spinnerPromise = addIconToContainer(help);
47 let transitionPromiseResolver = null;
48 const transitionPromise = new Promise(resolve => {
49 transitionPromiseResolver = resolve;
52 // Build up the html & js ready to place into the help section.
53 const contentPromise = Templates.renderForPromise('core_course/chooser_help', moduleData);
55 // Wait for the content to be ready, and for the transition to be complet.
56 Promise.all([contentPromise, spinnerPromise, transitionPromise])
57 .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))
59 help.querySelector(selectors.regions.chooserSummary.description).focus();
62 .catch(Notification.exception);
64 // Move to the next slide, and resolve the transition promise when it's done.
65 carousel.one('slid.bs.carousel', () => {
66 transitionPromiseResolver();
68 // Trigger the transition between 'pages'.
69 carousel.carousel('next');
73 * Register chooser related event listeners.
75 * @method registerListenerEvents
76 * @param {Promise} modal Our modal that we are working with
77 * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
79 const registerListenerEvents = (modal, mappedModules) => {
80 const bodyClickListener = e => {
81 if (e.target.closest(selectors.actions.optionActions.showSummary)) {
82 const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
84 const module = e.target.closest(selectors.regions.chooserOption.container);
85 const moduleName = module.dataset.modname;
86 const moduleData = mappedModules.get(moduleName);
87 showModuleHelp(carousel, moduleData);
90 // From the help screen go back to the module overview.
91 if (e.target.matches(selectors.actions.closeOption)) {
92 const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
94 // Trigger the transition between 'pages'.
95 carousel.carousel('prev');
96 carousel.on('slid.bs.carousel', () => {
97 const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
98 const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));
104 modal.getBodyPromise()
106 // The return value of getBodyPromise is a jquery object containing the body NodeElement.
107 .then(body => body[0])
109 // Set up the carousel.
111 $(body.querySelector(selectors.regions.carousel))
121 // Add the listener for clicks on the body.
123 body.addEventListener('click', bodyClickListener);
127 // Register event listeners related to the keyboard navigation controls.
129 initKeyboardNavigation(body, mappedModules);
137 * Initialise the keyboard navigation controls for the chooser.
139 * @method initKeyboardNavigation
140 * @param {NodeElement} body Our modal that we are working with
141 * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
143 const initKeyboardNavigation = (body, mappedModules) => {
145 const chooserOptions = body.querySelectorAll(selectors.regions.chooserOption.container);
147 Array.from(chooserOptions).forEach((element) => {
148 return element.addEventListener('keyup', (e) => {
149 const chooserOptions = document.querySelector(selectors.regions.chooserOptions);
151 // Check for enter/ space triggers for showing the help.
152 if (e.keyCode === enter || e.keyCode === space) {
153 if (e.target.matches(selectors.actions.optionActions.showSummary)) {
155 const module = e.target.closest(selectors.regions.chooserOption.container);
156 const moduleName = module.dataset.modname;
157 const moduleData = mappedModules.get(moduleName);
158 const carousel = $(body.querySelector(selectors.regions.carousel));
164 showModuleHelp(carousel, moduleData);
169 if (e.keyCode === arrowRight) {
171 const currentOption = e.target.closest(selectors.regions.chooserOption.container);
172 const nextOption = currentOption.nextElementSibling;
173 const firstOption = chooserOptions.firstElementChild;
174 const toFocusOption = clickErrorHandler(nextOption, firstOption);
175 focusChooserOption(toFocusOption, currentOption);
179 if (e.keyCode === arrowLeft) {
181 const currentOption = e.target.closest(selectors.regions.chooserOption.container);
182 const previousOption = currentOption.previousElementSibling;
183 const lastOption = chooserOptions.lastElementChild;
184 const toFocusOption = clickErrorHandler(previousOption, lastOption);
185 focusChooserOption(toFocusOption, currentOption);
188 if (e.keyCode === home) {
190 const currentOption = e.target.closest(selectors.regions.chooserOption.container);
191 const firstOption = chooserOptions.firstElementChild;
192 focusChooserOption(firstOption, currentOption);
195 if (e.keyCode === end) {
197 const currentOption = e.target.closest(selectors.regions.chooserOption.container);
198 const lastOption = chooserOptions.lastElementChild;
199 focusChooserOption(lastOption, currentOption);
206 * Focus on a chooser option element and remove the previous chooser element from the focus order
208 * @method focusChooserOption
209 * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus
210 * @param {HTMLElement} previousChooserOption The previous focused option element
212 const focusChooserOption = (currentChooserOption, previousChooserOption = false) => {
213 if (previousChooserOption !== false) {
214 const previousChooserOptionLink = previousChooserOption.querySelector(selectors.actions.addChooser);
215 const previousChooserOptionHelp = previousChooserOption.querySelector(selectors.actions.optionActions.showSummary);
216 // Set tabindex to -1 to remove the previous chooser option element from the focus order.
217 previousChooserOption.tabIndex = -1;
218 previousChooserOptionLink.tabIndex = -1;
219 previousChooserOptionHelp.tabIndex = -1;
222 const currentChooserOptionLink = currentChooserOption.querySelector(selectors.actions.addChooser);
223 const currentChooserOptionHelp = currentChooserOption.querySelector(selectors.actions.optionActions.showSummary);
224 // Set tabindex to 0 to add current chooser option element to the focus order.
225 currentChooserOption.tabIndex = 0;
226 currentChooserOptionLink.tabIndex = 0;
227 currentChooserOptionHelp.tabIndex = 0;
228 // Focus the current chooser option element.
229 currentChooserOption.focus();
233 * Small error handling function to make sure the navigated to object exists
235 * @method clickErrorHandler
236 * @param {HTMLElement} item What we want to check exists
237 * @param {HTMLElement} fallback If we dont match anything fallback the focus
240 const clickErrorHandler = (item, fallback) => {
249 * Display the module chooser.
251 * @method displayChooser
252 * @param {HTMLElement} origin The calling button
253 * @param {Object} modal Our created modal for the section
254 * @param {Array} sectionModules An array of all of the built module information
256 export const displayChooser = (origin, modal, sectionModules) => {
258 // Make a map so we can quickly fetch a specific module's object for either rendering or searching.
259 const mappedModules = new Map();
260 sectionModules.forEach((module) => {
261 mappedModules.set(module.modulename, module);
264 // Register event listeners.
265 registerListenerEvents(modal, mappedModules);
267 // We want to focus on the action select when the dialog is closed.
268 modal.getRoot().on(ModalEvents.hidden, () => {
272 // We want to focus on the first chooser option element as soon as the modal is opened.
273 modal.getRoot().on(ModalEvents.shown, () => {
274 modal.getModal()[0].tabIndex = -1;
276 modal.getBodyPromise()
278 const firstChooserOption = body[0].querySelector(selectors.regions.chooserOption.container);
279 focusChooserOption(firstChooserOption);
283 .catch(Notification.exception);