MDL-63457 block_myoverview: Hide courses from individual overview blocks
[moodle.git] / blocks / myoverview / amd / src / view.js
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/>.
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(
25 [
26     'jquery',
27     'block_myoverview/repository',
28     'core/paged_content_factory',
29     'core/custom_interaction_events',
30     'core/notification',
31     'core/ajax',
32     'core/templates',
33 ],
34 function(
35     $,
36     Repository,
37     PagedContentFactory,
38     CustomEvents,
39     Notification,
40     Ajax,
41     Templates
42 ) {
44     var SELECTORS = {
45         COURSE_REGION: '[data-region="course-view-content"]',
46         ACTION_HIDE_COURSE: '[data-action="hide-course"]',
47         ACTION_SHOW_COURSE: '[data-action="show-course"]',
48         ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
49         ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
50         FAVOURITE_ICON: '[data-region="favourite-icon"]',
51         ICON_IS_FAVOURITE: '[data-region="is-favourite"]',
52         ICON_NOT_FAVOURITE: '[data-region="not-favourite"]',
53         PAGED_CONTENT_CONTAINER: '[data-region="page-container"]'
55     };
57     var TEMPLATES = {
58         COURSES_CARDS: 'block_myoverview/view-cards',
59         COURSES_LIST: 'block_myoverview/view-list',
60         COURSES_SUMMARY: 'block_myoverview/view-summary',
61         NOCOURSES: 'block_myoverview/no-courses'
62     };
64     var NUMCOURSES_PERPAGE = [12, 24, 48];
66     var loadedPages = [];
68     /**
69      * Get filter values from DOM.
70      *
71      * @param {object} root The root element for the courses view.
72      * @return {filters} Set filters.
73      */
74     var getFilterValues = function(root) {
75         var filters = {};
76         filters.display = root.attr('data-display');
77         filters.grouping = root.attr('data-grouping');
78         filters.sort = root.attr('data-sort');
79         return filters;
80     };
82     // We want the paged content controls below the paged content area.
83     // and the controls should be ignored while data is loading.
84     var DEFAULT_PAGED_CONTENT_CONFIG = {
85         ignoreControlWhileLoading: true,
86         controlPlacementBottom: true,
87     };
89     /**
90      * Get enrolled courses from backend.
91      *
92      * @param {object} filters The filters for this view.
93      * @param {int} limit The number of courses to show.
94      * @param {int} pageNumber The pagenumber to view.
95      * @return {promise} Resolved with an array of courses.
96      */
97     var getMyCourses = function(filters, limit, pageNumber) {
99         return Repository.getEnrolledCoursesByTimeline({
100             offset:  pageNumber * limit,
101             limit: limit,
102             classification: filters.grouping,
103             sort: filters.sort
104         });
105     };
107     /**
108      * Get the container element for the favourite icon.
109      *
110      * @param  {Object} root The course overview container
111      * @param  {Number} courseId Course id number
112      * @return {Object} The favourite icon container
113      */
114     var getFavouriteIconContainer = function(root, courseId) {
115         return root.find(SELECTORS.FAVOURITE_ICON + '[data-course-id="' + courseId + '"]');
116     };
118     /**
119      * Get the paged content container element.
120      *
121      * @param  {Object} root The course overview container
122      * @param  {Number} index Rendered page index.
123      * @return {Object} The rendered paged container.
124      */
125     var getPagedContentContainer = function(root, index) {
126         return root.find('[data-region="paged-content-page"][data-page="' + index + '"]');
127     };
129     /**
130      * Get the course id from a favourite element.
131      *
132      * @param {Object} root The favourite icon container element.
133      * @return {Number} Course id.
134      */
135     var getCourseId = function(root) {
136         return root.attr('data-course-id');
137     };
139     /**
140      * Hide the favourite icon.
141      *
142      * @param {Object} root The favourite icon container element.
143      * @param  {Number} courseId Course id number.
144      */
145     var hideFavouriteIcon = function(root, courseId) {
146         var iconContainer = getFavouriteIconContainer(root, courseId);
147         var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
148         isFavouriteIcon.addClass('hidden');
149         isFavouriteIcon.attr('aria-hidden', true);
150         var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
151         notFavourteIcon.removeClass('hidden');
152         notFavourteIcon.attr('aria-hidden', false);
153     };
155     /**
156      * Show the favourite icon.
157      *
158      * @param  {Object} root The course overview container.
159      * @param  {Number} courseId Course id number.
160      */
161     var showFavouriteIcon = function(root, courseId) {
162         var iconContainer = getFavouriteIconContainer(root, courseId);
163         var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
164         isFavouriteIcon.removeClass('hidden');
165         isFavouriteIcon.attr('aria-hidden', false);
166         var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
167         notFavourteIcon.addClass('hidden');
168         notFavourteIcon.attr('aria-hidden', true);
169     };
171     /**
172      * Get the action menu item
173      *
174      * @param {Object} root  root The course overview container
175      * @param {Number} courseId Course id.
176      * @return {Object} The add to favourite menu item.
177      */
178     var getAddFavouriteMenuItem = function(root, courseId) {
179         return root.find('[data-action="add-favourite"][data-course-id="' + courseId + '"]');
180     };
182     /**
183      * Get the action menu item
184      *
185      * @param {Object} root  root The course overview container
186      * @param {Number} courseId Course id.
187      * @return {Object} The remove from favourites menu item.
188      */
189     var getRemoveFavouriteMenuItem = function(root, courseId) {
190         return root.find('[data-action="remove-favourite"][data-course-id="' + courseId + '"]');
191     };
193     /**
194      * Add course to favourites
195      *
196      * @param  {Object} root The course overview container
197      * @param  {Number} courseId Course id number
198      */
199     var addToFavourites = function(root, courseId) {
200         var removeAction = getRemoveFavouriteMenuItem(root, courseId);
201         var addAction = getAddFavouriteMenuItem(root, courseId);
203         setCourseFavouriteState(courseId, true).then(function(success) {
204             if (success) {
205                 removeAction.removeClass('hidden');
206                 addAction.addClass('hidden');
207                 showFavouriteIcon(root, courseId);
208             } else {
209                 Notification.alert('Starring course failed', 'Could not change favourite state');
210             }
211             return;
212         }).catch(Notification.exception);
213     };
215     /**
216      * Remove course from favourites
217      *
218      * @param  {Object} root The course overview container
219      * @param  {Number} courseId Course id number
220      */
221     var removeFromFavourites = function(root, courseId) {
222         var removeAction = getRemoveFavouriteMenuItem(root, courseId);
223         var addAction = getAddFavouriteMenuItem(root, courseId);
225         setCourseFavouriteState(courseId, false).then(function(success) {
226             if (success) {
227                 removeAction.addClass('hidden');
228                 addAction.removeClass('hidden');
229                 hideFavouriteIcon(root, courseId);
230             } else {
231                 Notification.alert('Starring course failed', 'Could not change favourite state');
232             }
233             return;
234         }).catch(Notification.exception);
235     };
237     /**
238      * Set the courses favourite status and push to repository
239      *
240      * @param  {Number} courseId Course id to favourite.
241      * @param  {Bool} status new favourite status.
242      * @return {Promise} Repository promise.
243      */
244     var setCourseFavouriteState = function(courseId, status) {
246         return Repository.setFavouriteCourses({
247             courses: [
248                     {
249                         'id': courseId,
250                         'favourite': status
251                     }
252                 ]
253         }).then(function(result) {
254             if (result.warnings.length == 0) {
255                 loadedPages.forEach(function(courseList) {
256                     courseList.courses.forEach(function(course, index) {
257                         if (course.id == courseId) {
258                             courseList.courses[index].isfavourite = status;
259                         }
260                     });
261                 });
262                 return true;
263             } else {
264                 return false;
265             }
266         }).catch(Notification.exception);
267     };
269     /**
270      * Render the dashboard courses.
271      *
272      * @param {object} root The root element for the courses view.
273      * @param {array} coursesData containing array of returned courses.
274      * @return {promise} jQuery promise resolved after rendering is complete.
275      */
276     var renderCourses = function(root, coursesData) {
278         var filters = getFilterValues(root);
280         var currentTemplate = '';
281         if (filters.display == 'cards') {
282             currentTemplate = TEMPLATES.COURSES_CARDS;
283         } else if (filters.display == 'list') {
284             currentTemplate = TEMPLATES.COURSES_LIST;
285         } else {
286             currentTemplate = TEMPLATES.COURSES_SUMMARY;
287         }
289         if (coursesData.courses.length) {
290             return Templates.render(currentTemplate, {
291                 courses: coursesData.courses
292             });
293         } else {
294             var nocoursesimg = root.attr('data-nocoursesimg');
295             return Templates.render(TEMPLATES.NOCOURSES, {
296                 nocoursesimg: nocoursesimg
297             });
298         }
299     };
301     /**
302      * Intialise the paged list and cards views on page load.
303      *
304      * @param {object} root The root element for the courses view.
305      * @param {object} content The content element for the courses view.
306      */
307     var initializePagedContent = function(root, content) {
308         var filters = getFilterValues(root);
310         var pagedContentPromise = PagedContentFactory.createWithLimit(
311             NUMCOURSES_PERPAGE,
312             function(pagesData, actions) {
313                 var promises = [];
315                 pagesData.forEach(function(pageData) {
316                     var currentPage = pageData.pageNumber;
317                     var pageNumber = pageData.pageNumber - 1;
319                     var pagePromise = getMyCourses(
320                         filters,
321                         pageData.limit,
322                         pageNumber
323                     ).then(function(coursesData) {
324                         if (coursesData.courses.length < pageData.limit) {
325                             actions.allItemsLoaded(pageData.pageNumber);
326                         }
327                         loadedPages[currentPage] = coursesData;
328                         return renderCourses(root, coursesData);
329                     })
330                         .catch(Notification.exception);
332                     promises.push(pagePromise);
333                 });
335                 return promises;
336             },
337             DEFAULT_PAGED_CONTENT_CONFIG
338         );
340         pagedContentPromise.then(function(html, js) {
341             return Templates.replaceNodeContents(content, html, js);
342         }).catch(Notification.exception);
343     };
345     /**
346      * Listen to, and handle events for  the myoverview block.
347      *
348      * @param {Object} root The myoverview block container element.
349      */
350     var registerEventListeners = function(root, content) {
351         CustomEvents.define(root, [
352             CustomEvents.events.activate
353         ]);
355         root.on(CustomEvents.events.activate, SELECTORS.ACTION_ADD_FAVOURITE, function(e, data) {
356             var favourite = $(e.target).closest(SELECTORS.ACTION_ADD_FAVOURITE);
357             var courseId = getCourseId(favourite);
358             addToFavourites(root, courseId);
359             data.originalEvent.preventDefault();
360         });
362         root.on(CustomEvents.events.activate, SELECTORS.ACTION_REMOVE_FAVOURITE, function(e, data) {
363             var favourite = $(e.target).closest(SELECTORS.ACTION_REMOVE_FAVOURITE);
364             var courseId = getCourseId(favourite);
365             removeFromFavourites(root, courseId);
366             data.originalEvent.preventDefault();
367         });
369         root.on(CustomEvents.events.activate, SELECTORS.FAVOURITE_ICON, function(e, data) {
370             data.originalEvent.preventDefault();
371         });
373         root.on(CustomEvents.events.activate, SELECTORS.ACTION_HIDE_COURSE, function(e, data) {
374             var target = $(e.target).closest(SELECTORS.ACTION_HIDE_COURSE);
375             var id = getCourseId(target);
377             var request = {
378                 preferences: [
379                     {
380                         type: 'block_myoverview_hidden_course_' + id,
381                         value: true
382                     }
383                 ]
384             };
385             Repository.updateUserPreferences(request);
387             // Reload the paged content based on the hidden course
388             initializePagedContent(root, content);
389             data.originalEvent.preventDefault();
390         });
392         root.on(CustomEvents.events.activate, SELECTORS.ACTION_SHOW_COURSE, function(e, data) {
393             var target = $(e.target).closest(SELECTORS.ACTION_SHOW_COURSE);
394             var id = getCourseId(target);
396             var request = {
397                 preferences: [
398                     {
399                         type: 'block_myoverview_hidden_course_' + id,
400                         value: null
401                     }
402                 ]
403             };
405             Repository.updateUserPreferences(request);
407             // Reload the paged content based on the hidden course
408             initializePagedContent(root, content);
409             data.originalEvent.preventDefault();
410         });
411     };
413     /**
414      * Intialise the courses list and cards views on page load.
415      * 
416      * @param {object} root The root element for the courses view.
417      * @param {object} content The content element for the courses view.
418      */
419     var init = function(root, content) {
421         root = $(root);
423         if (!root.attr('data-init')) {
424             registerEventListeners(root, content);
425             root.attr('data-init', true);
426         }
428         initializePagedContent(root, content);
429     };
431     /**
433      * Reset the courses views to their original
434      * state on first page load.
435      *
436      * This is called when configuration has changed for the event lists
437      * to cause them to reload their data.
438      *
439      * @param {Object} root The root element for the timeline view.
440      * @param {Object} content The content element for the timeline view.
441      */
442     var reset = function(root, content) {
444         if (loadedPages.length > 0) {
445             loadedPages.forEach(function(courseList, index) {
446                 var pagedContentPage = getPagedContentContainer(root, index);
447                 renderCourses(root, courseList).then(function(html, js) {
448                     return Templates.replaceNodeContents(pagedContentPage, html, js);
449                 }).catch(Notification.exception);
450             });
451         } else {
452             init(root, content);
453         }
454     };
456     return {
457         init: init,
458         reset: reset
459     };
460 });