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