Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 * Controls the notification popover in the nav bar.
18
 *
19
 * See template: message_popup/notification_popover
20
 *
21
 * @module     message_popup/notification_popover_controller
22
 * @class      notification_popover_controller
23
 * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',
27
            'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',
1441 ariadna 28
            'message_popup/notification_repository', 'message_popup/notification_area_events',
29
            'core/local/aria/focuslock',
30
        ],
1 efrain 31
        function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents,
1441 ariadna 32
            PopoverController, NotificationRepo, NotificationAreaEvents, FocusLock) {
1 efrain 33
 
34
    var SELECTORS = {
35
        MARK_ALL_READ_BUTTON: '[data-action="mark-all-read"]',
36
        ALL_NOTIFICATIONS_CONTAINER: '[data-region="all-notifications"]',
37
        NOTIFICATION: '[data-region="notification-content-item-container"]',
38
        UNREAD_NOTIFICATION: '[data-region="notification-content-item-container"].unread',
39
        NOTIFICATION_LINK: '[data-action="content-item-link"]',
40
        EMPTY_MESSAGE: '[data-region="empty-message"]',
41
        COUNT_CONTAINER: '[data-region="count-container"]',
1441 ariadna 42
        NOTIFICATION_READ_FEEDBACK: '[data-region="notification-read-feedback"]',
43
        CLOSE_NOTIFICATION_POPOVER: '[data-action="close-notification-popover"]',
1 efrain 44
    };
45
 
46
    /**
47
     * Constructor for the NotificationPopoverController.
48
     * Extends PopoverRegionController.
49
     *
50
     * @param {object} element jQuery object root element of the popover
51
     */
52
    var NotificationPopoverController = function(element) {
53
        // Initialise base class.
54
        PopoverController.call(this, element);
55
 
56
        this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);
57
        this.unreadCount = 0;
58
        this.lastQueried = 0;
59
        this.userId = this.root.attr('data-userid');
60
        this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER);
61
        this.limit = 20;
62
        this.offset = 0;
63
        this.loadedAll = false;
64
        this.initialLoad = false;
65
 
66
        // Let's find out how many unread notifications there are.
67
        this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();
68
    };
69
 
70
    /**
71
     * Clone the parent prototype.
72
     */
73
    NotificationPopoverController.prototype = Object.create(PopoverController.prototype);
74
 
75
    /**
76
     * Make sure the constructor is set correctly.
77
     */
78
    NotificationPopoverController.prototype.constructor = NotificationPopoverController;
79
 
80
    /**
81
     * Set the correct aria label on the menu toggle button to be read out by screen
82
     * readers. The message will indicate the state of the unread notifications.
83
     *
84
     * @method updateButtonAriaLabel
85
     */
86
    NotificationPopoverController.prototype.updateButtonAriaLabel = function() {
87
        if (this.isMenuOpen()) {
88
            Str.get_string('hidenotificationwindow', 'message').done(function(string) {
89
                this.menuToggle.attr('aria-label', string);
90
            }.bind(this));
91
        } else {
92
            if (this.unreadCount) {
93
                Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) {
94
                    this.menuToggle.attr('aria-label', string);
95
                }.bind(this));
96
            } else {
97
                Str.get_string('shownotificationwindownonew', 'message').done(function(string) {
98
                    this.menuToggle.attr('aria-label', string);
99
                }.bind(this));
100
            }
101
        }
102
    };
103
 
104
    /**
105
     * Return the jQuery element with the content. This will return either
106
     * the unread notification container or the all notification container
107
     * depending on which is currently visible.
108
     *
109
     * @method getContent
110
     * @return {object} jQuery object currently visible content contianer
111
     */
112
    NotificationPopoverController.prototype.getContent = function() {
113
        return this.container;
114
    };
115
 
116
    /**
117
     * Get the offset value for the current state of the popover in order
118
     * to sent to the backend to correctly paginate the notifications.
119
     *
120
     * @method getOffset
121
     * @return {int} current offset
122
     */
123
    NotificationPopoverController.prototype.getOffset = function() {
124
        return this.offset;
125
    };
126
 
127
    /**
128
     * Increment the offset for the current state, if required.
129
     *
130
     * @method incrementOffset
131
     */
132
    NotificationPopoverController.prototype.incrementOffset = function() {
133
        this.offset += this.limit;
134
    };
135
 
136
    /**
137
     * Check if the first load of notification has been triggered for the current
138
     * state of the popover.
139
     *
140
     * @method hasDoneInitialLoad
141
     * @return {bool} true if first notification loaded, false otherwise
142
     */
143
    NotificationPopoverController.prototype.hasDoneInitialLoad = function() {
144
        return this.initialLoad;
145
    };
146
 
147
    /**
148
     * Check if we've loaded all of the notifications for the current popover
149
     * state.
150
     *
151
     * @method hasLoadedAllContent
152
     * @return {bool} true if all notifications loaded, false otherwise
153
     */
154
    NotificationPopoverController.prototype.hasLoadedAllContent = function() {
155
        return this.loadedAll;
156
    };
157
 
158
    /**
159
     * Set the state of the loaded all content property for the current state
160
     * of the popover.
161
     *
162
     * @method setLoadedAllContent
163
     * @param {bool} val True if all content is loaded, false otherwise
164
     */
165
    NotificationPopoverController.prototype.setLoadedAllContent = function(val) {
166
        this.loadedAll = val;
167
    };
168
 
169
    /**
170
     * Show the unread notification count badge on the menu toggle if there
171
     * are unread notifications, otherwise hide it.
172
     *
173
     * @method renderUnreadCount
174
     */
175
    NotificationPopoverController.prototype.renderUnreadCount = function() {
176
        var element = this.root.find(SELECTORS.COUNT_CONTAINER);
177
 
178
        if (this.unreadCount) {
179
            element.text(this.unreadCount);
180
            element.removeClass('hidden');
181
        } else {
182
            element.addClass('hidden');
183
        }
184
    };
185
 
186
    /**
187
     * Hide the unread notification count badge on the menu toggle.
188
     *
189
     * @method hideUnreadCount
190
     */
191
    NotificationPopoverController.prototype.hideUnreadCount = function() {
192
        this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');
193
    };
194
 
195
    /**
196
     * Find the notification element for the given id.
197
     *
198
     * @param {int} id
199
     * @method getNotificationElement
200
     * @return {object|null} The notification element
201
     */
202
    NotificationPopoverController.prototype.getNotificationElement = function(id) {
203
        var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]');
204
        return element.length == 1 ? element : null;
205
    };
206
 
207
    /**
208
     * Render the notification data with the appropriate template and add it to the DOM.
209
     *
210
     * @method renderNotifications
211
     * @param {array} notifications Notification data
212
     * @param {object} container jQuery object the container to append the rendered notifications
213
     * @return {object} jQuery promise that is resolved when all notifications have been
214
     *                  rendered and added to the DOM
215
     */
216
    NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {
217
        var promises = [];
218
 
219
        $.each(notifications, function(index, notification) {
220
            // Determine what the offset was when loading this notification.
221
            var offset = this.getOffset() - this.limit;
222
            // Update the view more url to contain the offset to allow the notifications
223
            // page to load to the correct position in the list of notifications.
224
            notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {
225
                notificationid: notification.id,
226
                offset: offset,
227
            });
228
 
229
            // Link to mark read page before loading the actual link.
230
            var notificationurlparams = {
231
                notificationid: notification.id
232
            };
233
 
234
            notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams);
235
 
236
            var promise = Templates.render('message_popup/notification_content_item', notification)
237
            .then(function(html, js) {
238
                return {html: html, js: js};
239
            });
240
            promises.push(promise);
241
        }.bind(this));
242
 
243
        return $.when.apply($, promises).then(function() {
244
            // Each of the promises in the when will pass its results as an argument to the function.
245
            // The order of the arguments will be the order that the promises are passed to when()
246
            // i.e. the first promise's results will be in the first argument.
247
            $.each(arguments, function(index, argument) {
248
                container.append(argument.html);
249
                Templates.runTemplateJS(argument.js);
250
            });
251
            return;
252
        });
253
    };
254
 
255
    /**
256
     * Send a request for more notifications from the server, if we aren't already
257
     * loading some and haven't already loaded all of them.
258
     *
259
     * Takes into account the current mode of the popover and will request only
260
     * unread notifications if required.
261
     *
262
     * All notifications are marked as read by the server when they are returned.
263
     *
264
     * @method loadMoreNotifications
265
     * @return {object} jQuery promise that is resolved when notifications have been
266
     *                        retrieved and added to the DOM
267
     */
268
    NotificationPopoverController.prototype.loadMoreNotifications = function() {
269
        if (this.isLoading || this.hasLoadedAllContent()) {
270
            return $.Deferred().resolve();
271
        }
272
 
273
        this.startLoading();
274
        var request = {
275
            limit: this.limit,
276
            offset: this.getOffset(),
277
            useridto: this.userId,
278
        };
279
 
280
        var container = this.getContent();
281
        return NotificationRepo.query(request).then(function(result) {
282
            var notifications = result.notifications;
283
            this.unreadCount = result.unreadcount;
284
            this.lastQueried = Math.floor(new Date().getTime() / 1000);
285
            this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);
286
            this.initialLoad = true;
287
            this.updateButtonAriaLabel();
288
 
289
            if (notifications.length) {
290
                this.incrementOffset();
291
                return this.renderNotifications(notifications, container);
292
            }
293
 
294
            return false;
295
        }.bind(this))
296
        .always(function() {
297
            this.stopLoading();
298
        }.bind(this));
299
    };
300
 
301
    /**
302
     * Send a request to the server to mark all unread notifications as read and update
303
     * the unread count and unread notification elements appropriately.
304
     *
305
     * @return {Promise}
306
     * @method markAllAsRead
307
     */
308
    NotificationPopoverController.prototype.markAllAsRead = function() {
309
        this.markAllReadButton.addClass('loading');
310
 
311
        var request = {
312
            useridto: this.userId,
313
            timecreatedto: this.lastQueried,
314
        };
315
 
316
        return NotificationRepo.markAllAsRead(request)
317
            .then(function() {
318
                this.unreadCount = 0;
319
                this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');
1441 ariadna 320
 
321
                // Set the ARIA live region's contents with the feedback.
322
                const readFeedback = this.root.get(0).querySelector(SELECTORS.NOTIFICATION_READ_FEEDBACK);
323
                Str.get_string('notificationsmarkedasread', 'message').done((notificationsmarkedasread) => {
324
                    readFeedback.innerHTML = notificationsmarkedasread;
325
                });
1 efrain 326
            }.bind(this))
327
            .always(function() {
328
                this.markAllReadButton.removeClass('loading');
329
            }.bind(this));
330
    };
331
 
332
    /**
333
     * Add all of the required event listeners for this notification popover.
334
     *
335
     * @method registerEventListeners
336
     */
337
    NotificationPopoverController.prototype.registerEventListeners = function() {
338
        CustomEvents.define(this.root, [
339
            CustomEvents.events.activate,
340
        ]);
341
 
342
        // Mark all notifications read if the user activates the mark all as read button.
343
        this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {
1441 ariadna 344
            if (this.unreadCount > 0) {
345
                this.markAllAsRead();
346
            }
347
 
1 efrain 348
            e.stopPropagation();
349
            data.originalEvent.preventDefault();
350
        }.bind(this));
351
 
352
        // Mark individual notification read if the user activates it.
353
        this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {
354
            var element = $(e.target).closest(SELECTORS.NOTIFICATION);
355
 
356
            if (element.hasClass('unread')) {
357
                this.unreadCount--;
358
                element.removeClass('unread');
359
            }
360
 
361
            e.stopPropagation();
362
        }.bind(this));
363
 
1441 ariadna 364
        this.root.on(CustomEvents.events.activate, SELECTORS.CLOSE_NOTIFICATION_POPOVER, function(e) {
365
            e.preventDefault();
366
            $(this.root).trigger(CustomEvents.events.escape);
367
            e.stopPropagation();
368
        }.bind(this));
369
 
1 efrain 370
        // Update the notification information when the menu is opened.
371
        this.root.on(this.events().menuOpened, function() {
372
            this.hideUnreadCount();
373
            this.updateButtonAriaLabel();
374
 
375
            if (!this.hasDoneInitialLoad()) {
376
                this.loadMoreNotifications();
377
            }
1441 ariadna 378
 
379
            // Lock focus to the popover when it is opened, it is the parent of the container.
380
            const contentContainer = this.getContentContainer()[0].parentNode;
381
            FocusLock.trapFocus(contentContainer);
382
 
1 efrain 383
        }.bind(this));
384
 
385
        // Update the unread notification count when the menu is closed.
386
        this.root.on(this.events().menuClosed, function() {
387
            this.renderUnreadCount();
388
            this.updateButtonAriaLabel();
1441 ariadna 389
            // Lock focus to the popover when it is opened, it is the parent of the container.
390
            FocusLock.untrapFocus();
1 efrain 391
        }.bind(this));
392
 
393
        // Set aria attributes when popover is loading.
394
        this.root.on(this.events().startLoading, function() {
395
            this.getContent().attr('aria-busy', 'true');
396
        }.bind(this));
397
 
398
        // Set aria attributes when popover is finished loading.
399
        this.root.on(this.events().stopLoading, function() {
400
            this.getContent().attr('aria-busy', 'false');
401
        }.bind(this));
402
 
403
        // Load more notifications if the user has scrolled to the end of content
404
        // item list.
405
        this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {
406
            if (!this.isLoading && !this.hasLoadedAllContent()) {
407
                this.loadMoreNotifications();
408
            }
409
        }.bind(this));
410
 
411
        // Stop mouse scroll from propagating to the window element and
412
        // scrolling the page.
413
        CustomEvents.define(this.getContentContainer(), [
414
            CustomEvents.events.scrollLock
415
        ]);
416
 
417
        // Listen for when a notification is shown in the notifications page and mark
418
        // it as read, if it's unread.
419
        $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {
420
            if (!notification.read) {
421
                var element = this.getNotificationElement(notification.id);
422
 
423
                if (element) {
424
                    element.removeClass('unread');
425
                }
426
 
427
                this.unreadCount--;
428
                this.renderUnreadCount();
429
            }
430
        }.bind(this));
431
    };
432
 
433
    return NotificationPopoverController;
434
});