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
 * Notification manager for in-page notifications in Moodle.
18
 *
19
 * @module     core/notification
20
 * @copyright  2015 Damyon Wiese <damyon@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 * @since      2.9
23
 */
1441 ariadna 24
 
1 efrain 25
import Pending from 'core/pending';
1441 ariadna 26
import ModalEvents from 'core/modal_events';
1 efrain 27
import Log from 'core/log';
28
 
29
let currentContextId = M.cfg.contextid;
30
 
31
const notificationTypes = {
32
    success:  'core/notification_success',
33
    info:     'core/notification_info',
34
    warning:  'core/notification_warning',
35
    error:    'core/notification_error',
36
};
37
 
38
const notificationRegionId = 'user-notifications';
39
 
40
const Selectors = {
41
    notificationRegion: `#${notificationRegionId}`,
42
    fallbackRegionParents: [
43
        '#region-main',
44
        '[role="main"]',
45
        'body',
46
    ],
47
};
48
 
49
const setupTargetRegion = () => {
50
    let targetRegion = getNotificationRegion();
51
    if (targetRegion) {
52
        return false;
53
    }
54
 
55
    const newRegion = document.createElement('span');
56
    newRegion.id = notificationRegionId;
57
 
58
    return Selectors.fallbackRegionParents.some(selector => {
59
        const targetRegion = document.querySelector(selector);
60
 
61
        if (targetRegion) {
62
            targetRegion.prepend(newRegion);
63
            return true;
64
        }
65
 
66
        return false;
67
    });
68
};
69
 
70
/**
71
 * A notification object displayed to a user.
72
 *
73
 * @typedef  {Object} Notification
74
 * @property {string} message       The body of the notification
75
 * @property {string} type          The type of notification to add (error, warning, info, success).
76
 * @property {Boolean} closebutton  Whether to show the close button.
77
 * @property {Boolean} announce     Whether to announce to screen readers.
78
 */
79
 
80
/**
81
 * Poll the server for any new notifications.
82
 *
83
 * @method
84
 * @returns {Promise}
85
 */
86
export const fetchNotifications = async() => {
87
    const Ajax = await import('core/ajax');
88
 
89
    return Ajax.call([{
90
        methodname: 'core_fetch_notifications',
91
        args: {
92
            contextid: currentContextId
93
        }
94
    }])[0]
95
    .then(addNotifications);
96
};
97
 
98
/**
99
 * Add all of the supplied notifications.
100
 *
101
 * @method
102
 * @param {Notification[]} notifications The list of notificaitons
103
 * @returns {Promise}
104
 */
105
const addNotifications = notifications => {
106
    if (!notifications.length) {
107
        return Promise.resolve();
108
    }
109
 
110
    const pendingPromise = new Pending('core/notification:addNotifications');
111
    notifications.forEach(notification => renderNotification(notification.template, notification.variables));
112
 
113
    return pendingPromise.resolve();
114
};
115
 
116
/**
117
 * Add a notification to the page.
118
 *
119
 * Note: This does not cause the notification to be added to the session.
120
 *
121
 * @method
122
 * @param {Notification} notification The notification to add.
123
 * @returns {Promise}
124
 */
125
export const addNotification = notification => {
126
    const pendingPromise = new Pending('core/notification:addNotifications');
127
 
128
    let template = notificationTypes.error;
129
 
130
    notification = {
131
        closebutton:    true,
132
        announce:       true,
133
        type:           'error',
134
        ...notification,
135
    };
136
 
137
    if (notification.template) {
138
        template = notification.template;
139
        delete notification.template;
140
    } else if (notification.type) {
141
        if (typeof notificationTypes[notification.type] !== 'undefined') {
142
            template = notificationTypes[notification.type];
143
        }
144
        delete notification.type;
145
    }
146
 
147
    return renderNotification(template, notification)
148
    .then(pendingPromise.resolve);
149
};
150
 
151
const renderNotification = async(template, variables) => {
152
    if (typeof variables.message === 'undefined' || !variables.message) {
153
        Log.debug('Notification received without content. Skipping.');
154
        return;
155
    }
156
 
157
    const pendingPromise = new Pending('core/notification:renderNotification');
158
    const Templates = await import('core/templates');
159
 
160
    Templates.renderForPromise(template, variables)
161
    .then(({html, js = ''}) => {
162
        Templates.prependNodeContents(getNotificationRegion(), html, js);
163
 
164
        return;
165
    })
166
    .then(pendingPromise.resolve)
167
    .catch(exception);
168
};
169
 
170
const getNotificationRegion = () => document.querySelector(Selectors.notificationRegion);
171
 
172
/**
173
 * Alert dialogue.
174
 *
175
 * @method
176
 * @param {String|Promise} title
177
 * @param {String|Promise} message
178
 * @param {String|Promise} cancelText
179
 * @returns {Promise}
180
 */
181
export const alert = async(title, message, cancelText) => {
182
    var pendingPromise = new Pending('core/notification:alert');
183
 
184
    const AlertModal = await import('core/local/modal/alert');
185
 
186
    const modal = await AlertModal.create({
187
        body: message,
188
        title: title,
189
        buttons: {
190
            cancel: cancelText,
191
        },
192
        removeOnClose: true,
193
        show: true,
194
    });
195
    pendingPromise.resolve();
196
    return modal;
197
};
198
 
199
/**
200
 * The confirm has now been replaced with a save and cancel dialogue.
201
 *
202
 * @method
203
 * @param {String|Promise} title
204
 * @param {String|Promise} question
205
 * @param {String|Promise} saveLabel
206
 * @param {String|Promise} noLabel
207
 * @param {String|Promise} saveCallback
208
 * @param {String|Promise} cancelCallback
209
 * @returns {Promise}
210
 */
211
export const confirm = (title, question, saveLabel, noLabel, saveCallback, cancelCallback) =>
212
        saveCancel(title, question, saveLabel, saveCallback, cancelCallback);
213
 
214
/**
215
 * The Save and Cancel dialogue helper.
216
 *
217
 * @method
218
 * @param {String|Promise} title
219
 * @param {String|Promise} question
220
 * @param {String|Promise} saveLabel
221
 * @param {String|Promise} saveCallback
222
 * @param {String|Promise} cancelCallback
223
 * @param {Object} options
224
 * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden)
225
 * @returns {Promise}
226
 */
227
export const saveCancel = async(title, question, saveLabel, saveCallback, cancelCallback, {
228
    triggerElement = null,
229
} = {}) => {
230
    const pendingPromise = new Pending('core/notification:confirm');
231
 
232
    const [
233
        SaveCancelModal,
234
    ] = await Promise.all([
235
        import('core/modal_save_cancel'),
236
    ]);
237
 
238
    const modal = await SaveCancelModal.create({
239
        title,
240
        body: question,
241
        buttons: {
242
            // Note: The noLabel is no longer supported.
243
            save: saveLabel,
244
        },
245
        removeOnClose: true,
246
        show: true,
247
    });
248
    modal.getRoot().on(ModalEvents.save, saveCallback);
249
    modal.getRoot().on(ModalEvents.cancel, cancelCallback);
250
    modal.getRoot().on(ModalEvents.hidden, () => triggerElement?.focus());
251
    pendingPromise.resolve();
252
 
253
    return modal;
254
};
255
 
256
/**
257
 * The Delete and Cancel dialogue helper.
258
 *
259
 * @method
260
 * @param {String|Promise} title
261
 * @param {String|Promise} question
262
 * @param {String|Promise} deleteLabel
263
 * @param {String|Promise} deleteCallback
264
 * @param {String|Promise} cancelCallback
265
 * @param {Object} options
266
 * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden)
267
 * @returns {Promise}
268
 */
269
export const deleteCancel = async(title, question, deleteLabel, deleteCallback, cancelCallback, {
270
    triggerElement = null,
271
} = {}) => {
272
    const pendingPromise = new Pending('core/notification:confirm');
273
 
274
    const [
275
        DeleteCancelModal,
276
    ] = await Promise.all([
277
        import('core/modal_delete_cancel'),
278
    ]);
279
 
280
    const modal = await DeleteCancelModal.create({
281
        title: title,
282
        body: question,
283
        buttons: {
284
            'delete': deleteLabel
285
        },
286
        removeOnClose: true,
287
        show: true,
288
    });
289
        modal.getRoot().on(ModalEvents.delete, deleteCallback);
290
        modal.getRoot().on(ModalEvents.cancel, cancelCallback);
291
        modal.getRoot().on(ModalEvents.hidden, () => triggerElement?.focus());
292
        pendingPromise.resolve();
293
 
294
        return modal;
295
};
296
 
297
 
298
/**
299
 * Add all of the supplied notifications.
300
 *
301
 * @param {Promise|String} title The header of the modal
302
 * @param {Promise|String} question What do we want the user to confirm
303
 * @param {Promise|String} saveLabel The modal action link text
304
 * @param {Object} options
305
 * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden)
306
 * @return {Promise}
307
 */
308
export const saveCancelPromise = (title, question, saveLabel, {
309
    triggerElement = null,
310
} = {}) => new Promise((resolve, reject) => {
1441 ariadna 311
    saveCancel(title, question, saveLabel, resolve, reject, {triggerElement})
312
        .then(modal => modal.getRoot().on(ModalEvents.hidden, reject));
1 efrain 313
});
314
 
315
/**
316
 * Add all of the supplied notifications.
317
 *
318
 * @param {Promise|String} title The header of the modal
319
 * @param {Promise|String} question What do we want the user to confirm
320
 * @param {Promise|String} deleteLabel The modal action link text
321
 * @param {Object} options
322
 * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden)
323
 * @return {Promise}
324
 */
325
export const deleteCancelPromise = (title, question, deleteLabel, {
326
    triggerElement = null,
327
} = {}) => new Promise((resolve, reject) => {
328
    deleteCancel(title, question, deleteLabel, resolve, reject, {triggerElement});
329
});
330
 
331
/**
332
 * Wrap M.core.exception.
333
 *
334
 * @method
335
 * @param {Error} ex
336
 */
337
export const exception = async ex => {
338
    const pendingPromise = new Pending('core/notification:displayException');
339
 
340
    // Fudge some parameters.
341
    if (!ex.stack) {
342
        ex.stack = '';
343
    }
344
 
345
    if (ex.debuginfo) {
346
        ex.stack += ex.debuginfo + '\n';
347
    }
348
 
349
    if (!ex.backtrace && ex.stacktrace) {
350
        ex.backtrace = ex.stacktrace;
351
    }
352
 
353
    if (ex.backtrace) {
354
        ex.stack += ex.backtrace;
355
        const ln = ex.backtrace.match(/line ([^ ]*) of/);
356
        const fn = ex.backtrace.match(/ of ([^:]*): /);
357
        if (ln && ln[1]) {
358
            ex.lineNumber = ln[1];
359
        }
360
        if (fn && fn[1]) {
361
            ex.fileName = fn[1];
362
            if (ex.fileName.length > 30) {
363
                ex.fileName = '...' + ex.fileName.substr(ex.fileName.length - 27);
364
            }
365
        }
366
    }
367
 
368
    if (typeof ex.name === 'undefined' && ex.errorcode) {
369
        ex.name = ex.errorcode;
370
    }
371
 
372
    const Y = await import('core/yui');
373
    Y.use('moodle-core-notification-exception', function() {
374
        var modal = new M.core.exception(ex);
375
 
376
        modal.show();
377
 
378
        pendingPromise.resolve();
379
    });
380
};
381
 
382
/**
383
 * Initialise the page for the suppled context, and displaying the supplied notifications.
384
 *
385
 * @method
386
 * @param {Number} contextId
387
 * @param {Notification[]} notificationList
388
 */
389
export const init = (contextId, notificationList) => {
390
    currentContextId = contextId;
391
 
392
    // Setup the message target region if it isn't setup already.
393
    setupTargetRegion();
394
 
395
    // Add provided notifications.
396
    addNotifications(notificationList);
397
};
398
 
399
// To maintain backwards compatability we export default here.
400
export default {
401
    init,
402
    fetchNotifications,
403
    addNotification,
404
    alert,
405
    confirm,
406
    saveCancel,
1441 ariadna 407
    deleteCancel,
1 efrain 408
    saveCancelPromise,
409
    deleteCancelPromise,
410
    exception,
411
};