events.forEach((event) => {
document.addEventListener(event, async(e) => {
if (e.target.closest(selectors.elements.sectionmodchooser)) {
+ const data = await fetchModuleData();
const caller = e.target.closest(selectors.elements.sectionmodchooser);
- const builtModuleData = sectionIdMapper(await fetchModuleData(), caller.dataset.sectionid);
+ const favouriteFunction = partiallyAppliedFavouriteManager(data, caller.dataset.sectionid);
+ const builtModuleData = sectionIdMapper(data, caller.dataset.sectionid);
const sectionModal = await modalBuilder(builtModuleData);
- ChooserDialogue.displayChooser(caller, sectionModal, builtModuleData);
+ ChooserDialogue.displayChooser(caller, sectionModal, builtModuleData, favouriteFunction);
}
});
});
*
* @method modalBuilder
* @param {Map} data our map of section ID's & modules to generate modals for
- * @return {Object} TODO
+ * @return {Object} Our modal that we are going to show the user
*/
const modalBuilder = data => buildModal(templateDataBuilder(data));
*/
const templateDataBuilder = (data) => {
// Filter the incoming data to find favourite & recommended modules.
- const favourites = [];
+ const favourites = data.filter(mod => mod.favourite === true);
const recommended = data.filter(mod => mod.recommended === true);
// Given the results of the above filters lets figure out what tab to set active.
}
});
};
+
+/**
+ * A small helper function to handle the case where there are no more favourites
+ * and we need to mess a bit with the available tabs in the chooser
+ *
+ * @method nullFavouriteDomManager
+ * @param {HTMLElement} favouriteTabNav Dom node of the favourite tab nav
+ * @param {HTMLElement} modalBody Our current modals' body
+ */
+const nullFavouriteDomManager = (favouriteTabNav, modalBody) => {
+ favouriteTabNav.classList.add('d-none');
+ // Need to set active to an available tab.
+ if (favouriteTabNav.classList.contains('active')) {
+ favouriteTabNav.classList.remove('active');
+ const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);
+ favouriteTab.classList.remove('active');
+ const recommendedTabNav = modalBody.querySelector(selectors.regions.recommendedTabNav);
+ const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);
+ if (recommendedTabNav.classList.contains('d-none') === false) {
+ recommendedTabNav.classList.add('active');
+ const recommendedTab = modalBody.querySelector(selectors.regions.recommendedTab);
+ recommendedTab.classList.add('active');
+ } else {
+ defaultTabNav.classList.add('active');
+ const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);
+ defaultTab.classList.add('active');
+ }
+
+ }
+};
+
+/**
+ * Export a curried function where the builtModules has been applied.
+ * We have our array of modules so we can rerender the favourites area and have all of the items sorted.
+ *
+ * @method partiallyAppliedFavouriteManager
+ * @param {Array} moduleData This is our raw WS data that we need to manipulate
+ * @param {Number} sectionId We need this to add the sectionID to the URL's in the faves area after rerender
+ * @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array
+ */
+const partiallyAppliedFavouriteManager = (moduleData, sectionId) => {
+ /**
+ * Curried function that is being returned.
+ *
+ * @param {String} internal Internal name of the module to manage
+ * @param {Boolean} favourite Is the caller adding a favourite or removing one?
+ * @param {HTMLElement} modalBody What we need to update whilst we are here
+ */
+ return async(internal, favourite, modalBody) => {
+ const favouriteArea = modalBody.querySelector(selectors.render.favourites);
+
+ // eslint-disable-next-line max-len
+ const favouriteButtons = modalBody.querySelectorAll(`[data-internal="${internal}"] ${selectors.actions.optionActions.manageFavourite}`);
+ const favouriteTabNav = modalBody.querySelector(selectors.regions.favouriteTabNav);
+ const result = moduleData.content_items.find(({name}) => name === internal);
+ const newFaves = {};
+ if (result) {
+ if (favourite) {
+ result.favourite = true;
+
+ newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);
+
+ const builtFaves = sectionIdMapper(newFaves, sectionId);
+
+ const {html, js} = await Templates.renderForPromise('core_course/chooser_favourites', {favourites: builtFaves});
+
+ await Templates.replaceNodeContents(favouriteArea, html, js);
+
+ Array.from(favouriteButtons).forEach((element) => {
+ element.classList.remove('text-muted');
+ element.classList.add('text-primary');
+ element.dataset.favourited = 'true';
+ element.setAttribute('aria-pressed', true);
+ element.firstElementChild.classList.remove('fa-star-o');
+ element.firstElementChild.classList.add('fa-star');
+ });
+
+ favouriteTabNav.classList.remove('d-none');
+ } else {
+ result.favourite = false;
+
+ const nodeToRemove = favouriteArea.querySelector(`[data-internal="${internal}"]`);
+
+ nodeToRemove.parentNode.removeChild(nodeToRemove);
+
+ Array.from(favouriteButtons).forEach((element) => {
+ element.classList.add('text-muted');
+ element.classList.remove('text-primary');
+ element.dataset.favourited = 'false';
+ element.setAttribute('aria-pressed', false);
+ element.firstElementChild.classList.remove('fa-star');
+ element.firstElementChild.classList.add('fa-star-o');
+ });
+ const newFaves = moduleData.content_items.filter(mod => mod.favourite === true);
+
+ if (newFaves.length === 0) {
+ nullFavouriteDomManager(favouriteTabNav, modalBody);
+ }
+ }
+ }
+ };
+};
import * as Templates from 'core/templates';
import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
import {addIconToContainer} from 'core/loadingicon';
+import * as Repository from 'core_course/local/activitychooser/repository';
+import Notification from 'core/notification';
/**
* Given an event from the main module 'page' navigate to it's help section via a carousel.
carousel.carousel('next');
};
+/**
+ * Given a user wants to change the favourite state of a module we either add or remove the status.
+ * We also propergate this change across our map of modals.
+ *
+ * @method manageFavouriteState
+ * @param {HTMLElement} modalBody The DOM node of the modal to manipulate
+ * @param {HTMLElement} caller
+ * @param {Function} partialFavourite Partially applied function we need to manage favourite status
+ */
+const manageFavouriteState = async(modalBody, caller, partialFavourite) => {
+ const isFavourite = caller.dataset.favourited;
+ const id = caller.dataset.id;
+ const name = caller.dataset.name;
+ const internal = caller.dataset.internal;
+ // Switch on fave or not.
+ if (isFavourite === 'true') {
+ await Repository.unfavouriteModule(name, id);
+
+ partialFavourite(internal, false, modalBody);
+ } else {
+ await Repository.favouriteModule(name, id);
+
+ partialFavourite(internal, true, modalBody);
+ }
+
+};
+
/**
* 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}
+ * @param {Function} partialFavourite Partially applied function we need to manage favourite status
*/
-const registerListenerEvents = (modal, mappedModules) => {
+const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
const bodyClickListener = e => {
if (e.target.closest(selectors.actions.optionActions.showSummary)) {
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
showModuleHelp(carousel, moduleData);
}
+ if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {
+ const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);
+ manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
+ }
+
// 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));
if (previousChooserOption !== false) {
const previousChooserOptionLink = previousChooserOption.querySelector(selectors.actions.addChooser);
const previousChooserOptionHelp = previousChooserOption.querySelector(selectors.actions.optionActions.showSummary);
+ const previousChooserOptionFavourite = previousChooserOption.querySelector(selectors.actions.optionActions.manageFavourite);
// Set tabindex to -1 to remove the previous chooser option element from the focus order.
previousChooserOption.tabIndex = -1;
previousChooserOptionLink.tabIndex = -1;
previousChooserOptionHelp.tabIndex = -1;
+ previousChooserOptionFavourite.tabIndex = -1;
}
const currentChooserOptionLink = currentChooserOption.querySelector(selectors.actions.addChooser);
const currentChooserOptionHelp = currentChooserOption.querySelector(selectors.actions.optionActions.showSummary);
+ const currentChooserOptionFavourite = currentChooserOption.querySelector(selectors.actions.optionActions.manageFavourite);
// Set tabindex to 0 to add current chooser option element to the focus order.
currentChooserOption.tabIndex = 0;
currentChooserOptionLink.tabIndex = 0;
currentChooserOptionHelp.tabIndex = 0;
+ currentChooserOptionFavourite.tabIndex = 0;
// Focus the current chooser option element.
currentChooserOption.focus();
};
* @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
+ * @param {Function} partialFavourite Partially applied function we need to manage favourite status
*/
-export const displayChooser = (origin, modal, sectionModules) => {
+export const displayChooser = (origin, modal, sectionModules, partialFavourite) => {
// Make a map so we can quickly fetch a specific module's object for either rendering or searching.
const mappedModules = new Map();
});
// Register event listeners.
- registerListenerEvents(modal, mappedModules);
+ registerListenerEvents(modal, mappedModules, partialFavourite);
// We want to focus on the action select when the dialog is closed.
modal.getRoot().on(ModalEvents.hidden, () => {
};
return ajax.call([request])[0];
};
+
+/**
+ * Given a module name, module ID & the current course we want to specify that the module
+ * is a users' favourite.
+ *
+ * @method favouriteModule
+ * @param {String} modName Frankenstyle name of the component to add favourite
+ * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names
+ * @return {object} jQuery promise
+ */
+export const favouriteModule = (modName, modID) => {
+ const request = {
+ methodname: 'core_course_add_content_item_to_user_favourites',
+ args: {
+ componentname: modName,
+ contentitemid: modID,
+ },
+ };
+ return ajax.call([request])[0];
+};
+
+/**
+ * Given a module name, module ID & the current course we want to specify that the module
+ * is no longer a users' favourite.
+ *
+ * @method unfavouriteModule
+ * @param {String} modName Frankenstyle name of the component to add favourite
+ * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names
+ * @return {object} jQuery promise
+ */
+export const unfavouriteModule = (modName, modID) => {
+ const request = {
+ methodname: 'core_course_remove_content_item_from_user_favourites',
+ args: {
+ componentname: modName,
+ contentitemid: modID,
+ },
+ };
+ return ajax.call([request])[0];
+};
actions: {
optionActions: {
showSummary: getDataSelector('action', 'show-option-summary'),
+ manageFavourite: getDataSelector('action', 'manage-module-favourite'),
},
addChooser: getDataSelector('action', 'add-chooser-option'),
closeOption: getDataSelector('action', 'close-chooser-option-summary'),
hide: getDataSelector('action', 'hide')
},
+ render: {
+ favourites: getDataSelector('render', 'favourites-area'),
+ },
elements: {
section: '.section',
sectionmodchooser: 'button.section-modchooser-link',
<div class="tab-pane {{#favouritesFirst}}active{{/favouritesFirst}}" id="starred-{{uniqid}}" data-region="favourites" role="tabpanel" aria-labelledby="starred-tab-{{uniqid}}">
<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" data-render="favourites-area">
- {{#favourites}}
- {{>core_course/chooser_item}}
- {{/favourites}}
+ {{>core_course/chooser_favourites}}
</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_favourites
+
+ Chooser favourite template partial.
+
+ Example context (json):
+ {
+ "favourites": {
+ "label": "Option name",
+ "description": "Option description",
+ "urls": {
+ "addoption": "http://addoptionurl.com"
+ },
+ "icon": "<img class='icon' src='http://urltooptionicon'>"
+ }
+ }
+}}
+{{#favourites}}
+ {{>core_course/chooser_item}}
+{{/favourites}}
"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="{{componentname}}_{{link}}">
+<div role="menuitem" tabindex="-1" aria-label="{{title}}" class="option d-block text-center py-3 px-2" data-region="chooser-option-container" data-internal="{{name}}" data-modname="{{componentname}}_{{link}}">
<div class="optioninfo w-100" data-region="chooser-option-info-container">
<a class="d-block" href="{{link}}" title="{{#str}} addnew, moodle, {{title}} {{/str}}" tabindex="-1" data-action="add-chooser-option">
<span class="optionicon d-block">
<span class="optionname d-block">{{title}}</span>
</a>
<div class="optionactions btn-group" role="group" data-region="chooser-option-actions-container">
+ {{^legacyitem}}
+ <button class="btn btn-icon icon-no-margin icon-size-3 m-0 optionaction {{#favourite}}text-primary{{/favourite}}{{^favourite}}text-muted{{/favourite}}"
+ data-action="manage-module-favourite"
+ data-favourited="{{favourite}}"
+ data-id="{{id}}"
+ data-name="{{componentname}}"
+ data-internal="{{name}}"
+ {{^favourite}}
+ aria-pressed="false"
+ {{/favourite}}
+ {{#favourite}}
+ aria-pressed="true"
+ {{/favourite}}
+ aria-label="{{#str}} aria:modulefavourite, core_course, {{title}} {{/str}}"
+ tabindex="-1"
+ >
+ {{#favourite}}
+ {{#pix}} i/star, core {{/pix}}
+ {{/favourite}}
+ {{^favourite}}
+ {{#pix}} i/star-o, core {{/pix}}
+ {{/favourite}}
+ </button>
+ {{/legacyitem}}
<button class="btn btn-icon icon-no-margin icon-size-3 m-0 optionaction" data-action="show-option-summary" tabindex="-1">
<span aria-hidden="true">{{#pix}} docs, core {{/pix}}</span>
<span class="sr-only">{{#str}} informationformodule, core_course, {{title}} {{/str}}</span>
Then I should see "Recommended" in the "Add an activity or resource" "dialogue"
And I click on "Recommended" "link" in the "Add an activity or resource" "dialogue"
And I should see "Book" in the "recommended" "core_course > Activity chooser tab"
+
+ Scenario: Favourite a module in the activity chooser
+ Given I open the activity chooser
+ And I should not see "Starred" in the "Add an activity or resource" "dialogue"
+ And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
+ And I should see "Starred" in the "Add an activity or resource" "dialogue"
+ When I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
+ Then I should see "Assignment" in the "favourites" "core_course > Activity chooser tab"
+ And I click on "Information about the Assignment activity" "button" in the "favourites" "core_course > Activity chooser tab"
+ And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
+
+ Scenario: Add a favourite module and check it exists when reopening the chooser
+ Given I open the activity chooser
+ And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
+ And I click on "Star Forum module" "button" in the "Add an activity or resource" "dialogue"
+ And I should see "Starred" in the "Add an activity or resource" "dialogue"
+ And I click on "Close" "button" in the "Add an activity or resource" "dialogue"
+ When I click on "Add an activity or resource" "button" in the "Topic 3" "section"
+ And I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
+ Then I should see "Forum" in the "favourites" "core_course > Activity chooser tab"
+
+ Scenario: Add a favourite and then remove it whilst checking the tabs work as expected
+ Given I open the activity chooser
+ And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
+ And I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
+ And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
+ Then I should not see "Starred" in the "Add an activity or resource" "dialogue"
$string['aria:favourite'] = 'Course is starred';
$string['aria:favouritestab'] = 'Your starred modules';
$string['aria:recommendedtab'] = 'The recommended modules';
+$string['aria:modulefavourite'] = 'Star {$a} module';
$string['coursealreadyfinished'] = 'Course already finished';
$string['coursenotyetstarted'] = 'The course has not yet started';
$string['coursenotyetfinished'] = 'The course has not yet finished';
'core:i/show' => 'fa-eye-slash',
'core:i/siteevent' => 'fa-globe',
'core:i/star' => 'fa-star',
+ 'core:i/star-o' => 'fa-star-o',
'core:i/star-rating' => 'fa-star',
'core:i/stats' => 'fa-line-chart',
'core:i/switch' => 'fa-exchange',