1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
17 * Javascript to load and render the list of calendar events for a
20 * @module block_myoverview/event_list
21 * @package block_myoverview
22 * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 define(['jquery', 'core/notification', 'core/templates',
26 'core/custom_interaction_events',
27 'block_myoverview/calendar_events_repository'],
28 function($, Notification, Templates, CustomEvents, CalendarEventsRepository) {
30 var SECONDS_IN_DAY = 60 * 60 * 24;
33 EMPTY_MESSAGE: '[data-region="empty-message"]',
34 ROOT: '[data-region="event-list-container"]',
35 EVENT_LIST: '[data-region="event-list"]',
36 EVENT_LIST_CONTENT: '[data-region="event-list-content"]',
37 EVENT_LIST_GROUP_CONTAINER: '[data-region="event-list-group-container"]',
38 LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
39 VIEW_MORE_BUTTON: '[data-action="view-more"]'
43 EVENT_LIST_ITEMS: 'block_myoverview/event-list-items',
44 COURSE_EVENT_LIST_ITEMS: 'block_myoverview/course-event-list-items'
48 * Set a flag on the element to indicate that it has completed
49 * loading all event data.
51 * @method setLoadedAll
53 * @param {object} root The container element
55 var setLoadedAll = function(root) {
56 root.attr('data-loaded-all', true);
60 * Check if all event data has finished loading.
62 * @method hasLoadedAll
64 * @param {object} root The container element
65 * @return {bool} if the element has completed all loading
67 var hasLoadedAll = function(root) {
68 return !!root.attr('data-loaded-all');
72 * Set the element state to loading.
74 * @method startLoading
76 * @param {object} root The container element
78 var startLoading = function(root) {
79 var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER),
80 viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON);
82 root.addClass('loading');
83 loadingIcon.removeClass('hidden');
84 viewMoreButton.prop('disabled', true);
88 * Remove the loading state from the element.
92 * @param {object} root The container element
94 var stopLoading = function(root) {
95 var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER),
96 viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON);
98 root.removeClass('loading');
99 loadingIcon.addClass('hidden');
101 if (!hasLoadedAll(root)) {
102 // Only enable the button if we've got more events to load.
103 viewMoreButton.prop('disabled', false);
108 * Check if the element is currently loading some event data.
112 * @param {object} root The container element
115 var isLoading = function(root) {
116 return root.hasClass('loading');
120 * Flag the root element to remember that it contains events.
122 * @method setHasContent
124 * @param {object} root The container element
126 var setHasContent = function(root) {
127 root.attr('data-has-events', true);
131 * Check if the root element has had events loaded.
135 * @param {object} root The container element
138 var hasContent = function(root) {
139 return root.attr('data-has-events') ? true : false;
143 * Update the visibility of the content area. The content area
144 * is hidden if we have no events.
146 * @method updateContentVisibility
148 * @param {object} root The container element
149 * @param {int} eventCount A count of the events we just received.
151 var updateContentVisibility = function(root, eventCount) {
153 // We've rendered some events, let's remember that.
156 // If this is the first time trying to load events and
157 // we don't have any then there isn't any so let's show
158 // the empty message.
159 if (!hasContent(root)) {
166 * Hide the content area and display the empty content message.
168 * @method hideContent
170 * @param {object} root The container element
172 var hideContent = function(root) {
173 root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');
174 root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
178 * Render a group of calendar events and add them to the event
181 * @method renderGroup
183 * @param {object} group The group container element
184 * @param {array} calendarEvents The list of calendar events
185 * @param {string} templateName The template name
186 * @return {promise} Resolved when the elements are attached to the DOM
188 var renderGroup = function(group, calendarEvents, templateName) {
190 group.removeClass('hidden');
192 return Templates.render(
194 {events: calendarEvents}
195 ).done(function(html, js) {
196 Templates.appendNodeContents(group.find(SELECTORS.EVENT_LIST), html, js);
201 * Determine the time (in seconds) from the given timestamp until the calendar
202 * event will need actioning.
204 * @method timeUntilEvent
206 * @param {int} timestamp The time to compare with
207 * @param {object} event The calendar event
210 var timeUntilEvent = function(timestamp, event) {
211 var orderTime = event.timesort || 0;
212 return orderTime - timestamp;
216 * Check if the given calendar event should be added to the given event
217 * list group container. The event list group container will specify a
218 * day range for the time boundary it is interested in.
220 * If only a start day is specified for the container then it will be treated
221 * as an open catchment for all events that begin after that time.
223 * @method eventBelongsInContainer
225 * @param {object} root The root element
226 * @param {object} event The calendar event
227 * @param {object} container The group event list container
230 var eventBelongsInContainer = function(root, event, container) {
231 var todayTime = root.attr('data-midnight'),
232 timeUntilContainerStart = +container.attr('data-start-day') * SECONDS_IN_DAY,
233 timeUntilContainerEnd = +container.attr('data-end-day') * SECONDS_IN_DAY,
234 timeUntilEventNeedsAction = timeUntilEvent(todayTime, event);
236 if (container.attr('data-end-day') === '') {
237 return timeUntilContainerStart <= timeUntilEventNeedsAction;
239 return timeUntilContainerStart <= timeUntilEventNeedsAction &&
240 timeUntilEventNeedsAction < timeUntilContainerEnd;
245 * Return a function that can be used to filter a list of events based on the day
246 * range specified on the given event list group container.
248 * @method getFilterCallbackForContainer
250 * @param {object} root The root element
251 * @param {object} container Event list group container
254 var getFilterCallbackForContainer = function(root, container) {
255 return function(event) {
256 return eventBelongsInContainer(root, event, $(container));
261 * Render the given calendar events in the container element. The container
262 * elements must have a day range defined using data attributes that will be
263 * used to group the calendar events according to their order time.
267 * @param {object} root The container element
268 * @param {array} calendarEvents A list of calendar events
269 * @return {promise} Resolved with a count of the number of rendered events
271 var render = function(root, calendarEvents) {
273 var templateName = TEMPLATES.EVENT_LIST_ITEMS;
275 if (root.attr('data-course-id')) {
276 templateName = TEMPLATES.COURSE_EVENT_LIST_ITEMS;
279 // Loop over each of the element list groups and find the set of calendar events
280 // that belong to that group (as defined by the group's day range). The matching
281 // list of calendar events are rendered and added to the DOM within that group.
282 return $.when.apply($, $.map(root.find(SELECTORS.EVENT_LIST_GROUP_CONTAINER), function(container) {
283 var events = calendarEvents.filter(getFilterCallbackForContainer(root, container));
286 renderCount += events.length;
287 return renderGroup($(container), events, templateName);
291 })).then(function() {
297 * Retrieve a list of calendar events, render and append them to the end of the
298 * existing list. The events will be loaded based on the set of data attributes
299 * on the root element.
301 * This function can be provided with a jQuery promise. If it is then it won't
302 * attempt to load data by itself, instead it will use the given promise.
304 * The provided promise must resolve with an an object that has an events key
305 * and value is an array of calendar events.
307 * { events: ['event 1', 'event 2'] }
310 * @param {object} root The root element of the event list
311 * @param {object} promise A jQuery promise resolved with events
312 * @return {promise} A jquery promise
314 var load = function(root, promise) {
316 var limit = +root.attr('data-limit'),
317 courseId = +root.attr('data-course-id'),
318 lastId = root.attr('data-last-id'),
319 midnight = root.attr('data-midnight'),
320 startTime = midnight - (14 * SECONDS_IN_DAY);
323 if (isLoading(root)) {
324 return $.Deferred().resolve();
329 // If we haven't been provided a promise to resolve the
330 // data then we will load our own.
331 if (typeof promise == 'undefined') {
333 starttime: startTime,
338 args.aftereventid = lastId;
341 // If we have a course id then we only want events from that course.
343 args.courseid = courseId;
344 promise = CalendarEventsRepository.queryByCourse(args);
346 // Otherwise we want events from any course.
347 promise = CalendarEventsRepository.queryByTime(args);
351 // Request data from the server.
352 return promise.then(function(result) {
353 if (!result.events.length) {
354 // No events, nothing to do.
359 var calendarEvents = result.events;
361 // Remember the last id we've seen.
362 root.attr('data-last-id', calendarEvents[calendarEvents.length - 1].id);
364 if (calendarEvents.length <= limit) {
365 // No more events to load, disable loading button.
369 // Render the events.
370 return render(root, calendarEvents).then(function(renderCount) {
371 if (renderCount < calendarEvents.length) {
372 // if the number of events that was rendered is less than
373 // the number we sent for rendering we can assume that there
374 // are no groups to add them in. Since the ordering of the
375 // events is guaranteed it means that any future requests will
376 // also yield events that can't be rendered, so let's not bother
377 // sending any more requests.
380 return calendarEvents.length;
382 }).then(function(eventCount) {
383 return updateContentVisibility(root, eventCount);
385 Notification.exception
386 ).always(function() {
392 * Register the event listeners for the container element.
394 * @method registerEventListeners
395 * @param {object} root The root element of the event list
397 var registerEventListeners = function(root) {
398 CustomEvents.define(root, [CustomEvents.events.activate]);
399 root.on(CustomEvents.events.activate, SELECTORS.VIEW_MORE_BUTTON, function() {
405 init: function(root) {
408 registerEventListeners(root);
410 registerEventListeners: registerEventListeners,
412 rootSelector: SELECTORS.ROOT,