f4304fadb369b445c10225503c408bb9672c923c
[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/templates',
32 ],
33 function(
34     $,
35     Repository,
36     PagedContentFactory,
37     CustomEvents,
38     Notification,
39     Templates
40 ) {
42     var SELECTORS = {
43         ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
44         ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
45         FAVOURITE_ICON: '[data-region="favourite-icon"]',
46         ICON_IS_FAVOURITE: '[data-region="is-favourite"]',
47         ICON_NOT_FAVOURITE: '[data-region="not-favourite"]',
48         PAGED_CONTENT_CONTAINER: '[data-region="page-container"]'
50     };
52     var TEMPLATES = {
53         COURSES_CARDS: 'block_myoverview/view-cards',
54         COURSES_LIST: 'block_myoverview/view-list',
55         COURSES_SUMMARY: 'block_myoverview/view-summary',
56         NOCOURSES: 'block_myoverview/no-courses'
57     };
59     var NUMCOURSES_PERPAGE = [12, 24, 48];
61     var loadedPages = [];
63     /**
64      * Get filter values from DOM.
65      *
66      * @param {object} root The root element for the courses view.
67      * @return {filters} Set filters.
68      */
69     var getFilterValues = function(root) {
70         var filters = {};
71         filters.display = root.attr('data-display');
72         filters.grouping = root.attr('data-grouping');
73         filters.sort = root.attr('data-sort');
74         return filters;
75     };
77     // We want the paged content controls below the paged content area.
78     // and the controls should be ignored while data is loading.
79     var DEFAULT_PAGED_CONTENT_CONFIG = {
80         ignoreControlWhileLoading: true,
81         controlPlacementBottom: true,
82     };
84     /**
85      * Get enrolled courses from backend.
86      *
87      * @param {object} filters The filters for this view.
88      * @param {int} limit The number of courses to show.
89      * @param {int} pageNumber The pagenumber to view.
90      * @return {promise} Resolved with an array of courses.
91      */
92     var getMyCourses = function(filters, limit, pageNumber) {
94         return Repository.getEnrolledCoursesByTimeline({
95             offset:  pageNumber * limit,
96             limit: limit,
97             classification: filters.grouping,
98             sort: filters.sort
99         });
100     };
102     /**
103      * Get the container element for the favourite icon.
104      *
105      * @param  {Object} root The course overview container
106      * @param  {Number} courseId Course id number
107      * @return {Object} The favourite icon container
108      */
109     var getFavouriteIconContainer = function(root, courseId) {
110         return root.find(SELECTORS.FAVOURITE_ICON + '[data-course-id="' + courseId + '"]');
111     };
113     /**
114      * Get the paged content container element.
115      *
116      * @param  {Object} root The course overview container
117      * @param  {Number} index Rendered page index.
118      * @return {Object} The rendered paged container.
119      */
120     var getPagedContentContainer = function(root, index) {
121         return root.find('[data-region="paged-content-page"][data-page="' + index + '"]');
122     };
124     /**
125      * Get the course id from a favourite element.
126      *
127      * @param {Object} root The favourite icon container element.
128      * @return {Number} Course id.
129      */
130     var getFavouriteCourseId = function(root) {
131         return root.attr('data-course-id');
132     };
134     /**
135      * Hide the favourite icon.
136      *
137      * @param {Object} root The favourite icon container element.
138      * @param  {Number} courseId Course id number.
139      */
140     var hideFavouriteIcon = function(root, courseId) {
141         var iconContainer = getFavouriteIconContainer(root, courseId);
142         var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
143         isFavouriteIcon.addClass('hidden');
144         isFavouriteIcon.attr('aria-hidden', true);
145         var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
146         notFavourteIcon.removeClass('hidden');
147         notFavourteIcon.attr('aria-hidden', false);
148     };
150     /**
151      * Show the favourite icon.
152      *
153      * @param  {Object} root The course overview container.
154      * @param  {Number} courseId Course id number.
155      */
156     var showFavouriteIcon = function(root, courseId) {
157         var iconContainer = getFavouriteIconContainer(root, courseId);
158         var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
159         isFavouriteIcon.removeClass('hidden');
160         isFavouriteIcon.attr('aria-hidden', false);
161         var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
162         notFavourteIcon.addClass('hidden');
163         notFavourteIcon.attr('aria-hidden', true);
164     };
166     /**
167      * Get the action menu item
168      *
169      * @param {Object} root  root The course overview container
170      * @param {Number} courseId Course id.
171      * @return {Object} The add to favourite menu item.
172      */
173     var getAddFavouriteMenuItem = function(root, courseId) {
174         return root.find('[data-action="add-favourite"][data-course-id="' + courseId + '"]');
175     };
177     /**
178      * Get the action menu item
179      *
180      * @param {Object} root  root The course overview container
181      * @param {Number} courseId Course id.
182      * @return {Object} The remove from favourites menu item.
183      */
184     var getRemoveFavouriteMenuItem = function(root, courseId) {
185         return root.find('[data-action="remove-favourite"][data-course-id="' + courseId + '"]');
186     };
188     /**
189      * Add course to favourites
190      *
191      * @param  {Object} root The course overview container
192      * @param  {Number} courseId Course id number
193      */
194     var addToFavourites = function(root, courseId) {
195         var removeAction = getRemoveFavouriteMenuItem(root, courseId);
196         var addAction = getAddFavouriteMenuItem(root, courseId);
198         setCourseFavouriteState(courseId, true).then(function(success) {
199             if (success) {
200                 removeAction.removeClass('hidden');
201                 addAction.addClass('hidden');
202                 showFavouriteIcon(root, courseId);
203             } else {
204                 Notification.alert('Starring course failed', 'Could not change favourite state');
205             }
206             return;
207         }).catch(Notification.exception);
208     };
210     /**
211      * Remove course from favourites
212      *
213      * @param  {Object} root The course overview container
214      * @param  {Number} courseId Course id number
215      */
216     var removeFromFavourites = function(root, courseId) {
217         var removeAction = getRemoveFavouriteMenuItem(root, courseId);
218         var addAction = getAddFavouriteMenuItem(root, courseId);
220         setCourseFavouriteState(courseId, false).then(function(success) {
221             if (success) {
222                 removeAction.addClass('hidden');
223                 addAction.removeClass('hidden');
224                 hideFavouriteIcon(root, courseId);
225             } else {
226                 Notification.alert('Starring course failed', 'Could not change favourite state');
227             }
228             return;
229         }).catch(Notification.exception);
230     };
232     /**
233      * Set the courses favourite status and push to repository
234      *
235      * @param  {Number} courseId Course id to favourite.
236      * @param  {Bool} status new favourite status.
237      * @return {Promise} Repository promise.
238      */
239     var setCourseFavouriteState = function(courseId, status) {
241         return Repository.setFavouriteCourses({
242             courses: [
243                     {
244                         'id': courseId,
245                         'favourite': status
246                     }
247                 ]
248         }).then(function(result) {
249             if (result.warnings.length == 0) {
250                 loadedPages.forEach(function(courseList) {
251                     courseList.courses.forEach(function(course, index) {
252                         if (course.id == courseId) {
253                             courseList.courses[index].isfavourite = status;
254                         }
255                     });
256                 });
257                 return true;
258             } else {
259                 return false;
260             }
261         }).catch(Notification.exception);
262     };
264     /**
265      * Render the dashboard courses.
266      *
267      * @param {object} root The root element for the courses view.
268      * @param {array} coursesData containing array of returned courses.
269      * @return {promise} jQuery promise resolved after rendering is complete.
270      */
271     var renderCourses = function(root, coursesData) {
273         var filters = getFilterValues(root);
275         var currentTemplate = '';
276         if (filters.display == 'cards') {
277             currentTemplate = TEMPLATES.COURSES_CARDS;
278         } else if (filters.display == 'list') {
279             currentTemplate = TEMPLATES.COURSES_LIST;
280         } else {
281             currentTemplate = TEMPLATES.COURSES_SUMMARY;
282         }
284         if (coursesData.courses.length) {
285             return Templates.render(currentTemplate, {
286                 courses: coursesData.courses
287             });
288         } else {
289             var nocoursesimg = root.attr('data-nocoursesimg');
290             return Templates.render(TEMPLATES.NOCOURSES, {
291                 nocoursesimg: nocoursesimg
292             });
293         }
294     };
296     /**
297      * Intialise the courses list and cards views on page load.
298      *
299      * @param {object} root The root element for the courses view.
300      * @param {object} content The content element for the courses view.
301      */
302     var init = function(root, content) {
304         root = $(root);
306         if (!root.attr('data-init')) {
307             registerEventListeners(root);
308             root.attr('data-init', true);
309         }
311         var filters = getFilterValues(root);
313         var pagedContentPromise = PagedContentFactory.createWithLimit(
314             NUMCOURSES_PERPAGE,
315             function(pagesData, actions) {
316                 var promises = [];
318                 pagesData.forEach(function(pageData) {
319                     var currentPage = pageData.pageNumber;
320                     var pageNumber = pageData.pageNumber - 1;
322                     var pagePromise = getMyCourses(
323                         filters,
324                         pageData.limit,
325                         pageNumber
326                     ).then(function(coursesData) {
327                         if (coursesData.courses.length < pageData.limit) {
328                             actions.allItemsLoaded(pageData.pageNumber);
329                         }
330                         loadedPages[currentPage] = coursesData;
331                         return renderCourses(root, coursesData);
332                     })
333                     .catch(Notification.exception);
335                     promises.push(pagePromise);
336                 });
338                 return promises;
339             },
340             DEFAULT_PAGED_CONTENT_CONFIG
341         );
343         pagedContentPromise.then(function(html, js) {
344             return Templates.replaceNodeContents(content, html, js);
345         }).catch(Notification.exception);
346     };
348     /**
349      * Listen to, and handle events for  the myoverview block.
350      *
351      * @param {Object} root The myoverview block container element.
352      */
353     var registerEventListeners = function(root) {
354         CustomEvents.define(root, [
355             CustomEvents.events.activate
356         ]);
358         root.on(CustomEvents.events.activate, SELECTORS.ACTION_ADD_FAVOURITE, function(e, data) {
359             var favourite = $(e.target).closest(SELECTORS.ACTION_ADD_FAVOURITE);
360             var courseId = getFavouriteCourseId(favourite);
361             addToFavourites(root, courseId);
362             data.originalEvent.preventDefault();
363         });
365         root.on(CustomEvents.events.activate, SELECTORS.ACTION_REMOVE_FAVOURITE, function(e, data) {
366             var favourite = $(e.target).closest(SELECTORS.ACTION_REMOVE_FAVOURITE);
367             var courseId = getFavouriteCourseId(favourite);
368             removeFromFavourites(root, courseId);
369             data.originalEvent.preventDefault();
370         });
372         root.on(CustomEvents.events.activate, SELECTORS.FAVOURITE_ICON, function(e, data) {
373             data.originalEvent.preventDefault();
374         });
375     };
377     /**
378      * Reset the courses views to their original
379      * state on first page load.
380      *
381      * This is called when configuration has changed for the event lists
382      * to cause them to reload their data.
383      *
384      * @param {Object} root The root element for the timeline view.
385      * @param {Object} content The content element for the timeline view.
386      */
387     var reset = function(root, content) {
389         if (loadedPages.length > 0) {
390             loadedPages.forEach(function(courseList, index) {
391                 var pagedContentPage = getPagedContentContainer(root, index);
392                 renderCourses(root, courseList).then(function(html, js) {
393                     return Templates.replaceNodeContents(pagedContentPage, html, js);
394                 }).catch(Notification.exception);
395             });
396         } else {
397             init(root, content);
398         }
399     };
401     return {
402         init: init,
403         reset: reset
404     };
405 });