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