MDL-64438 block_recentlyaccessedcourses: Optionally show course category
[moodle.git] / blocks / recentlyaccessedcourses / amd / src / main.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  * Javascript to initialise the Recently accessed courses block.
18  *
19  * @module     block_recentlyaccessedcourses/main.js
20  * @package    block_recentlyaccessedcourses
21  * @copyright  2018 Victor Deniz <victor@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 define(
26     [
27         'jquery',
28         'core/custom_interaction_events',
29         'core/notification',
30         'core/pubsub',
31         'core/paged_content_paging_bar',
32         'core/templates',
33         'core_course/events',
34         'core_course/repository',
35     ],
36     function(
37         $,
38         CustomEvents,
39         Notification,
40         PubSub,
41         PagedContentPagingBar,
42         Templates,
43         CourseEvents,
44         CoursesRepository
45     ) {
47         // Constants.
48         var NUM_COURSES_TOTAL = 10;
49         var SELECTORS = {
50             BLOCK_CONTAINER: '[data-region="recentlyaccessedcourses"]',
51             CARD_CONTAINER: '[data-region="card-deck"]',
52             COURSE_IS_FAVOURITE: '[data-region="is-favourite"]',
53             CONTENT: '[data-region="view-content"]',
54             EMPTY_MESSAGE: '[data-region="empty-message"]',
55             LOADING_PLACEHOLDER: '[data-region="loading-placeholder"]',
56             PAGING_BAR: '[data-region="paging-bar"]',
57             PAGING_BAR_NEXT: '[data-control="next"]',
58             PAGING_BAR_PREVIOUS: '[data-control="previous"]'
59         };
60         // Module variables.
61         var contentLoaded = false;
62         var allCourses = [];
63         var visibleCoursesId = null;
64         var cardWidth = null;
65         var viewIndex = 0;
66         var availableVisibleCards = 1;
68         /**
69          * Show the empty message when no course are found.
70          *
71          * @param {object} root The root element for the courses view.
72          */
73         var showEmptyMessage = function(root) {
74             root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
75             root.find(SELECTORS.LOADING_PLACEHOLDER).addClass('hidden');
76             root.find(SELECTORS.CONTENT).addClass('hidden');
77         };
79         /**
80          * Show the empty message when no course are found.
81          *
82          * @param {object} root The root element for the courses view.
83          */
84         var showContent = function(root) {
85             root.find(SELECTORS.CONTENT).removeClass('hidden');
86             root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');
87             root.find(SELECTORS.LOADING_PLACEHOLDER).addClass('hidden');
88         };
90         /**
91          * Show the paging bar.
92          *
93          * @param {object} root The root element for the courses view.
94          */
95         var showPagingBar = function(root) {
96             var pagingBar = root.find(SELECTORS.PAGING_BAR);
97             pagingBar.css('opacity', 1);
98             pagingBar.css('visibility', 'visible');
99             pagingBar.attr('aria-hidden', 'false');
100         };
102         /**
103          * Hide the paging bar.
104          *
105          * @param {object} root The root element for the courses view.
106          */
107         var hidePagingBar = function(root) {
108             var pagingBar = root.find(SELECTORS.PAGING_BAR);
109             pagingBar.css('opacity', 0);
110             pagingBar.css('visibility', 'hidden');
111             pagingBar.attr('aria-hidden', 'true');
112         };
114         /**
115          * Show the favourite indicator for the given course (if it's in the list).
116          *
117          * @param {object} root The root element for the courses view.
118          * @param {number} courseId The id of the course to be favourited.
119          */
120         var favouriteCourse = function(root, courseId) {
121             allCourses.forEach(function(course) {
122                 if (course.attr('data-course-id') == courseId) {
123                     course.find(SELECTORS.COURSE_IS_FAVOURITE).removeClass('hidden');
124                 }
125             });
126         };
128         /**
129          * Hide the favourite indicator for the given course (if it's in the list).
130          *
131          * @param {object} root The root element for the courses view.
132          * @param {number} courseId The id of the course to be unfavourited.
133          */
134         var unfavouriteCourse = function(root, courseId) {
135             allCourses.forEach(function(course) {
136                 if (course.attr('data-course-id') == courseId) {
137                     course.find(SELECTORS.COURSE_IS_FAVOURITE).addClass('hidden');
138                 }
139             });
140         };
142         /**
143          * Render the a list of courses.
144          *
145          * @param {array} courses containing array of courses.
146          * @return {promise} Resolved with list of rendered courses as jQuery objects.
147          */
148         var renderAllCourses = function(courses) {
149             var showcoursecategory = $(SELECTORS.BLOCK_CONTAINER).data('displaycoursecategory');
150             var promises = courses.map(function(course) {
151                 course.showcoursecategory = showcoursecategory;
152                 return Templates.render('block_recentlyaccessedcourses/course-card', course);
153             });
155             return $.when.apply(null, promises).then(function() {
156                 var renderedCourses = [];
158                 promises.forEach(function(promise) {
159                     promise.then(function(html) {
160                         renderedCourses.push($(html));
161                         return;
162                     })
163                     .catch(Notification.exception);
164                 });
166                 return renderedCourses;
167             });
168         };
170         /**
171          * Fetch user's recently accessed courses and reload the content of the block.
172          *
173          * @param {int} userid User whose courses will be shown
174          * @returns {promise} The updated content for the block.
175          */
176         var loadContent = function(userid) {
177             return CoursesRepository.getLastAccessedCourses(userid, NUM_COURSES_TOTAL)
178                 .then(function(courses) {
179                     return renderAllCourses(courses);
180                 });
181         };
183         /**
184          * Recalculate the number of courses that should be visible.
185          *
186          * @param {object} root The root element for the courses view.
187          */
188         var recalculateVisibleCourses = function(root) {
189             var container = root.find(SELECTORS.CONTENT).find(SELECTORS.CARD_CONTAINER);
190             var availableWidth = parseFloat(root.css('width'));
191             var numberOfCourses = allCourses.length;
192             var start = 0;
194             if (!cardWidth) {
195                 container.html(allCourses[0]);
196                 // Render one card initially to calculate the width of the cards
197                 // including the margins.
198                 cardWidth = allCourses[0].outerWidth(true);
199             }
201             availableVisibleCards = Math.floor(availableWidth / cardWidth);
203             if (viewIndex + availableVisibleCards < numberOfCourses) {
204                 start = viewIndex;
205             } else {
206                 var overflow = (viewIndex + availableVisibleCards) - numberOfCourses;
207                 start = viewIndex - overflow;
208                 start = start >= 0 ? start : 0;
209             }
211             var coursesToShow = allCourses.slice(start, start + availableVisibleCards);
212             // Create an id for the list of courses we expect to be displayed.
213             var newVisibleCoursesId = coursesToShow.reduce(function(carry, course) {
214                 return carry + course.attr('data-course-id');
215             }, '');
217             // Centre the courses if we have an overflow of courses.
218             if (allCourses.length > coursesToShow.length) {
219                 container.addClass('justify-content-center');
220                 container.removeClass('justify-content-start');
221             } else {
222                 container.removeClass('justify-content-center');
223                 container.addClass('justify-content-start');
224             }
226             // Don't bother updating the DOM unless the visible courses have changed.
227             if (visibleCoursesId != newVisibleCoursesId) {
228                 var pagingBar = root.find(PagedContentPagingBar.rootSelector);
229                 container.html(coursesToShow);
230                 visibleCoursesId = newVisibleCoursesId;
232                 if (availableVisibleCards >= allCourses.length) {
233                     hidePagingBar(root);
234                 } else {
235                     showPagingBar(root);
237                     if (viewIndex === 0) {
238                         PagedContentPagingBar.disablePreviousControlButtons(pagingBar);
239                     } else {
240                         PagedContentPagingBar.enablePreviousControlButtons(pagingBar);
241                     }
243                     if (viewIndex + availableVisibleCards >= allCourses.length) {
244                         PagedContentPagingBar.disableNextControlButtons(pagingBar);
245                     } else {
246                         PagedContentPagingBar.enableNextControlButtons(pagingBar);
247                     }
248                 }
249             }
250         };
252         /**
253          * Register event listeners for the block.
254          *
255          * @param {object} root The root element for the recentlyaccessedcourses block.
256          */
257         var registerEventListeners = function(root) {
258             var resizeTimeout = null;
259             var drawerToggling = false;
261             PubSub.subscribe(CourseEvents.favourited, function(courseId) {
262                 favouriteCourse(root, courseId);
263             });
265             PubSub.subscribe(CourseEvents.unfavorited, function(courseId) {
266                 unfavouriteCourse(root, courseId);
267             });
269             PubSub.subscribe('nav-drawer-toggle-start', function() {
270                 if (!contentLoaded || !allCourses.length || drawerToggling) {
271                     // Nothing to recalculate.
272                     return;
273                 }
275                 drawerToggling = true;
276                 var recalculationCount = 0;
277                 // This function is going to recalculate the number of courses while
278                 // the nav drawer is opening or closes (up to a maximum of 5 recalcs).
279                 var doRecalculation = function() {
280                     setTimeout(function() {
281                         recalculateVisibleCourses(root);
282                         recalculationCount++;
284                         if (recalculationCount < 5 && drawerToggling) {
285                             // If we haven't done too many recalculations and the drawer
286                             // is still toggling then recurse.
287                             doRecalculation();
288                         }
289                     }, 100);
290                 };
292                 // Start the recalculations.
293                 doRecalculation(root);
294             });
296             PubSub.subscribe('nav-drawer-toggle-end', function() {
297                 drawerToggling = false;
298             });
300             $(window).on('resize', function() {
301                 if (!contentLoaded || !allCourses.length) {
302                     // Nothing to reclculate.
303                     return;
304                 }
306                 // Resize events fire rapidly so recalculating the visible courses each
307                 // time can be expensive. Let's debounce them,
308                 if (!resizeTimeout) {
309                     resizeTimeout = setTimeout(function() {
310                         resizeTimeout = null;
311                         recalculateVisibleCourses(root);
312                     // The recalculateVisibleCourses function will execute at a rate of 15fps.
313                     }, 66);
314                 }
315             });
317             CustomEvents.define(root, [CustomEvents.events.activate]);
318             root.on(CustomEvents.events.activate, SELECTORS.PAGING_BAR_NEXT, function(e, data) {
319                 var button = $(e.target).closest(SELECTORS.PAGING_BAR_NEXT);
320                 if (!button.hasClass('disabled')) {
321                     viewIndex = viewIndex + availableVisibleCards;
322                     recalculateVisibleCourses(root);
323                 }
325                 data.originalEvent.preventDefault();
326             });
328             root.on(CustomEvents.events.activate, SELECTORS.PAGING_BAR_PREVIOUS, function(e, data) {
329                 var button = $(e.target).closest(SELECTORS.PAGING_BAR_PREVIOUS);
330                 if (!button.hasClass('disabled')) {
331                     viewIndex = viewIndex - availableVisibleCards;
332                     viewIndex = viewIndex < 0 ? 0 : viewIndex;
333                     recalculateVisibleCourses(root);
334                 }
336                 data.originalEvent.preventDefault();
337             });
338         };
340         /**
341          * Get and show the recent courses into the block.
342          *
343          * @param {int} userid User from which the courses will be obtained
344          * @param {object} root The root element for the recentlyaccessedcourses block.
345          */
346         var init = function(userid, root) {
347             root = $(root);
349             registerEventListeners(root);
350             loadContent(userid)
351                 .then(function(renderedCourses) {
352                     allCourses = renderedCourses;
353                     contentLoaded = true;
355                     if (allCourses.length) {
356                         showContent(root);
357                         recalculateVisibleCourses(root);
358                     } else {
359                         showEmptyMessage(root);
360                     }
362                     return;
363                 })
364                 .catch(Notification.exception);
365         };
367         return {
368             init: init
369         };
370     });