1 |
efrain |
1 |
// This file is part of Moodle - http://moodle.org/
|
|
|
2 |
//
|
|
|
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.
|
|
|
7 |
//
|
|
|
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.
|
|
|
12 |
//
|
|
|
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/>.
|
|
|
15 |
|
|
|
16 |
/**
|
|
|
17 |
* A type of dialogue used as for choosing options.
|
|
|
18 |
*
|
|
|
19 |
* @module core_course/local/activitychooser/dialogue
|
|
|
20 |
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
|
|
|
21 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
22 |
*/
|
|
|
23 |
|
|
|
24 |
import $ from 'jquery';
|
|
|
25 |
import * as ModalEvents from 'core/modal_events';
|
|
|
26 |
import selectors from 'core_course/local/activitychooser/selectors';
|
|
|
27 |
import * as Templates from 'core/templates';
|
|
|
28 |
import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
|
|
|
29 |
import {addIconToContainer} from 'core/loadingicon';
|
|
|
30 |
import * as Repository from 'core_course/local/activitychooser/repository';
|
|
|
31 |
import Notification from 'core/notification';
|
|
|
32 |
import {debounce} from 'core/utils';
|
|
|
33 |
const getPlugin = pluginName => import(pluginName);
|
|
|
34 |
|
|
|
35 |
/**
|
|
|
36 |
* Given an event from the main module 'page' navigate to it's help section via a carousel.
|
|
|
37 |
*
|
|
|
38 |
* @method showModuleHelp
|
|
|
39 |
* @param {jQuery} carousel Our initialized carousel to manipulate
|
|
|
40 |
* @param {Object} moduleData Data of the module to carousel to
|
|
|
41 |
* @param {jQuery} modal We need to figure out if the current modal has a footer.
|
|
|
42 |
*/
|
|
|
43 |
const showModuleHelp = (carousel, moduleData, modal = null) => {
|
|
|
44 |
// If we have a real footer then we need to change temporarily.
|
|
|
45 |
if (modal !== null && moduleData.showFooter === true) {
|
|
|
46 |
modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));
|
|
|
47 |
}
|
|
|
48 |
const help = carousel.find(selectors.regions.help)[0];
|
|
|
49 |
help.innerHTML = '';
|
|
|
50 |
help.classList.add('m-auto');
|
|
|
51 |
|
|
|
52 |
// Add a spinner.
|
|
|
53 |
const spinnerPromise = addIconToContainer(help);
|
|
|
54 |
|
|
|
55 |
// Used later...
|
|
|
56 |
let transitionPromiseResolver = null;
|
|
|
57 |
const transitionPromise = new Promise(resolve => {
|
|
|
58 |
transitionPromiseResolver = resolve;
|
|
|
59 |
});
|
|
|
60 |
|
|
|
61 |
// Build up the html & js ready to place into the help section.
|
|
|
62 |
const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);
|
|
|
63 |
|
|
|
64 |
// Wait for the content to be ready, and for the transition to be complet.
|
|
|
65 |
Promise.all([contentPromise, spinnerPromise, transitionPromise])
|
|
|
66 |
.then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))
|
|
|
67 |
.then(() => {
|
|
|
68 |
help.querySelector(selectors.regions.chooserSummary.header).focus();
|
|
|
69 |
return help;
|
|
|
70 |
})
|
|
|
71 |
.catch(Notification.exception);
|
|
|
72 |
|
|
|
73 |
// Move to the next slide, and resolve the transition promise when it's done.
|
|
|
74 |
carousel.one('slid.bs.carousel', () => {
|
|
|
75 |
transitionPromiseResolver();
|
|
|
76 |
});
|
|
|
77 |
// Trigger the transition between 'pages'.
|
|
|
78 |
carousel.carousel('next');
|
|
|
79 |
};
|
|
|
80 |
|
|
|
81 |
/**
|
|
|
82 |
* Given a user wants to change the favourite state of a module we either add or remove the status.
|
|
|
83 |
* We also propergate this change across our map of modals.
|
|
|
84 |
*
|
|
|
85 |
* @method manageFavouriteState
|
|
|
86 |
* @param {HTMLElement} modalBody The DOM node of the modal to manipulate
|
|
|
87 |
* @param {HTMLElement} caller
|
|
|
88 |
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
|
|
|
89 |
*/
|
|
|
90 |
const manageFavouriteState = async(modalBody, caller, partialFavourite) => {
|
|
|
91 |
const isFavourite = caller.dataset.favourited;
|
|
|
92 |
const id = caller.dataset.id;
|
|
|
93 |
const name = caller.dataset.name;
|
|
|
94 |
const internal = caller.dataset.internal;
|
|
|
95 |
// Switch on fave or not.
|
|
|
96 |
if (isFavourite === 'true') {
|
|
|
97 |
await Repository.unfavouriteModule(name, id);
|
|
|
98 |
|
|
|
99 |
partialFavourite(internal, false, modalBody);
|
|
|
100 |
} else {
|
|
|
101 |
await Repository.favouriteModule(name, id);
|
|
|
102 |
|
|
|
103 |
partialFavourite(internal, true, modalBody);
|
|
|
104 |
}
|
|
|
105 |
|
|
|
106 |
};
|
|
|
107 |
|
|
|
108 |
/**
|
|
|
109 |
* Register chooser related event listeners.
|
|
|
110 |
*
|
|
|
111 |
* @method registerListenerEvents
|
|
|
112 |
* @param {Promise} modal Our modal that we are working with
|
|
|
113 |
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
|
|
|
114 |
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
|
|
|
115 |
* @param {Object} footerData Our base footer object.
|
|
|
116 |
*/
|
|
|
117 |
const registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {
|
|
|
118 |
const bodyClickListener = async(e) => {
|
|
|
119 |
if (e.target.closest(selectors.actions.optionActions.showSummary)) {
|
|
|
120 |
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
|
|
|
121 |
|
|
|
122 |
const module = e.target.closest(selectors.regions.chooserOption.container);
|
|
|
123 |
const moduleName = module.dataset.modname;
|
|
|
124 |
const moduleData = mappedModules.get(moduleName);
|
|
|
125 |
// We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
|
|
|
126 |
moduleData.showFooter = modal.hasFooterContent();
|
|
|
127 |
showModuleHelp(carousel, moduleData, modal);
|
|
|
128 |
}
|
|
|
129 |
|
|
|
130 |
if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {
|
|
|
131 |
const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);
|
|
|
132 |
await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
|
|
|
133 |
const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute("href");
|
|
|
134 |
const sectionChooserOptions = modal.getBody()[0]
|
|
|
135 |
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
|
|
|
136 |
const firstChooserOption = sectionChooserOptions
|
|
|
137 |
.querySelector(selectors.regions.chooserOption.container);
|
|
|
138 |
toggleFocusableChooserOption(firstChooserOption, true);
|
|
|
139 |
initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal);
|
|
|
140 |
}
|
|
|
141 |
|
|
|
142 |
// From the help screen go back to the module overview.
|
|
|
143 |
if (e.target.matches(selectors.actions.closeOption)) {
|
|
|
144 |
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
|
|
|
145 |
|
|
|
146 |
// Trigger the transition between 'pages'.
|
|
|
147 |
carousel.carousel('prev');
|
|
|
148 |
carousel.on('slid.bs.carousel', () => {
|
|
|
149 |
const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
|
|
|
150 |
const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));
|
|
|
151 |
caller.focus();
|
|
|
152 |
});
|
|
|
153 |
}
|
|
|
154 |
|
|
|
155 |
// The "clear search" button is triggered.
|
|
|
156 |
if (e.target.closest(selectors.actions.clearSearch)) {
|
|
|
157 |
// Clear the entered search query in the search bar and hide the search results container.
|
|
|
158 |
const searchInput = modal.getBody()[0].querySelector(selectors.actions.search);
|
|
|
159 |
searchInput.value = "";
|
|
|
160 |
searchInput.focus();
|
|
|
161 |
toggleSearchResultsView(modal, mappedModules, searchInput.value);
|
|
|
162 |
}
|
|
|
163 |
};
|
|
|
164 |
|
|
|
165 |
// We essentially have two types of footer.
|
|
|
166 |
// A fake one that is handled within the template for chooser_help and then all of the stuff for
|
|
|
167 |
// modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we
|
|
|
168 |
// need to manage. The below code handles a real footer going to a mnet carousel item.
|
|
|
169 |
const footerClickListener = async(e) => {
|
|
|
170 |
if (footerData.footer === true) {
|
|
|
171 |
const footerjs = await getPlugin(footerData.customfooterjs);
|
|
|
172 |
await footerjs.footerClickListener(e, footerData, modal);
|
|
|
173 |
}
|
|
|
174 |
};
|
|
|
175 |
|
|
|
176 |
modal.getBodyPromise()
|
|
|
177 |
|
|
|
178 |
// The return value of getBodyPromise is a jquery object containing the body NodeElement.
|
|
|
179 |
.then(body => body[0])
|
|
|
180 |
|
|
|
181 |
// Set up the carousel.
|
|
|
182 |
.then(body => {
|
|
|
183 |
$(body.querySelector(selectors.regions.carousel))
|
|
|
184 |
.carousel({
|
|
|
185 |
interval: false,
|
|
|
186 |
pause: true,
|
|
|
187 |
keyboard: false
|
|
|
188 |
});
|
|
|
189 |
|
|
|
190 |
return body;
|
|
|
191 |
})
|
|
|
192 |
|
|
|
193 |
// Add the listener for clicks on the body.
|
|
|
194 |
.then(body => {
|
|
|
195 |
body.addEventListener('click', bodyClickListener);
|
|
|
196 |
return body;
|
|
|
197 |
})
|
|
|
198 |
|
|
|
199 |
// Add a listener for an input change in the activity chooser's search bar.
|
|
|
200 |
.then(body => {
|
|
|
201 |
const searchInput = body.querySelector(selectors.actions.search);
|
|
|
202 |
// The search input is triggered.
|
|
|
203 |
searchInput.addEventListener('input', debounce(() => {
|
|
|
204 |
// Display the search results.
|
|
|
205 |
toggleSearchResultsView(modal, mappedModules, searchInput.value);
|
|
|
206 |
}, 300));
|
|
|
207 |
return body;
|
|
|
208 |
})
|
|
|
209 |
|
|
|
210 |
// Register event listeners related to the keyboard navigation controls.
|
|
|
211 |
.then(body => {
|
|
|
212 |
// Get the active chooser options section.
|
|
|
213 |
const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute("href");
|
|
|
214 |
const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
|
|
|
215 |
const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);
|
|
|
216 |
|
|
|
217 |
toggleFocusableChooserOption(firstChooserOption, true);
|
|
|
218 |
initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);
|
|
|
219 |
|
|
|
220 |
return body;
|
|
|
221 |
})
|
|
|
222 |
.catch();
|
|
|
223 |
|
|
|
224 |
modal.getFooterPromise()
|
|
|
225 |
|
|
|
226 |
// The return value of getBodyPromise is a jquery object containing the body NodeElement.
|
|
|
227 |
.then(footer => footer[0])
|
|
|
228 |
// Add the listener for clicks on the footer.
|
|
|
229 |
.then(footer => {
|
|
|
230 |
footer.addEventListener('click', footerClickListener);
|
|
|
231 |
return footer;
|
|
|
232 |
})
|
|
|
233 |
.catch();
|
|
|
234 |
};
|
|
|
235 |
|
|
|
236 |
/**
|
|
|
237 |
* Initialise the keyboard navigation controls for the chooser options.
|
|
|
238 |
*
|
|
|
239 |
* @method initChooserOptionsKeyboardNavigation
|
|
|
240 |
* @param {HTMLElement} body Our modal that we are working with
|
|
|
241 |
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
|
|
|
242 |
* @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items
|
|
|
243 |
* @param {Object} modal Our created modal for the section
|
|
|
244 |
*/
|
|
|
245 |
const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {
|
|
|
246 |
const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);
|
|
|
247 |
|
|
|
248 |
Array.from(chooserOptions).forEach((element) => {
|
|
|
249 |
return element.addEventListener('keydown', (e) => {
|
|
|
250 |
|
|
|
251 |
// Check for enter/ space triggers for showing the help.
|
|
|
252 |
if (e.keyCode === enter || e.keyCode === space) {
|
|
|
253 |
if (e.target.matches(selectors.actions.optionActions.showSummary)) {
|
|
|
254 |
e.preventDefault();
|
|
|
255 |
const module = e.target.closest(selectors.regions.chooserOption.container);
|
|
|
256 |
const moduleName = module.dataset.modname;
|
|
|
257 |
const moduleData = mappedModules.get(moduleName);
|
|
|
258 |
const carousel = $(body.querySelector(selectors.regions.carousel));
|
|
|
259 |
carousel.carousel({
|
|
|
260 |
interval: false,
|
|
|
261 |
pause: true,
|
|
|
262 |
keyboard: false
|
|
|
263 |
});
|
|
|
264 |
|
|
|
265 |
// We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
|
|
|
266 |
moduleData.showFooter = modal.hasFooterContent();
|
|
|
267 |
showModuleHelp(carousel, moduleData, modal);
|
|
|
268 |
}
|
|
|
269 |
}
|
|
|
270 |
|
|
|
271 |
// Next.
|
|
|
272 |
if (e.keyCode === arrowRight) {
|
|
|
273 |
e.preventDefault();
|
|
|
274 |
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
|
|
275 |
const nextOption = currentOption.nextElementSibling;
|
|
|
276 |
const firstOption = chooserOptionsContainer.firstElementChild;
|
|
|
277 |
const toFocusOption = clickErrorHandler(nextOption, firstOption);
|
|
|
278 |
focusChooserOption(toFocusOption, currentOption);
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
// Previous.
|
|
|
282 |
if (e.keyCode === arrowLeft) {
|
|
|
283 |
e.preventDefault();
|
|
|
284 |
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
|
|
285 |
const previousOption = currentOption.previousElementSibling;
|
|
|
286 |
const lastOption = chooserOptionsContainer.lastElementChild;
|
|
|
287 |
const toFocusOption = clickErrorHandler(previousOption, lastOption);
|
|
|
288 |
focusChooserOption(toFocusOption, currentOption);
|
|
|
289 |
}
|
|
|
290 |
|
|
|
291 |
if (e.keyCode === home) {
|
|
|
292 |
e.preventDefault();
|
|
|
293 |
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
|
|
294 |
const firstOption = chooserOptionsContainer.firstElementChild;
|
|
|
295 |
focusChooserOption(firstOption, currentOption);
|
|
|
296 |
}
|
|
|
297 |
|
|
|
298 |
if (e.keyCode === end) {
|
|
|
299 |
e.preventDefault();
|
|
|
300 |
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
|
|
301 |
const lastOption = chooserOptionsContainer.lastElementChild;
|
|
|
302 |
focusChooserOption(lastOption, currentOption);
|
|
|
303 |
}
|
|
|
304 |
});
|
|
|
305 |
});
|
|
|
306 |
};
|
|
|
307 |
|
|
|
308 |
/**
|
|
|
309 |
* Focus on a chooser option element and remove the previous chooser element from the focus order
|
|
|
310 |
*
|
|
|
311 |
* @method focusChooserOption
|
|
|
312 |
* @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus
|
|
|
313 |
* @param {HTMLElement|null} previousChooserOption The previous focused option element
|
|
|
314 |
*/
|
|
|
315 |
const focusChooserOption = (currentChooserOption, previousChooserOption = null) => {
|
|
|
316 |
if (previousChooserOption !== null) {
|
|
|
317 |
toggleFocusableChooserOption(previousChooserOption, false);
|
|
|
318 |
}
|
|
|
319 |
|
|
|
320 |
toggleFocusableChooserOption(currentChooserOption, true);
|
|
|
321 |
currentChooserOption.focus();
|
|
|
322 |
};
|
|
|
323 |
|
|
|
324 |
/**
|
|
|
325 |
* Add or remove a chooser option from the focus order.
|
|
|
326 |
*
|
|
|
327 |
* @method toggleFocusableChooserOption
|
|
|
328 |
* @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order
|
|
|
329 |
* @param {Boolean} isFocusable Whether the chooser element is focusable or not
|
|
|
330 |
*/
|
|
|
331 |
const toggleFocusableChooserOption = (chooserOption, isFocusable) => {
|
|
|
332 |
const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);
|
|
|
333 |
const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);
|
|
|
334 |
const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);
|
|
|
335 |
|
|
|
336 |
if (isFocusable) {
|
|
|
337 |
// Set tabindex to 0 to add current chooser option element to the focus order.
|
|
|
338 |
chooserOption.tabIndex = 0;
|
|
|
339 |
chooserOptionLink.tabIndex = 0;
|
|
|
340 |
chooserOptionHelp.tabIndex = 0;
|
|
|
341 |
chooserOptionFavourite.tabIndex = 0;
|
|
|
342 |
} else {
|
|
|
343 |
// Set tabindex to -1 to remove the previous chooser option element from the focus order.
|
|
|
344 |
chooserOption.tabIndex = -1;
|
|
|
345 |
chooserOptionLink.tabIndex = -1;
|
|
|
346 |
chooserOptionHelp.tabIndex = -1;
|
|
|
347 |
chooserOptionFavourite.tabIndex = -1;
|
|
|
348 |
}
|
|
|
349 |
};
|
|
|
350 |
|
|
|
351 |
/**
|
|
|
352 |
* Small error handling function to make sure the navigated to object exists
|
|
|
353 |
*
|
|
|
354 |
* @method clickErrorHandler
|
|
|
355 |
* @param {HTMLElement} item What we want to check exists
|
|
|
356 |
* @param {HTMLElement} fallback If we dont match anything fallback the focus
|
|
|
357 |
* @return {HTMLElement}
|
|
|
358 |
*/
|
|
|
359 |
const clickErrorHandler = (item, fallback) => {
|
|
|
360 |
if (item !== null) {
|
|
|
361 |
return item;
|
|
|
362 |
} else {
|
|
|
363 |
return fallback;
|
|
|
364 |
}
|
|
|
365 |
};
|
|
|
366 |
|
|
|
367 |
/**
|
|
|
368 |
* Render the search results in a defined container
|
|
|
369 |
*
|
|
|
370 |
* @method renderSearchResults
|
|
|
371 |
* @param {HTMLElement} searchResultsContainer The container where the data should be rendered
|
|
|
372 |
* @param {Object} searchResultsData Data containing the module items that satisfy the search criteria
|
|
|
373 |
*/
|
|
|
374 |
const renderSearchResults = async(searchResultsContainer, searchResultsData) => {
|
|
|
375 |
const templateData = {
|
|
|
376 |
'searchresultsnumber': searchResultsData.length,
|
|
|
377 |
'searchresults': searchResultsData
|
|
|
378 |
};
|
|
|
379 |
// Build up the html & js ready to place into the help section.
|
|
|
380 |
const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);
|
|
|
381 |
await Templates.replaceNodeContents(searchResultsContainer, html, js);
|
|
|
382 |
};
|
|
|
383 |
|
|
|
384 |
/**
|
|
|
385 |
* Toggle (display/hide) the search results depending on the value of the search query
|
|
|
386 |
*
|
|
|
387 |
* @method toggleSearchResultsView
|
|
|
388 |
* @param {Object} modal Our created modal for the section
|
|
|
389 |
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
|
|
|
390 |
* @param {String} searchQuery The search query
|
|
|
391 |
*/
|
|
|
392 |
const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
|
|
|
393 |
const modalBody = modal.getBody()[0];
|
|
|
394 |
const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);
|
|
|
395 |
const chooserContainer = modalBody.querySelector(selectors.regions.chooser);
|
|
|
396 |
const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);
|
|
|
397 |
|
|
|
398 |
if (searchQuery.length > 0) { // Search query is present.
|
|
|
399 |
const searchResultsData = searchModules(mappedModules, searchQuery);
|
|
|
400 |
await renderSearchResults(searchResultsContainer, searchResultsData);
|
|
|
401 |
const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);
|
|
|
402 |
const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);
|
|
|
403 |
if (firstSearchResultItem) {
|
|
|
404 |
// Set the first result item to be focusable.
|
|
|
405 |
toggleFocusableChooserOption(firstSearchResultItem, true);
|
|
|
406 |
// Register keyboard events on the created search result items.
|
|
|
407 |
initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);
|
|
|
408 |
}
|
|
|
409 |
// Display the "clear" search button in the activity chooser search bar.
|
|
|
410 |
clearSearchButton.classList.remove('d-none');
|
|
|
411 |
// Hide the default chooser options container.
|
|
|
412 |
chooserContainer.setAttribute('hidden', 'hidden');
|
|
|
413 |
// Display the search results container.
|
|
|
414 |
searchResultsContainer.removeAttribute('hidden');
|
|
|
415 |
} else { // Search query is not present.
|
|
|
416 |
// Hide the "clear" search button in the activity chooser search bar.
|
|
|
417 |
clearSearchButton.classList.add('d-none');
|
|
|
418 |
// Hide the search results container.
|
|
|
419 |
searchResultsContainer.setAttribute('hidden', 'hidden');
|
|
|
420 |
// Display the default chooser options container.
|
|
|
421 |
chooserContainer.removeAttribute('hidden');
|
|
|
422 |
}
|
|
|
423 |
};
|
|
|
424 |
|
|
|
425 |
/**
|
|
|
426 |
* Return the list of modules which have a name or description that matches the given search term.
|
|
|
427 |
*
|
|
|
428 |
* @method searchModules
|
|
|
429 |
* @param {Array} modules List of available modules
|
|
|
430 |
* @param {String} searchTerm The search term to match
|
|
|
431 |
* @return {Array}
|
|
|
432 |
*/
|
|
|
433 |
const searchModules = (modules, searchTerm) => {
|
|
|
434 |
if (searchTerm === '') {
|
|
|
435 |
return modules;
|
|
|
436 |
}
|
|
|
437 |
searchTerm = searchTerm.toLowerCase();
|
|
|
438 |
const searchResults = [];
|
|
|
439 |
modules.forEach((activity) => {
|
|
|
440 |
const activityName = activity.title.toLowerCase();
|
|
|
441 |
const activityDesc = activity.help.toLowerCase();
|
|
|
442 |
if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {
|
|
|
443 |
searchResults.push(activity);
|
|
|
444 |
}
|
|
|
445 |
});
|
|
|
446 |
|
|
|
447 |
return searchResults;
|
|
|
448 |
};
|
|
|
449 |
|
|
|
450 |
/**
|
|
|
451 |
* Set up our tabindex information across the chooser.
|
|
|
452 |
*
|
|
|
453 |
* @method setupKeyboardAccessibility
|
|
|
454 |
* @param {Promise} modal Our created modal for the section
|
|
|
455 |
* @param {Map} mappedModules A map of all of the built module information
|
|
|
456 |
*/
|
|
|
457 |
const setupKeyboardAccessibility = (modal, mappedModules) => {
|
|
|
458 |
modal.getModal()[0].tabIndex = -1;
|
|
|
459 |
|
|
|
460 |
modal.getBodyPromise().then(body => {
|
|
|
461 |
$(selectors.elements.tab).on('shown.bs.tab', (e) => {
|
|
|
462 |
const activeSectionId = e.target.getAttribute("href");
|
|
|
463 |
const activeSectionChooserOptions = body[0]
|
|
|
464 |
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
|
|
|
465 |
const firstChooserOption = activeSectionChooserOptions
|
|
|
466 |
.querySelector(selectors.regions.chooserOption.container);
|
|
|
467 |
const prevActiveSectionId = e.relatedTarget.getAttribute("href");
|
|
|
468 |
const prevActiveSectionChooserOptions = body[0]
|
|
|
469 |
.querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));
|
|
|
470 |
|
|
|
471 |
// Disable the focus of every chooser option in the previous active section.
|
|
|
472 |
disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
|
|
|
473 |
// Enable the focus of the first chooser option in the current active section.
|
|
|
474 |
toggleFocusableChooserOption(firstChooserOption, true);
|
|
|
475 |
initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);
|
|
|
476 |
});
|
|
|
477 |
return;
|
|
|
478 |
}).catch(Notification.exception);
|
|
|
479 |
};
|
|
|
480 |
|
|
|
481 |
/**
|
|
|
482 |
* Disable the focus of all chooser options in a specific container (section).
|
|
|
483 |
*
|
|
|
484 |
* @method disableFocusAllChooserOptions
|
|
|
485 |
* @param {HTMLElement} sectionChooserOptions The section that contains the chooser items
|
|
|
486 |
*/
|
|
|
487 |
const disableFocusAllChooserOptions = (sectionChooserOptions) => {
|
|
|
488 |
const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);
|
|
|
489 |
allChooserOptions.forEach((chooserOption) => {
|
|
|
490 |
toggleFocusableChooserOption(chooserOption, false);
|
|
|
491 |
});
|
|
|
492 |
};
|
|
|
493 |
|
|
|
494 |
/**
|
|
|
495 |
* Display the module chooser.
|
|
|
496 |
*
|
|
|
497 |
* @method displayChooser
|
|
|
498 |
* @param {Promise} modalPromise Our created modal for the section
|
|
|
499 |
* @param {Array} sectionModules An array of all of the built module information
|
|
|
500 |
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
|
|
|
501 |
* @param {Object} footerData Our base footer object.
|
|
|
502 |
*/
|
|
|
503 |
export const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {
|
|
|
504 |
// Make a map so we can quickly fetch a specific module's object for either rendering or searching.
|
|
|
505 |
const mappedModules = new Map();
|
|
|
506 |
sectionModules.forEach((module) => {
|
|
|
507 |
mappedModules.set(module.componentname + '_' + module.link, module);
|
|
|
508 |
});
|
|
|
509 |
|
|
|
510 |
// Register event listeners.
|
|
|
511 |
modalPromise.then(modal => {
|
|
|
512 |
registerListenerEvents(modal, mappedModules, partialFavourite, footerData);
|
|
|
513 |
|
|
|
514 |
// We want to focus on the first chooser option element as soon as the modal is opened.
|
|
|
515 |
setupKeyboardAccessibility(modal, mappedModules);
|
|
|
516 |
|
|
|
517 |
// We want to focus on the action select when the dialog is closed.
|
|
|
518 |
modal.getRoot().on(ModalEvents.hidden, () => {
|
|
|
519 |
modal.destroy();
|
|
|
520 |
});
|
|
|
521 |
|
|
|
522 |
return modal;
|
|
|
523 |
}).catch();
|
|
|
524 |
};
|