Proyectos de Subversion Moodle

Rev

| 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 area on the notification page.
18
 *
19
 * @module     message_popup/notification_area_control_area
20
 * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
define(['jquery', 'core/templates', 'core/notification', 'core/custom_interaction_events',
24
        'message_popup/notification_repository', 'message_popup/notification_area_events'],
25
    function($, Templates, DebugNotification, CustomEvents, NotificationRepo, NotificationAreaEvents) {
26
 
27
    var SELECTORS = {
28
        CONTAINER: '[data-region="notification-area"]',
29
        CONTENT: '[data-region="content"]',
30
        NOTIFICATION: '[data-region="notification-content-item-container"]',
31
        CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
32
    };
33
 
34
    var TEMPLATES = {
35
        NOTIFICATION: 'message_popup/notification_content_item',
36
    };
37
 
38
    /**
39
     * Constructor for ControlArea
40
     *
41
     * @class
42
     * @param {object} root The root element for the content area
43
     * @param {int} userId The user id of the current user
44
     */
45
    var ControlArea = function(root, userId) {
46
        this.root = $(root);
47
        this.container = this.root.closest(SELECTORS.CONTAINER);
48
        this.userId = userId;
49
        this.content = this.root.find(SELECTORS.CONTENT);
50
        this.offset = 0;
51
        this.limit = 20;
52
        this.initialLoad = false;
53
        this.isLoading = false;
54
        this.loadedAll = false;
55
        this.notifications = {};
56
 
57
        this.registerEventListeners();
58
    };
59
 
60
    /**
61
     * Get the root element.
62
     *
63
     * @method getRoot
64
     * @return {object} jQuery element
65
     */
66
    ControlArea.prototype.getRoot = function() {
67
        return this.root;
68
    };
69
 
70
    /**
71
     * Get the container element (which the control area is within).
72
     *
73
     * @method getContainer
74
     * @return {object} jQuery element
75
     */
76
    ControlArea.prototype.getContainer = function() {
77
        return this.container;
78
    };
79
 
80
    /**
81
     * Get the user id.
82
     *
83
     * @method getUserId
84
     * @return {int}
85
     */
86
    ControlArea.prototype.getUserId = function() {
87
        return this.userId;
88
    };
89
 
90
    /**
91
     * Get the control area content element.
92
     *
93
     * @method getContent
94
     * @return {object} jQuery element
95
     */
96
    ControlArea.prototype.getContent = function() {
97
        return this.content;
98
    };
99
 
100
    /**
101
     * Get the offset value for paginated loading of the
102
     * notifications.
103
     *
104
     * @method getOffset
105
     * @return {int}
106
     */
107
    ControlArea.prototype.getOffset = function() {
108
        return this.offset;
109
    };
110
 
111
    /**
112
     * Get the limit value for the paginated loading of the
113
     * notifications.
114
     *
115
     * @method getLimit
116
     * @return {int}
117
     */
118
    ControlArea.prototype.getLimit = function() {
119
        return this.limit;
120
    };
121
 
122
    /**
123
     * Set the offset value for the paginated loading of the
124
     * notifications.
125
     *
126
     * @method setOffset
127
     * @param {int} value The new offset value
128
     */
129
    ControlArea.prototype.setOffset = function(value) {
130
        this.offset = value;
131
    };
132
 
133
    /**
134
     * Set the limit value for the paginated loading of the
135
     * notifications.
136
     *
137
     * @method setLimit
138
     * @param {int} value The new limit value
139
     */
140
    ControlArea.prototype.setLimit = function(value) {
141
        this.limit = value;
142
    };
143
 
144
    /**
145
     * Increment the offset by the limit amount.
146
     *
147
     * @method incrementOffset
148
     */
149
    ControlArea.prototype.incrementOffset = function() {
150
        this.offset += this.limit;
151
    };
152
 
153
    /**
154
     * Flag the control area as loading.
155
     *
156
     * @method startLoading
157
     */
158
    ControlArea.prototype.startLoading = function() {
159
        this.isLoading = true;
160
        this.getRoot().addClass('loading');
161
    };
162
 
163
    /**
164
     * Remove the loading flag from the control area.
165
     *
166
     * @method stopLoading
167
     */
168
    ControlArea.prototype.stopLoading = function() {
169
        this.isLoading = false;
170
        this.getRoot().removeClass('loading');
171
    };
172
 
173
    /**
174
     * Check if the first load of notifications has been triggered.
175
     *
176
     * @method hasDoneInitialLoad
177
     * @return {bool} true if first notification loaded, false otherwise
178
     */
179
    ControlArea.prototype.hasDoneInitialLoad = function() {
180
        return this.initialLoad;
181
    };
182
 
183
    /**
184
     * Check if all of the notifications have been loaded.
185
     *
186
     * @method hasLoadedAllContent
187
     * @return {bool}
188
     */
189
    ControlArea.prototype.hasLoadedAllContent = function() {
190
        return this.loadedAll;
191
    };
192
 
193
    /**
194
     * Set the state of the loaded all content property.
195
     *
196
     * @method setLoadedAllContent
197
     * @param {bool} val True if all content is loaded, false otherwise
198
     */
199
    ControlArea.prototype.setLoadedAllContent = function(val) {
200
        this.loadedAll = val;
201
    };
202
 
203
    /**
204
     * Save a notification in the cache.
205
     *
206
     * @method setCacheNotification
207
     * @param {object} notification A notification returned by a webservice
208
     */
209
    ControlArea.prototype.setCacheNotification = function(notification) {
210
        this.notifications[notification.id] = notification;
211
    };
212
 
213
    /**
214
     * Retrieve a notification from the cache.
215
     *
216
     * @method getCacheNotification
217
     * @param {int} id The id for the notification you wish to retrieve
218
     * @return {object} A notification (as returned by a webservice)
219
     */
220
    ControlArea.prototype.getCacheNotification = function(id) {
221
        return this.notifications[id];
222
    };
223
 
224
    /**
225
     * Find the notification element in the control area for the given id.
226
     *
227
     * @method getNotificationElement
228
     * @param {int} id The notification id
229
     * @return {(object|null)} jQuery element or null
230
     */
231
    ControlArea.prototype.getNotificationElement = function(id) {
232
        var element = this.getRoot().find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]');
233
        return element.length == 1 ? element : null;
234
    };
235
 
236
    /**
237
     * Scroll the notification element into view within the control area, if it
238
     * isn't already visible.
239
     *
240
     * @method scrollNotificationIntoView
241
     * @param {object} notificationElement The jQuery notification element
242
     */
243
    ControlArea.prototype.scrollNotificationIntoView = function(notificationElement) {
244
        var position = notificationElement.position();
245
        var container = this.getRoot();
246
        var relativeTop = position.top - container.scrollTop();
247
 
248
        // If the element isn't in the view window.
249
        if (relativeTop > container.innerHeight()) {
250
            var height = notificationElement.outerHeight();
251
            // Offset enough to make sure the notification will be in view.
252
            height = height * 4;
253
            var scrollTo = position.top - height;
254
            container.scrollTop(scrollTo);
255
        }
256
    };
257
 
258
    /**
259
     * Show the full notification for the given notification element. The notification
260
     * context is retrieved from the cache and send as data with an event to be
261
     * rendered in the content area.
262
     *
263
     * @method showNotification
264
     * @param {(int|object)} notificationElement The notification id or jQuery notification element
265
     */
266
    ControlArea.prototype.showNotification = function(notificationElement) {
267
        if (typeof notificationElement !== 'object') {
268
            // Assume it's an ID if it's not an object.
269
            notificationElement = this.getNotificationElement(notificationElement);
270
        }
271
 
272
        if (notificationElement && notificationElement.length) {
273
            this.getRoot().find(SELECTORS.NOTIFICATION).removeClass('selected');
274
            notificationElement.addClass('selected').find(SELECTORS.CAN_RECEIVE_FOCUS).focus();
275
            var notificationId = notificationElement.attr('data-id');
276
            var notification = this.getCacheNotification(notificationId);
277
            this.scrollNotificationIntoView(notificationElement);
278
            // Create a new version of the notification to send with the notification so
279
            // this copy isn't modified.
280
            this.getContainer().trigger(NotificationAreaEvents.showNotification, [$.extend({}, notification)]);
281
        }
282
    };
283
 
284
    /**
285
     * Send a request to mark the notification as read in the server and remove the unread
286
     * status from the element.
287
     *
288
     * @method markNotificationAsRead
289
     * @param {object} notificationElement The jQuery notification element
290
     * @return {object} jQuery promise
291
     */
292
    ControlArea.prototype.markNotificationAsRead = function(notificationElement) {
293
        return NotificationRepo.markAsRead(notificationElement.attr('data-id')).done(function() {
294
            notificationElement.removeClass('unread');
295
        });
296
    };
297
 
298
 
299
    /**
300
     * Render the notification data with the appropriate template and add it to the DOM.
301
     *
302
     * @method renderNotifications
303
     * @param {array} notifications Array of notification data
304
     * @return {object} jQuery promise that is resolved when all notifications have been
305
     *                  rendered and added to the DOM
306
     */
307
    ControlArea.prototype.renderNotifications = function(notifications) {
308
        var promises = [];
309
        var container = this.getContent();
310
 
311
        $.each(notifications, function(index, notification) {
312
            // Need to remove the contexturl so the item isn't rendered
313
            // as a link.
314
            var contextUrl = notification.contexturl;
315
            delete notification.contexturl;
316
 
317
            var promise = Templates.render(TEMPLATES.NOTIFICATION, notification)
318
            .then(function(html, js) {
319
                // Restore it for the cache.
320
                notification.contexturl = contextUrl;
321
                this.setCacheNotification(notification);
322
                // Pass the Rendered content out.
323
                return {html: html, js: js};
324
            }.bind(this));
325
            promises.push(promise);
326
        }.bind(this));
327
 
328
        return $.when.apply($, promises).then(function() {
329
            // Each of the promises in the when will pass its results as an argument to the function.
330
            // The order of the arguments will be the order that the promises are passed to when()
331
            // i.e. the first promise's results will be in the first argument.
332
            $.each(arguments, function(index, argument) {
333
                container.append(argument.html);
334
                Templates.runTemplateJS(argument.js);
335
            });
336
            return;
337
        });
338
    };
339
 
340
    /**
341
     * Load notifications from the server and render them.
342
     *
343
     * @method loadMoreNotifications
344
     * @return {object} jQuery promise
345
     */
346
    ControlArea.prototype.loadMoreNotifications = function() {
347
        if (this.isLoading || this.hasLoadedAllContent()) {
348
            return $.Deferred().resolve();
349
        }
350
 
351
        this.startLoading();
352
        var request = {
353
            limit: this.getLimit(),
354
            offset: this.getOffset(),
355
            useridto: this.getUserId(),
356
        };
357
 
358
        if (!this.initialLoad) {
359
            // If this is the first load we may have been given a non-zero offset,
360
            // in which case we need to load all notifications preceeding that offset
361
            // to make sure the full list is rendered.
362
            request.limit = this.getOffset() + this.getLimit();
363
            request.offset = 0;
364
        }
365
 
366
        var promise = NotificationRepo.query(request).then(function(result) {
367
            var notifications = result.notifications;
368
            this.unreadCount = result.unreadcount;
369
            this.setLoadedAllContent(!notifications.length || notifications.length < this.getLimit());
370
            this.initialLoad = true;
371
 
372
            if (notifications.length) {
373
                this.incrementOffset();
374
                return this.renderNotifications(notifications);
375
            }
376
 
377
            return false;
378
        }.bind(this))
379
        .always(function() {
380
            this.stopLoading();
381
        }.bind(this));
382
 
383
        return promise;
384
    };
385
 
386
    /**
387
     * Create the event listeners for the control area.
388
     *
389
     * @method registerEventListeners
390
     */
391
    ControlArea.prototype.registerEventListeners = function() {
392
        CustomEvents.define(this.getRoot(), [
393
            CustomEvents.events.activate,
394
            CustomEvents.events.scrollBottom,
395
            CustomEvents.events.scrollLock,
396
            CustomEvents.events.up,
397
            CustomEvents.events.down,
398
        ]);
399
 
400
        this.getRoot().on(CustomEvents.events.scrollBottom, function() {
401
            this.loadMoreNotifications();
402
        }.bind(this));
403
 
404
        this.getRoot().on(CustomEvents.events.activate, SELECTORS.NOTIFICATION, function(e) {
405
            var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);
406
            this.showNotification(notificationElement);
407
        }.bind(this));
408
 
409
        // Show the previous notification in the list.
410
        this.getRoot().on(CustomEvents.events.up, SELECTORS.NOTIFICATION, function(e, data) {
411
            var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);
412
            this.showNotification(notificationElement.prev());
413
 
414
            data.originalEvent.preventDefault();
415
        }.bind(this));
416
 
417
        // Show the next notification in the list.
418
        this.getRoot().on(CustomEvents.events.down, SELECTORS.NOTIFICATION, function(e, data) {
419
            var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);
420
            this.showNotification(notificationElement.next());
421
 
422
            data.originalEvent.preventDefault();
423
        }.bind(this));
424
 
425
        this.getContainer().on(NotificationAreaEvents.notificationShown, function(e, notification) {
426
            if (!notification.read) {
427
                var element = this.getNotificationElement(notification.id);
428
 
429
                if (element) {
430
                    this.markNotificationAsRead(element);
431
                }
432
 
433
                var cachedNotification = this.getCacheNotification(notification.id);
434
 
435
                if (cachedNotification) {
436
                    cachedNotification.read = true;
437
                }
438
            }
439
        }.bind(this));
440
    };
441
 
442
    return ControlArea;
443
});