2c59a72ba6d4008941cfa91b8985e508d35e7598
[moodle.git] / calendar / amd / src / view_manager.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  * A javascript module to handler calendar view changes.
18  *
19  * @module     core_calendar/view_manager
20  * @package    core_calendar
21  * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 define([
25     'jquery',
26     'core/templates',
27     'core/str',
28     'core/notification',
29     'core_calendar/repository',
30     'core_calendar/events',
31     'core_calendar/selectors',
32     'core/modal_factory',
33     'core/modal_events',
34     'core_calendar/summary_modal',
35 ], function(
36     $,
37     Templates,
38     Str,
39     Notification,
40     CalendarRepository,
41     CalendarEvents,
42     CalendarSelectors,
43     ModalFactory,
44     ModalEvents,
45     SummaryModal
46 ) {
48         /**
49          * Register event listeners for the module.
50          *
51          * @param {object} root The root element.
52          */
53         var registerEventListeners = function(root) {
54             root = $(root);
56             // Bind click events to event links.
57             root.on('click', CalendarSelectors.links.eventLink, function(e) {
58                 var target = $(e.target);
59                 var eventId = null;
61                 var eventLink;
62                 if (target.is(CalendarSelectors.actions.viewEvent)) {
63                     eventLink = target;
64                 } else {
65                     eventLink = target.closest(CalendarSelectors.actions.viewEvent);
66                 }
68                 if (eventLink.length) {
69                     eventId = eventLink.data('eventId');
70                 } else {
71                     eventId = target.find(CalendarSelectors.actions.viewEvent).data('eventId');
72                 }
74                 if (eventId) {
75                     // A link was found. Show the modal.
77                     e.preventDefault();
78                     // We've handled the event so stop it from bubbling
79                     // and causing the day click handler to fire.
80                     e.stopPropagation();
82                     renderEventSummaryModal(eventId);
83                 }
84             });
87             root.on('click', CalendarSelectors.links.navLink, function(e) {
88                 var wrapper = root.find(CalendarSelectors.wrapper);
89                 var view = wrapper.data('view');
90                 var courseId = wrapper.data('courseid');
91                 var categoryId = wrapper.data('categoryid');
92                 var link = $(e.currentTarget);
94                 if (view === 'month') {
95                     changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId, categoryId);
96                     e.preventDefault();
97                 } else if (view === 'day') {
98                     changeDay(root, link.attr('href'), link.data('year'), link.data('month'), link.data('day'),
99                         courseId, categoryId);
100                     e.preventDefault();
101                 }
103             });
104         };
106         /**
107          * Refresh the month content.
108          *
109          * @param {object} root The root element.
110          * @param {Number} year Year
111          * @param {Number} month Month
112          * @param {Number} courseid The id of the course whose events are shown
113          * @param {Number} categoryid The id of the category whose events are shown
114          * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
115          * @return {promise}
116          */
117         var refreshMonthContent = function(root, year, month, courseid, categoryid, target) {
118             startLoading(root);
120             target = target || root.find(CalendarSelectors.wrapper);
122             M.util.js_pending([root.get('id'), year, month, courseid].join('-'));
123             var includenavigation = root.data('includenavigation');
124             var mini = root.data('mini');
125             return CalendarRepository.getCalendarMonthData(year, month, courseid, categoryid, includenavigation, mini)
126                 .then(function(context) {
127                     return Templates.render(root.attr('data-template'), context);
128                 })
129                 .then(function(html, js) {
130                     return Templates.replaceNode(target, html, js);
131                 })
132                 .then(function() {
133                     $('body').trigger(CalendarEvents.viewUpdated);
134                     return;
135                 })
136                 .always(function() {
137                     M.util.js_complete([root.get('id'), year, month, courseid].join('-'));
138                     return stopLoading(root);
139                 })
140                 .fail(Notification.exception);
141         };
143         /**
144          * Handle changes to the current calendar view.
145          *
146          * @param {object} root The container element
147          * @param {String} url The calendar url to be shown
148          * @param {Number} year Year
149          * @param {Number} month Month
150          * @param {Number} courseid The id of the course whose events are shown
151          * @param {Number} categoryid The id of the category whose events are shown
152          * @return {promise}
153          */
154         var changeMonth = function(root, url, year, month, courseid, categoryid) {
155             return refreshMonthContent(root, year, month, courseid, categoryid)
156                 .then(function() {
157                     if (url.length && url !== '#') {
158                         window.history.pushState({}, '', url);
159                     }
160                     return arguments;
161                 })
162                 .then(function() {
163                     $('body').trigger(CalendarEvents.monthChanged, [year, month, courseid, categoryid]);
164                     return arguments;
165                 });
166         };
168         /**
169          * Reload the current month view data.
170          *
171          * @param {object} root The container element.
172          * @param {Number} courseId The course id.
173          * @param {Number} categoryId The id of the category whose events are shown
174          * @return {promise}
175          */
176         var reloadCurrentMonth = function(root, courseId, categoryId) {
177             var year = root.find(CalendarSelectors.wrapper).data('year');
178             var month = root.find(CalendarSelectors.wrapper).data('month');
180             if (typeof courseId === 'undefined') {
181                 courseId = root.find(CalendarSelectors.wrapper).data('courseid');
182             }
184             if (typeof categoryId === 'undefined') {
185                 categoryId = root.find(CalendarSelectors.wrapper).data('categoryid');
186             }
188             return refreshMonthContent(root, year, month, courseId, categoryId);
189         };
192         /**
193          * Refresh the day content.
194          *
195          * @param {object} root The root element.
196          * @param {Number} year Year
197          * @param {Number} month Month
198          * @param {Number} day Day
199          * @param {Number} courseid The id of the course whose events are shown
200          * @param {Number} categoryId The id of the category whose events are shown
201          * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
202          * @return {promise}
203          */
204         var refreshDayContent = function(root, year, month, day, courseid, categoryId, target) {
205             startLoading(root);
207             target = target || root.find(CalendarSelectors.wrapper);
209             M.util.js_pending([root.get('id'), year, month, day, courseid, categoryId].join('-'));
210             var includenavigation = root.data('includenavigation');
211             return CalendarRepository.getCalendarDayData(year, month, day, courseid, categoryId, includenavigation)
212                 .then(function(context) {
213                     return Templates.render(root.attr('data-template'), context);
214                 })
215                 .then(function(html, js) {
216                     return Templates.replaceNode(target, html, js);
217                 })
218                 .then(function() {
219                     $('body').trigger(CalendarEvents.viewUpdated);
220                     return;
221                 })
222                 .always(function() {
223                     M.util.js_complete([root.get('id'), year, month, day, courseid, categoryId].join('-'));
224                     return stopLoading(root);
225                 })
226                 .fail(Notification.exception);
227         };
229         /**
230          * Reload the current day view data.
231          *
232          * @param {object} root The container element.
233          * @param {Number} courseId The course id.
234          * @param {Number} categoryId The id of the category whose events are shown
235          * @return {promise}
236          */
237         var reloadCurrentDay = function(root, courseId, categoryId) {
238             var wrapper = root.find(CalendarSelectors.wrapper);
239             var year = wrapper.data('year');
240             var month = wrapper.data('month');
241             var day = wrapper.data('day');
243             if (!courseId) {
244                 courseId = root.find(CalendarSelectors.wrapper).data('courseid');
245             }
247             if (typeof categoryId === 'undefined') {
248                 categoryId = root.find(CalendarSelectors.wrapper).data('categoryid');
249             }
251             return refreshDayContent(root, year, month, day, courseId, categoryId);
252         };
254         /**
255          * Handle changes to the current calendar view.
256          *
257          * @param {object} root The root element.
258          * @param {String} url The calendar url to be shown
259          * @param {Number} year Year
260          * @param {Number} month Month
261          * @param {Number} day Day
262          * @param {Number} courseId The id of the course whose events are shown
263          * @param {Number} categoryId The id of the category whose events are shown
264          * @return {promise}
265          */
266         var changeDay = function(root, url, year, month, day, courseId, categoryId) {
267             return refreshDayContent(root, year, month, day, courseId, categoryId)
268                 .then(function() {
269                     if (url.length && url !== '#') {
270                         window.history.pushState({}, '', url);
271                     }
272                     return arguments;
273                 })
274                 .then(function() {
275                     $('body').trigger(CalendarEvents.dayChanged, [year, month, day, courseId, categoryId]);
276                     return arguments;
277                 });
278         };
280         /**
281          * Set the element state to loading.
282          *
283          * @param {object} root The container element
284          * @method startLoading
285          */
286         var startLoading = function(root) {
287             var loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
289             loadingIconContainer.removeClass('hidden');
290         };
292         /**
293          * Remove the loading state from the element.
294          *
295          * @param {object} root The container element
296          * @method stopLoading
297          */
298         var stopLoading = function(root) {
299             var loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
301             loadingIconContainer.addClass('hidden');
302         };
304         /**
305          * Reload the current month view data.
306          *
307          * @param {object} root The container element.
308          * @param {Number} courseId The course id.
309          * @param {Number} categoryId The id of the category whose events are shown
310          * @return {promise}
311          */
312         var reloadCurrentUpcoming = function(root, courseId, categoryId) {
313             startLoading(root);
315             var target = root.find(CalendarSelectors.wrapper);
317             if (typeof courseId === 'undefined') {
318                 courseId = root.find(CalendarSelectors.wrapper).data('courseid');
319             }
321             if (typeof categoryId === 'undefined') {
322                 categoryId = root.find(CalendarSelectors.wrapper).data('categoryid');
323             }
325             return CalendarRepository.getCalendarUpcomingData(courseId, categoryId)
326                 .then(function(context) {
327                     return Templates.render(root.attr('data-template'), context);
328                 })
329                 .then(function(html, js) {
330                     return Templates.replaceNode(target, html, js);
331                 })
332                 .then(function() {
333                     $('body').trigger(CalendarEvents.viewUpdated);
334                     return;
335                 })
336                 .always(function() {
337                     return stopLoading(root);
338                 })
339                 .fail(Notification.exception);
340         };
342         /**
343          * Convert the given event type into one of either user, site,
344          * group, category, or course.
345          *
346          * @param {String} eventType The calendar event type
347          * @return {String}
348          */
349         var normaliseEventType = function(eventType) {
350             switch (eventType) {
351                 case 'user':
352                     return 'user';
353                 case 'site':
354                     return 'site';
355                 case 'group':
356                     return 'group';
357                 case 'category':
358                     return 'category';
359                 default:
360                     return 'course';
361             }
362         };
364         /**
365          * Get the CSS class to apply for the given event type.
366          *
367          * @param {String} eventType The calendar event type
368          * @return {String}
369          */
370         var getEventTypeClassFromType = function(eventType) {
371             return 'calendar_event_' + normaliseEventType(eventType);
372         };
374         /**
375          * Render the event summary modal.
376          *
377          * @param {Number} eventId The calendar event id.
378          */
379         var renderEventSummaryModal = function(eventId) {
380             var typeClass = '';
382             // Calendar repository promise.
383             CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
384                 if (!getEventResponse.event) {
385                     throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
386                 }
387                 var eventData = getEventResponse.event;
388                 typeClass = getEventTypeClassFromType(eventData.eventtype);
390                 return getEventType(eventData.eventtype).then(function(eventType) {
391                     eventData.eventtype = eventType;
392                     return eventData;
393                 });
394             }).then(function(eventData) {
395                 // Build the modal parameters from the event data.
396                 var modalParams = {
397                     title: eventData.name,
398                     type: SummaryModal.TYPE,
399                     body: Templates.render('core_calendar/event_summary_body', eventData),
400                     templateContext: {
401                         canedit: eventData.canedit,
402                         candelete: eventData.candelete,
403                         headerclasses: typeClass,
404                         isactionevent: eventData.isactionevent,
405                         url: eventData.url
406                     }
407                 };
409                 // Create the modal.
410                 return ModalFactory.create(modalParams);
412             }).done(function(modal) {
413                 // Handle hidden event.
414                 modal.getRoot().on(ModalEvents.hidden, function() {
415                     // Destroy when hidden.
416                     modal.destroy();
417                 });
419                 // Finally, render the modal!
420                 modal.show();
422             }).fail(Notification.exception);
423         };
425         /**
426          * Get the event type lang string.
427          *
428          * @param {String} eventType The event type.
429          * @return {promise} The lang string promise.
430          */
431         var getEventType = function(eventType) {
432             var lang = 'type' + normaliseEventType(eventType);
433             return Str.get_string(lang, 'core_calendar').then(function(langStr) {
434                 return langStr;
435             });
436         };
438         return {
439             init: function(root) {
440                 registerEventListeners(root);
441             },
442             reloadCurrentMonth: reloadCurrentMonth,
443             changeMonth: changeMonth,
444             refreshMonthContent: refreshMonthContent,
445             reloadCurrentDay: reloadCurrentDay,
446             changeDay: changeDay,
447             refreshDayContent: refreshDayContent,
448             reloadCurrentUpcoming: reloadCurrentUpcoming
449         };
450     });