| 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 |  * A system for displaying small snackbar notifications to users which disappear shortly after they are shown.
 | 
        
           |  |  | 18 |  *
 | 
        
           |  |  | 19 |  * @module     core/toast
 | 
        
           |  |  | 20 |  * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
 | 
        
           |  |  | 21 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 22 |  */
 | 
        
           |  |  | 23 | import Templates from 'core/templates';
 | 
        
           |  |  | 24 | import Notification from 'core/notification';
 | 
        
           |  |  | 25 | import Pending from 'core/pending';
 | 
        
           |  |  | 26 |   | 
        
           |  |  | 27 | const regionSelector = '.toast-wrapper';
 | 
        
           |  |  | 28 |   | 
        
           |  |  | 29 | /**
 | 
        
           |  |  | 30 |  * Add a new region to place toasts in, taking in a parent element.
 | 
        
           |  |  | 31 |  *
 | 
        
           |  |  | 32 |  * @method
 | 
        
           |  |  | 33 |  * @param {HTMLElement} parent
 | 
        
           |  |  | 34 |  */
 | 
        
           |  |  | 35 | export const addToastRegion = async(parent) => {
 | 
        
           |  |  | 36 |     const pendingPromise = new Pending('addToastRegion');
 | 
        
           |  |  | 37 |   | 
        
           |  |  | 38 |     try {
 | 
        
           |  |  | 39 |         const {html, js} = await Templates.renderForPromise('core/local/toast/wrapper', {});
 | 
        
           |  |  | 40 |         Templates.prependNodeContents(parent, html, js);
 | 
        
           |  |  | 41 |     } catch (e) {
 | 
        
           |  |  | 42 |         Notification.exception(e);
 | 
        
           |  |  | 43 |     }
 | 
        
           |  |  | 44 |   | 
        
           |  |  | 45 |     pendingPromise.resolve();
 | 
        
           |  |  | 46 | };
 | 
        
           |  |  | 47 |   | 
        
           |  |  | 48 | /**
 | 
        
           |  |  | 49 |  * Add a new toast or snackbar notification to the page.
 | 
        
           |  |  | 50 |  *
 | 
        
           |  |  | 51 |  * @method
 | 
        
           |  |  | 52 |  * @param {String|Promise<string>} message
 | 
        
           |  |  | 53 |  * @param {Object} configuration
 | 
        
           |  |  | 54 |  * @param {String} [configuration.title]
 | 
        
           |  |  | 55 |  * @param {String} [configuration.subtitle]
 | 
        
           |  |  | 56 |  * @param {String} [configuration.type=info] Optional type of the toast notification ('success', 'info', 'warning' or 'danger')
 | 
        
           |  |  | 57 |  * @param {Boolean} [configuration.autohide=true]
 | 
        
           |  |  | 58 |  * @param {Boolean} [configuration.closeButton=false]
 | 
        
           |  |  | 59 |  * @param {Number} [configuration.delay=4000]
 | 
        
           |  |  | 60 |  *
 | 
        
           |  |  | 61 |  * @example
 | 
        
           |  |  | 62 |  * import {add as addToast} from 'core/toast';
 | 
        
           |  |  | 63 |  * import {getString} from 'core/str';
 | 
        
           |  |  | 64 |  *
 | 
        
           |  |  | 65 |  * addToast('Example string', {
 | 
        
           |  |  | 66 |  *     type: 'warning',
 | 
        
           |  |  | 67 |  *     autohide: false,
 | 
        
           |  |  | 68 |  *     closeButton: true,
 | 
        
           |  |  | 69 |  * });
 | 
        
           |  |  | 70 |  *
 | 
        
           |  |  | 71 |  * addToast(getString('example', 'mod_myexample'), {
 | 
        
           |  |  | 72 |  *     type: 'warning',
 | 
        
           |  |  | 73 |  *     autohide: false,
 | 
        
           |  |  | 74 |  *     closeButton: true,
 | 
        
           |  |  | 75 |  * });
 | 
        
           |  |  | 76 |  */
 | 
        
           |  |  | 77 | export const add = async(message, configuration) => {
 | 
        
           |  |  | 78 |     const pendingPromise = new Pending('addToastRegion');
 | 
        
           |  |  | 79 |     configuration = {
 | 
        
           |  |  | 80 |         type: 'info',
 | 
        
           |  |  | 81 |         closeButton: false,
 | 
        
           |  |  | 82 |         autohide: true,
 | 
        
           |  |  | 83 |         delay: 4000,
 | 
        
           |  |  | 84 |         ...configuration,
 | 
        
           |  |  | 85 |     };
 | 
        
           |  |  | 86 |   | 
        
           |  |  | 87 |     const templateName = `core/local/toast/message`;
 | 
        
           |  |  | 88 |     try {
 | 
        
           |  |  | 89 |         const {html, js} = await Templates.renderForPromise(templateName, {
 | 
        
           |  |  | 90 |             message: await message,
 | 
        
           |  |  | 91 |             ...configuration
 | 
        
           |  |  | 92 |         });
 | 
        
           |  |  | 93 |         const targetNode = await getTargetNode();
 | 
        
           |  |  | 94 |         Templates.prependNodeContents(targetNode, html, js);
 | 
        
           |  |  | 95 |     } catch (e) {
 | 
        
           |  |  | 96 |         Notification.exception(e);
 | 
        
           |  |  | 97 |     }
 | 
        
           |  |  | 98 |   | 
        
           |  |  | 99 |     pendingPromise.resolve();
 | 
        
           |  |  | 100 | };
 | 
        
           |  |  | 101 |   | 
        
           |  |  | 102 | const getTargetNode = async() => {
 | 
        
           |  |  | 103 |     const regions = document.querySelectorAll(regionSelector);
 | 
        
           |  |  | 104 |   | 
        
           |  |  | 105 |     if (regions.length) {
 | 
        
           |  |  | 106 |         return regions[regions.length - 1];
 | 
        
           |  |  | 107 |     }
 | 
        
           |  |  | 108 |   | 
        
           |  |  | 109 |     await addToastRegion(document.body, 'fixed-bottom');
 | 
        
           |  |  | 110 |     return getTargetNode();
 | 
        
           |  |  | 111 | };
 | 
        
           |  |  | 112 |   | 
        
           |  |  | 113 | /**
 | 
        
           |  |  | 114 |  * Remove a parent region.
 | 
        
           |  |  | 115 |  *
 | 
        
           |  |  | 116 |  * This is useful in cases such as where a dialog is to be removed and the toast region should be moved back to the body.
 | 
        
           |  |  | 117 |  *
 | 
        
           |  |  | 118 |  * @param {HTMLElement} parent The region that the toast region is currently a child of.
 | 
        
           |  |  | 119 |  * @param {HTMLElement} newParent The parent element to move the toast region content to.
 | 
        
           |  |  | 120 |  */
 | 
        
           |  |  | 121 | export const removeToastRegion = async(parent, newParent = document) => {
 | 
        
           |  |  | 122 |     const pendingPromise = new Pending('core/toast:removeToastRegion');
 | 
        
           |  |  | 123 |     const getRegionFromParent = (thisParent) => thisParent.querySelector(regionSelector);
 | 
        
           |  |  | 124 |   | 
        
           |  |  | 125 |     const regionToRemove = getRegionFromParent(parent);
 | 
        
           |  |  | 126 |     if (regionToRemove) {
 | 
        
           |  |  | 127 |         const targetRegion = getRegionFromParent(newParent);
 | 
        
           |  |  | 128 |   | 
        
           |  |  | 129 |         regionToRemove.children.forEach((node) => {
 | 
        
           |  |  | 130 |             targetRegion.insertBefore(node, targetRegion.firstChild);
 | 
        
           |  |  | 131 |         });
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 |         regionToRemove.remove();
 | 
        
           |  |  | 134 |     }
 | 
        
           |  |  | 135 |     pendingPromise.resolve();
 | 
        
           |  |  | 136 | };
 |