MDL-63064 block_myoverview: trigger event when course is starred
[moodle.git] / blocks / myoverview / amd / src / view.js
blob9e9de76c6c6337f63dcf73961c7f18566eb129d0
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.
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/>.
16 /**
17  * Manage the courses view for the overview block.
18  *
19  * @package    block_myoverview
20  * @copyright  2018 Bas Brands <bas@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 define(
26     'jquery',
27     'block_myoverview/repository',
28     'core/paged_content_factory',
29     'core/custom_interaction_events',
30     'core/notification',
31     'core/templates',
33 function(
34     $,
35     Repository,
36     PagedContentFactory,
37     CustomEvents,
38     Notification,
39     Templates
40 ) {
42     var SELECTORS = {
43         ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
44         ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
45         FAVOURITE_ICON: '[data-region="favourite-icon"]',
46         ICON_IS_FAVOURITE: '[data-region="is-favourite"]',
47         ICON_NOT_FAVOURITE: '[data-region="not-favourite"]',
48         PAGED_CONTENT_CONTAINER: '[data-region="page-container"]'
50     };
52     var TEMPLATES = {
53         COURSES_CARDS: 'block_myoverview/view-cards',
54         COURSES_LIST: 'block_myoverview/view-list',
55         COURSES_SUMMARY: 'block_myoverview/view-summary',
56         NOCOURSES: 'block_myoverview/no-courses'
57     };
59     var NUMCOURSES_PERPAGE = [12, 24, 48];
61     var loadedPages = [];
63     /**
64      * Get filter values from DOM.
65      *
66      * @param {object} root The root element for the courses view.
67      * @return {filters} Set filters.
68      */
69     var getFilterValues = function(root) {
70         var filters = {};
71         filters.display = root.attr('data-display');
72         filters.grouping = root.attr('data-grouping');
73         filters.sort = root.attr('data-sort');
74         return filters;
75     };
77     // We want the paged content controls below the paged content area.
78     // and the controls should be ignored while data is loading.
79     var DEFAULT_PAGED_CONTENT_CONFIG = {
80         ignoreControlWhileLoading: true,
81         controlPlacementBottom: true,
82     };
84     /**
85      * Get enrolled courses from backend.
86      *
87      * @param {object} filters The filters for this view.
88      * @param {int} limit The number of courses to show.
89      * @param {int} pageNumber The pagenumber to view.
90      * @return {promise} Resolved with an array of courses.
91      */
92     var getMyCourses = function(filters, limit, pageNumber) {
94         return Repository.getEnrolledCoursesByTimeline({
95             offset:  pageNumber * limit,
96             limit: limit,
97             classification: filters.grouping,
98             sort: filters.sort
99         });
100     };
102     /**
103      * Get the container element for the favourite icon.
104      *
105      * @param  {Object} root The course overview container
106      * @param  {Number} courseId Course id number
107      * @return {Object} The favourite icon container
108      */
109     var getFavouriteIconContainer = function(root, courseId) {
110         return root.find(SELECTORS.FAVOURITE_ICON + '[data-course-id="' + courseId + '"]');
111     };
113     /**
114      * Get the paged content container element.
115      *
116      * @param  {Object} root The course overview container
117      * @param  {Number} index Rendered page index.
118      * @return {Object} The rendered paged container.
119      */
120     var getPagedContentContainer = function(root, index) {
121         return root.find('[data-region="paged-content-page"][data-page="' + index + '"]');
122     };
124     /**
125      * Get the course id from a favourite element.
126      *
127      * @param {Object} root The favourite icon container element.
128      * @return {Number} Course id.
129      */
130     var getFavouriteCourseId = function(root) {
131         return root.attr('data-course-id');
132     };
134     /**
135      * Hide the favourite icon.
136      *
137      * @param {Object} root The favourite icon container element.
138      * @param  {Number} courseId Course id number.
139      */
140     var hideFavouriteIcon = function(root, courseId) {
141         var iconContainer = getFavouriteIconContainer(root, courseId);
142         var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
143         isFavouriteIcon.addClass('hidden');
144         isFavouriteIcon.attr('aria-hidden', true);
145         var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
146         notFavourteIcon.removeClass('hidden');
147         notFavourteIcon.attr('aria-hidden', false);
148     };
150     /**
151      * Show the favourite icon.
152      *
153      * @param  {Object} root The course overview container.
154      * @param  {Number} courseId Course id number.
155      */
156     var showFavouriteIcon = function(root, courseId) {
157         var iconContainer = getFavouriteIconContainer(root, courseId);
158         var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
159         isFavouriteIcon.removeClass('hidden');
160         isFavouriteIcon.attr('aria-hidden', false);
161         var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
162         notFavourteIcon.addClass('hidden');
163         notFavourteIcon.attr('aria-hidden', true);
164     };
166     /**
167      * Get the action menu item
168      *
169      * @param {Object} root  root The course overview container
170      * @param {Number} courseId Course id.
171      * @return {Object} The add to favourite menu item.
172      */
173     var getAddFavouriteMenuItem = function(root, courseId) {
174         return root.find('[data-action="add-favourite"][data-course-id="' + courseId + '"]');
175     };
177     /**
178      * Get the action menu item
179      *
180      * @param {Object} root  root The course overview container
181      * @param {Number} courseId Course id.
182      * @return {Object} The remove from favourites menu item.
183      */
184     var getRemoveFavouriteMenuItem = function(root, courseId) {
185         return root.find('[data-action="remove-favourite"][data-course-id="' + courseId + '"]');
186     };
188     /**
189      * Add course to favourites
190      *
191      * @param  {Object} root The course overview container
192      * @param  {Number} courseId Course id number
193      */
194     var addToFavourites = function(root, courseId) {
195         var removeAction = getRemoveFavouriteMenuItem(root, courseId);
196         var addAction = getAddFavouriteMenuItem(root, courseId);
198         setCourseFavouriteState(courseId, true).then(function(success) {
199             if (success) {
200                 // Trigger a JS event so the starred courses block can refresh the list of courses.
201                 $('body').trigger('myoverview-events:course_starred', [courseId]);
203                 removeAction.removeClass('hidden');
204                 addAction.addClass('hidden');
205                 showFavouriteIcon(root, courseId);
206             } else {
207                 Notification.alert('Starring course failed', 'Could not change favourite state');
208             }
209             return;
210         }).catch(Notification.exception);
211     };
213     /**
214      * Remove course from favourites
215      *
216      * @param  {Object} root The course overview container
217      * @param  {Number} courseId Course id number
218      */
219     var removeFromFavourites = function(root, courseId) {
220         var removeAction = getRemoveFavouriteMenuItem(root, courseId);
221         var addAction = getAddFavouriteMenuItem(root, courseId);
223         setCourseFavouriteState(courseId, false).then(function(success) {
224             if (success) {
225                 $('body').trigger('myoverview-events:course_unstarred', [courseId]);
226                 removeAction.addClass('hidden');
227                 addAction.removeClass('hidden');
228                 hideFavouriteIcon(root, courseId);
229             } else {
230                 Notification.alert('Starring course failed', 'Could not change favourite state');
231             }
232             return;
233         }).catch(Notification.exception);
234     };
236     /**
237      * Set the courses favourite status and push to repository
238      *
239      * @param  {Number} courseId Course id to favourite.
240      * @param  {Bool} status new favourite status.
241      * @return {Promise} Repository promise.
242      */
243     var setCourseFavouriteState = function(courseId, status) {
245         return Repository.setFavouriteCourses({
246             courses: [
247                     {
248                         'id': courseId,
249                         'favourite': status
250                     }
251                 ]
252         }).then(function(result) {
253             if (result.warnings.length == 0) {
254                 loadedPages.forEach(function(courseList) {
255                     courseList.courses.forEach(function(course, index) {
256                         if (course.id == courseId) {
257                             courseList.courses[index].isfavourite = status;
258                         }
259                     });
260                 });
261                 return true;
262             } else {
263                 return false;
264             }
265         }).catch(Notification.exception);
266     };
268     /**
269      * Render the dashboard courses.
270      *
271      * @param {object} root The root element for the courses view.
272      * @param {array} coursesData containing array of returned courses.
273      * @return {promise} jQuery promise resolved after rendering is complete.
274      */
275     var renderCourses = function(root, coursesData) {
277         var filters = getFilterValues(root);
279         var currentTemplate = '';
280         if (filters.display == 'cards') {
281             currentTemplate = TEMPLATES.COURSES_CARDS;
282         } else if (filters.display == 'list') {
283             currentTemplate = TEMPLATES.COURSES_LIST;
284         } else {
285             currentTemplate = TEMPLATES.COURSES_SUMMARY;
286         }
288         if (coursesData.courses.length) {
289             return Templates.render(currentTemplate, {
290                 courses: coursesData.courses
291             });
292         } else {
293             var nocoursesimg = root.attr('data-nocoursesimg');
294             return Templates.render(TEMPLATES.NOCOURSES, {
295                 nocoursesimg: nocoursesimg
296             });
297         }
298     };
300     /**
301      * Intialise the courses list and cards views on page load.
302      *
303      * @param {object} root The root element for the courses view.
304      * @param {object} content The content element for the courses view.
305      */
306     var init = function(root, content) {
308         root = $(root);
310         if (!root.attr('data-init')) {
311             registerEventListeners(root);
312             root.attr('data-init', true);
313         }
315         var filters = getFilterValues(root);
317         var pagedContentPromise = PagedContentFactory.createWithLimit(
318             NUMCOURSES_PERPAGE,
319             function(pagesData, actions) {
320                 var promises = [];
322                 pagesData.forEach(function(pageData) {
323                     var currentPage = pageData.pageNumber;
324                     var pageNumber = pageData.pageNumber - 1;
326                     var pagePromise = getMyCourses(
327                         filters,
328                         pageData.limit,
329                         pageNumber
330                     ).then(function(coursesData) {
331                         if (coursesData.courses.length < pageData.limit) {
332                             actions.allItemsLoaded(pageData.pageNumber);
333                         }
334                         loadedPages[currentPage] = coursesData;
335                         return renderCourses(root, coursesData);
336                     })
337                     .catch(Notification.exception);
339                     promises.push(pagePromise);
340                 });
342                 return promises;
343             },
344             DEFAULT_PAGED_CONTENT_CONFIG
345         );
347         pagedContentPromise.then(function(html, js) {
348             return Templates.replaceNodeContents(content, html, js);
349         }).catch(Notification.exception);
350     };
352     /**
353      * Listen to, and handle events for  the myoverview block.
354      *
355      * @param {Object} root The myoverview block container element.
356      */
357     var registerEventListeners = function(root) {
358         CustomEvents.define(root, [
359             CustomEvents.events.activate
360         ]);
362         root.on(CustomEvents.events.activate, SELECTORS.ACTION_ADD_FAVOURITE, function(e, data) {
363             var favourite = $(e.target).closest(SELECTORS.ACTION_ADD_FAVOURITE);
364             var courseId = getFavouriteCourseId(favourite);
365             addToFavourites(root, courseId);
366             data.originalEvent.preventDefault();
367         });
369         root.on(CustomEvents.events.activate, SELECTORS.ACTION_REMOVE_FAVOURITE, function(e, data) {
370             var favourite = $(e.target).closest(SELECTORS.ACTION_REMOVE_FAVOURITE);
371             var courseId = getFavouriteCourseId(favourite);
372             removeFromFavourites(root, courseId);
373             data.originalEvent.preventDefault();
374         });
376         root.on(CustomEvents.events.activate, SELECTORS.FAVOURITE_ICON, function(e, data) {
377             data.originalEvent.preventDefault();
378         });
379     };
381     /**
382      * Reset the courses views to their original
383      * state on first page load.
384      *
385      * This is called when configuration has changed for the event lists
386      * to cause them to reload their data.
387      *
388      * @param {Object} root The root element for the timeline view.
389      * @param {Object} content The content element for the timeline view.
390      */
391     var reset = function(root, content) {
393         if (loadedPages.length > 0) {
394             loadedPages.forEach(function(courseList, index) {
395                 var pagedContentPage = getPagedContentContainer(root, index);
396                 renderCourses(root, courseList).then(function(html, js) {
397                     return Templates.replaceNodeContents(pagedContentPage, html, js);
398                 }).catch(Notification.exception);
399             });
400         } else {
401             init(root, content);
402         }
403     };
405     return {
406         init: init,
407         reset: reset
408     };