MDL-64438 block_recentlyaccessedcourses: Optionally show course category
[moodle.git] / blocks / recentlyaccessedcourses / amd / src / main.js
CommitLineData
41f61293
VDF
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/>.
15
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 */
24
25define(
26 [
27 'jquery',
3edde4fb 28 'core/custom_interaction_events',
4f6680a1
VDF
29 'core/notification',
30 'core/pubsub',
3edde4fb
RW
31 'core/paged_content_paging_bar',
32 'core/templates',
33 'core_course/events',
34 'core_course/repository',
41f61293
VDF
35 ],
36 function(
37 $,
3edde4fb 38 CustomEvents,
4f6680a1
VDF
39 Notification,
40 PubSub,
3edde4fb
RW
41 PagedContentPagingBar,
42 Templates,
43 CourseEvents,
44 CoursesRepository
41f61293
VDF
45 ) {
46
3edde4fb
RW
47 // Constants.
48 var NUM_COURSES_TOTAL = 10;
41f61293 49 var SELECTORS = {
d0b237b3 50 BLOCK_CONTAINER: '[data-region="recentlyaccessedcourses"]',
3edde4fb 51 CARD_CONTAINER: '[data-region="card-deck"]',
6d972762 52 COURSE_IS_FAVOURITE: '[data-region="is-favourite"]',
3edde4fb
RW
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"]'
41f61293 59 };
3edde4fb
RW
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;
41f61293
VDF
67
68 /**
6d972762 69 * Show the empty message when no course are found.
41f61293 70 *
6d972762
RW
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');
3edde4fb
RW
75 root.find(SELECTORS.LOADING_PLACEHOLDER).addClass('hidden');
76 root.find(SELECTORS.CONTENT).addClass('hidden');
77 };
78
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 };
89
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 };
101
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');
6d972762
RW
112 };
113
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.
41f61293 119 */
6d972762 120 var favouriteCourse = function(root, courseId) {
3edde4fb
RW
121 allCourses.forEach(function(course) {
122 if (course.attr('data-course-id') == courseId) {
123 course.find(SELECTORS.COURSE_IS_FAVOURITE).removeClass('hidden');
124 }
125 });
6d972762
RW
126 };
127
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) {
3edde4fb
RW
135 allCourses.forEach(function(course) {
136 if (course.attr('data-course-id') == courseId) {
137 course.find(SELECTORS.COURSE_IS_FAVOURITE).addClass('hidden');
138 }
139 });
41f61293
VDF
140 };
141
142 /**
3edde4fb 143 * Render the a list of courses.
41f61293 144 *
3edde4fb
RW
145 * @param {array} courses containing array of courses.
146 * @return {promise} Resolved with list of rendered courses as jQuery objects.
41f61293 147 */
3edde4fb 148 var renderAllCourses = function(courses) {
d0b237b3 149 var showcoursecategory = $(SELECTORS.BLOCK_CONTAINER).data('displaycoursecategory');
3edde4fb 150 var promises = courses.map(function(course) {
d0b237b3 151 course.showcoursecategory = showcoursecategory;
3edde4fb
RW
152 return Templates.render('block_recentlyaccessedcourses/course-card', course);
153 });
154
155 return $.when.apply(null, promises).then(function() {
156 var renderedCourses = [];
157
158 promises.forEach(function(promise) {
159 promise.then(function(html) {
160 renderedCourses.push($(html));
161 return;
162 })
163 .catch(Notification.exception);
164 });
165
166 return renderedCourses;
6d972762 167 });
41f61293
VDF
168 };
169
170 /**
4f6680a1 171 * Fetch user's recently accessed courses and reload the content of the block.
41f61293 172 *
4f6680a1 173 * @param {int} userid User whose courses will be shown
4f6680a1 174 * @returns {promise} The updated content for the block.
41f61293 175 */
3edde4fb
RW
176 var loadContent = function(userid) {
177 return CoursesRepository.getLastAccessedCourses(userid, NUM_COURSES_TOTAL)
6d972762 178 .then(function(courses) {
3edde4fb
RW
179 return renderAllCourses(courses);
180 });
181 };
182
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;
193
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 }
200
201 availableVisibleCards = Math.floor(availableWidth / cardWidth);
202
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 }
210
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 }, '');
216
3accb67b
RW
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 }
225
3edde4fb
RW
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;
231
232 if (availableVisibleCards >= allCourses.length) {
233 hidePagingBar(root);
234 } else {
235 showPagingBar(root);
236
237 if (viewIndex === 0) {
238 PagedContentPagingBar.disablePreviousControlButtons(pagingBar);
6d972762 239 } else {
3edde4fb 240 PagedContentPagingBar.enablePreviousControlButtons(pagingBar);
6d972762 241 }
3edde4fb
RW
242
243 if (viewIndex + availableVisibleCards >= allCourses.length) {
244 PagedContentPagingBar.disableNextControlButtons(pagingBar);
245 } else {
246 PagedContentPagingBar.enableNextControlButtons(pagingBar);
247 }
248 }
249 }
41f61293
VDF
250 };
251
4f6680a1
VDF
252 /**
253 * Register event listeners for the block.
254 *
4f6680a1
VDF
255 * @param {object} root The root element for the recentlyaccessedcourses block.
256 */
6d972762 257 var registerEventListeners = function(root) {
3edde4fb
RW
258 var resizeTimeout = null;
259 var drawerToggling = false;
260
6d972762
RW
261 PubSub.subscribe(CourseEvents.favourited, function(courseId) {
262 favouriteCourse(root, courseId);
4f6680a1
VDF
263 });
264
6d972762
RW
265 PubSub.subscribe(CourseEvents.unfavorited, function(courseId) {
266 unfavouriteCourse(root, courseId);
4f6680a1 267 });
3edde4fb
RW
268
269 PubSub.subscribe('nav-drawer-toggle-start', function() {
270 if (!contentLoaded || !allCourses.length || drawerToggling) {
271 // Nothing to recalculate.
272 return;
273 }
274
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++;
283
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 };
291
292 // Start the recalculations.
293 doRecalculation(root);
294 });
295
296 PubSub.subscribe('nav-drawer-toggle-end', function() {
297 drawerToggling = false;
298 });
299
300 $(window).on('resize', function() {
301 if (!contentLoaded || !allCourses.length) {
302 // Nothing to reclculate.
303 return;
304 }
305
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 });
316
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 }
324
325 data.originalEvent.preventDefault();
326 });
327
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 }
335
336 data.originalEvent.preventDefault();
337 });
4f6680a1
VDF
338 };
339
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);
348
6d972762 349 registerEventListeners(root);
3edde4fb
RW
350 loadContent(userid)
351 .then(function(renderedCourses) {
352 allCourses = renderedCourses;
353 contentLoaded = true;
354
355 if (allCourses.length) {
356 showContent(root);
357 recalculateVisibleCourses(root);
358 } else {
359 showEmptyMessage(root);
360 }
361
362 return;
363 })
364 .catch(Notification.exception);
4f6680a1
VDF
365 };
366
41f61293
VDF
367 return {
368 init: init
369 };
370 });