ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ.// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Notification manager for in-page notifications in Moodle. * * @module core/notification * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 2.9 */ import Pending from 'core/pending'; import Log from 'core/log'; let currentContextId = M.cfg.contextid; const notificationTypes = { success: 'core/notification_success', info: 'core/notification_info', warning: 'core/notification_warning', error: 'core/notification_error', }; const notificationRegionId = 'user-notifications'; const Selectors = { notificationRegion: `#${notificationRegionId}`, fallbackRegionParents: [ '#region-main', '[role="main"]', 'body', ], }; const setupTargetRegion = () => { let targetRegion = getNotificationRegion(); if (targetRegion) { return false; } const newRegion = document.createElement('span'); newRegion.id = notificationRegionId; return Selectors.fallbackRegionParents.some(selector => { const targetRegion = document.querySelector(selector); if (targetRegion) { targetRegion.prepend(newRegion); return true; } return false; }); }; /** * A notification object displayed to a user. * * @typedef {Object} Notification * @property {string} message The body of the notification * @property {string} type The type of notification to add (error, warning, info, success). * @property {Boolean} closebutton Whether to show the close button. * @property {Boolean} announce Whether to announce to screen readers. */ /** * Poll the server for any new notifications. * * @method * @returns {Promise} */ export const fetchNotifications = async() => { const Ajax = await import('core/ajax'); return Ajax.call([{ methodname: 'core_fetch_notifications', args: { contextid: currentContextId } }])[0] .then(addNotifications); }; /** * Add all of the supplied notifications. * * @method * @param {Notification[]} notifications The list of notificaitons * @returns {Promise} */ const addNotifications = notifications => { if (!notifications.length) { return Promise.resolve(); } const pendingPromise = new Pending('core/notification:addNotifications'); notifications.forEach(notification => renderNotification(notification.template, notification.variables)); return pendingPromise.resolve(); }; /** * Add a notification to the page. * * Note: This does not cause the notification to be added to the session. * * @method * @param {Notification} notification The notification to add. * @returns {Promise} */ export const addNotification = notification => { const pendingPromise = new Pending('core/notification:addNotifications'); let template = notificationTypes.error; notification = { closebutton: true, announce: true, type: 'error', ...notification, }; if (notification.template) { template = notification.template; delete notification.template; } else if (notification.type) { if (typeof notificationTypes[notification.type] !== 'undefined') { template = notificationTypes[notification.type]; } delete notification.type; } return renderNotification(template, notification) .then(pendingPromise.resolve); }; const renderNotification = async(template, variables) => { if (typeof variables.message === 'undefined' || !variables.message) { Log.debug('Notification received without content. Skipping.'); return; } const pendingPromise = new Pending('core/notification:renderNotification'); const Templates = await import('core/templates'); Templates.renderForPromise(template, variables) .then(({html, js = ''}) => { Templates.prependNodeContents(getNotificationRegion(), html, js); return; }) .then(pendingPromise.resolve) .catch(exception); }; const getNotificationRegion = () => document.querySelector(Selectors.notificationRegion); /** * Alert dialogue. * * @method * @param {String|Promise} title * @param {String|Promise} message * @param {String|Promise} cancelText * @returns {Promise} */ export const alert = async(title, message, cancelText) => { var pendingPromise = new Pending('core/notification:alert'); const AlertModal = await import('core/local/modal/alert'); const modal = await AlertModal.create({ body: message, title: title, buttons: { cancel: cancelText, }, removeOnClose: true, show: true, }); pendingPromise.resolve(); return modal; }; /** * The confirm has now been replaced with a save and cancel dialogue. * * @method * @param {String|Promise} title * @param {String|Promise} question * @param {String|Promise} saveLabel * @param {String|Promise} noLabel * @param {String|Promise} saveCallback * @param {String|Promise} cancelCallback * @returns {Promise} */ export const confirm = (title, question, saveLabel, noLabel, saveCallback, cancelCallback) => saveCancel(title, question, saveLabel, saveCallback, cancelCallback); /** * The Save and Cancel dialogue helper. * * @method * @param {String|Promise} title * @param {String|Promise} question * @param {String|Promise} saveLabel * @param {String|Promise} saveCallback * @param {String|Promise} cancelCallback * @param {Object} options * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden) * @returns {Promise} */ export const saveCancel = async(title, question, saveLabel, saveCallback, cancelCallback, { triggerElement = null, } = {}) => { const pendingPromise = new Pending('core/notification:confirm'); const [ SaveCancelModal, ModalEvents, ] = await Promise.all([ import('core/modal_save_cancel'), import('core/modal_events'), ]); const modal = await SaveCancelModal.create({ title, body: question, buttons: { // Note: The noLabel is no longer supported. save: saveLabel, }, removeOnClose: true, show: true, }); modal.getRoot().on(ModalEvents.save, saveCallback); modal.getRoot().on(ModalEvents.cancel, cancelCallback); modal.getRoot().on(ModalEvents.hidden, () => triggerElement?.focus()); pendingPromise.resolve(); return modal; }; /** * The Delete and Cancel dialogue helper. * * @method * @param {String|Promise} title * @param {String|Promise} question * @param {String|Promise} deleteLabel * @param {String|Promise} deleteCallback * @param {String|Promise} cancelCallback * @param {Object} options * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden) * @returns {Promise} */ export const deleteCancel = async(title, question, deleteLabel, deleteCallback, cancelCallback, { triggerElement = null, } = {}) => { const pendingPromise = new Pending('core/notification:confirm'); const [ DeleteCancelModal, ModalEvents, ] = await Promise.all([ import('core/modal_delete_cancel'), import('core/modal_events'), ]); const modal = await DeleteCancelModal.create({ title: title, body: question, buttons: { 'delete': deleteLabel }, removeOnClose: true, show: true, }); modal.getRoot().on(ModalEvents.delete, deleteCallback); modal.getRoot().on(ModalEvents.cancel, cancelCallback); modal.getRoot().on(ModalEvents.hidden, () => triggerElement?.focus()); pendingPromise.resolve(); return modal; }; /** * Add all of the supplied notifications. * * @param {Promise|String} title The header of the modal * @param {Promise|String} question What do we want the user to confirm * @param {Promise|String} saveLabel The modal action link text * @param {Object} options * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden) * @return {Promise} */ export const saveCancelPromise = (title, question, saveLabel, { triggerElement = null, } = {}) => new Promise((resolve, reject) => { saveCancel(title, question, saveLabel, resolve, reject, {triggerElement}); }); /** * Add all of the supplied notifications. * * @param {Promise|String} title The header of the modal * @param {Promise|String} question What do we want the user to confirm * @param {Promise|String} deleteLabel The modal action link text * @param {Object} options * @param {HTMLElement} [options.triggerElement=null] The element that triggered the modal (will receive the focus after hidden) * @return {Promise} */ export const deleteCancelPromise = (title, question, deleteLabel, { triggerElement = null, } = {}) => new Promise((resolve, reject) => { deleteCancel(title, question, deleteLabel, resolve, reject, {triggerElement}); }); /** * Wrap M.core.exception. * * @method * @param {Error} ex */ export const exception = async ex => { const pendingPromise = new Pending('core/notification:displayException'); // Fudge some parameters. if (!ex.stack) { ex.stack = ''; } if (ex.debuginfo) { ex.stack += ex.debuginfo + '\n'; } if (!ex.backtrace && ex.stacktrace) { ex.backtrace = ex.stacktrace; } if (ex.backtrace) { ex.stack += ex.backtrace; const ln = ex.backtrace.match(/line ([^ ]*) of/); const fn = ex.backtrace.match(/ of ([^:]*): /); if (ln && ln[1]) { ex.lineNumber = ln[1]; } if (fn && fn[1]) { ex.fileName = fn[1]; if (ex.fileName.length > 30) { ex.fileName = '...' + ex.fileName.substr(ex.fileName.length - 27); } } } if (typeof ex.name === 'undefined' && ex.errorcode) { ex.name = ex.errorcode; } const Y = await import('core/yui'); Y.use('moodle-core-notification-exception', function() { var modal = new M.core.exception(ex); modal.show(); pendingPromise.resolve(); }); }; /** * Initialise the page for the suppled context, and displaying the supplied notifications. * * @method * @param {Number} contextId * @param {Notification[]} notificationList */ export const init = (contextId, notificationList) => { currentContextId = contextId; // Setup the message target region if it isn't setup already. setupTargetRegion(); // Add provided notifications. addNotifications(notificationList); }; // To maintain backwards compatability we export default here. export default { init, fetchNotifications, addNotification, alert, confirm, saveCancel, saveCancelPromise, deleteCancelPromise, exception, };