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