MDL-66828 core: Add toast-style notifications
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 3 Oct 2019 01:39:39 +0000 (09:39 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Wed, 30 Oct 2019 02:23:41 +0000 (10:23 +0800)
Part of MDL-66074

15 files changed:
lang/en/moodle.php
lib/amd/build/toast.min.js [new file with mode: 0644]
lib/amd/build/toast.min.js.map [new file with mode: 0644]
lib/amd/src/toast.js [new file with mode: 0644]
lib/templates/local/toast/message.mustache [new file with mode: 0644]
lib/templates/local/toast/wrapper.mustache [new file with mode: 0644]
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/embedded.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/secure.mustache
theme/classic/style/moodle.css

index 4fd52e3..ef4c7ce 100644 (file)
@@ -512,6 +512,7 @@ $string['digitalminor_desc'] = 'Please ask your parent/guardian to contact:';
 $string['directory'] = 'Directory';
 $string['disable'] = 'Disable';
 $string['disabledcomments'] = 'Comments are disabled';
+$string['dismissnotification'] = 'Dismiss this notification';
 $string['displayingfirst'] = 'Only the first {$a->count} {$a->things} are displayed';
 $string['displayingrecords'] = 'Displaying {$a} records';
 $string['displayingusers'] = 'Displaying users {$a->start} to {$a->end}';
diff --git a/lib/amd/build/toast.min.js b/lib/amd/build/toast.min.js
new file mode 100644 (file)
index 0000000..0ec003f
Binary files /dev/null and b/lib/amd/build/toast.min.js differ
diff --git a/lib/amd/build/toast.min.js.map b/lib/amd/build/toast.min.js.map
new file mode 100644 (file)
index 0000000..ef6ab4a
Binary files /dev/null and b/lib/amd/build/toast.min.js.map differ
diff --git a/lib/amd/src/toast.js b/lib/amd/src/toast.js
new file mode 100644 (file)
index 0000000..8fe5688
--- /dev/null
@@ -0,0 +1,89 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * A system for displaying small snackbar notifications to users which disappear shortly after they are shown.
+ *
+ * @module     core/toast
+ * @package    core
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import Templates from 'core/templates';
+import Notification from 'core/notification';
+import Pending from 'core/pending';
+
+/**
+ * Add a new region to place toasts in, taking in a parent element.
+ *
+ * @param {Element} parent
+ */
+export const addToastRegion = async(parent) => {
+    const pendingPromise = new Pending('addToastRegion');
+
+    try {
+        const {html, js} = await Templates.renderForPromise('core/local/toast/wrapper', {});
+        Templates.prependNodeContents(parent, html, js);
+    } catch (e) {
+        Notification.exception(e);
+    }
+
+    pendingPromise.resolve();
+};
+
+/**
+ * Add a new toast or snackbar notification to the page.
+ *
+ * @param {String} message
+ * @param {Object} configuration
+ * @param {String} [configuration.title]
+ * @param {String} [configuration.subtitle]
+ * @param {Bool} [configuration.autohide=true]
+ * @param {Number} [configuration.delay=4000]
+ */
+export const add = async(message, configuration) => {
+    const pendingPromise = new Pending('addToastRegion');
+    configuration = {
+        closeButton: false,
+        autohide: true,
+        delay: 4000,
+        ...configuration,
+    };
+
+    const templateName = `core/local/toast/message`;
+    try {
+        const targetNode = await getTargetNode();
+        const {html, js} = await Templates.renderForPromise(templateName, {
+            message,
+            ...configuration
+        });
+        Templates.prependNodeContents(targetNode, html, js);
+    } catch (e) {
+        Notification.exception(e);
+    }
+
+    pendingPromise.resolve();
+};
+
+const getTargetNode = async() => {
+    const region = document.querySelector('.toast-wrapper');
+
+    if (region) {
+        return region;
+    }
+
+    await addToastRegion(document.body, 'fixed-bottom');
+    return getTargetNode();
+};
diff --git a/lib/templates/local/toast/message.mustache b/lib/templates/local/toast/message.mustache
new file mode 100644 (file)
index 0000000..a1a0d5e
--- /dev/null
@@ -0,0 +1,21 @@
+<div id="toast-{{uniqid}}" class="toast bg-dark text-white rounded mx-auto" data-delay="{{delay}}" {{^autohide}}data-autohide="false"{{/autohide}}>
+  <div class="toast-body">
+    <span>{{{message}}}</span>
+    {{#closeButton}}
+    <button type="button" class="ml-2 mb-1 close text-white" data-dismiss="toast" aria-label="{{#str}}dismissnotification, core{{/str}}">
+      <span aria-hidden="true">&times;</span>
+    </button>
+    {{/closeButton}}
+  </div>
+</div>
+{{#js}}
+require(['jquery', 'theme_boost/toast'], function(jQuery) {
+    // Show the toast.
+    // Bootstrap toast components are not shown automatically.
+    jQuery('#toast-{{uniqid}}').toast('show');
+
+    jQuery('#toast-{{uniqid}}').on('hidden.bs.toast', function(e) {
+        e.target.remove();
+    });
+});
+{{/js}}
diff --git a/lib/templates/local/toast/wrapper.mustache b/lib/templates/local/toast/wrapper.mustache
new file mode 100644 (file)
index 0000000..d905905
--- /dev/null
@@ -0,0 +1 @@
+<div class="toast-wrapper mx-auto py-3 fixed-top" role="status" aria-live="polite"></div>
index d4f7f52..cb6f5a4 100644 (file)
@@ -2286,7 +2286,6 @@ $switch-transition: .2s all !default;
     float: right !important; /* stylelint-disable-line declaration-no-important */
 }
 
-
 // Emoji picker.
 $picker-width: 350px !default;
 $picker-width-xs: 320px !default;
@@ -2370,3 +2369,9 @@ $picker-emojis-per-row: 7 !default;
         }
     }
 }
+
+.toast-wrapper {
+    max-width: $toast-max-width;
+    // Place these above any modals and other elements.
+    z-index: 1051;
+}
index 3ae6190..7439089 100644 (file)
@@ -11545,6 +11545,10 @@ div.editor_atto_toolbar button .icon {
     .emoji-auto-complete .btn.btn-link.btn-icon.emoji-button.active, .emoji-auto-complete .btn.btn-icon.emoji-button.active, .emoji-auto-complete #page-grade-grading-manage .actions .btn-icon.emoji-button.active.action, #page-grade-grading-manage .actions .emoji-auto-complete .btn-icon.emoji-button.active.action, .emoji-auto-complete #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-icon.emoji-button.active, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .emoji-auto-complete input.btn-icon.emoji-button.active, .emoji-auto-complete #rubric-rubric.gradingform_rubric .btn-icon.emoji-button.active.addcriterion, #rubric-rubric.gradingform_rubric .emoji-auto-complete .btn-icon.emoji-button.active.addcriterion {
       background-color: #e9ecef; }
 
+.toast-wrapper {
+  max-width: 350px;
+  z-index: 1051; }
+
 .icon {
   font-size: 16px;
   width: 16px;
index e64e37b..eef017b 100644 (file)
@@ -39,6 +39,7 @@
 {{> theme_boost/head }}
 
 <body {{{ bodyattributes }}}>
+{{> core/local/toast/wrapper}}
 
 <div id="page-wrapper">
 
index 9953d8e..4fca4fb 100644 (file)
@@ -51,6 +51,7 @@
 {{> theme_boost/head }}
 
 <body {{{ bodyattributes }}}>
+{{> core/local/toast/wrapper}}
 
 <div id="page-wrapper">
 
index ce62a56..f922038 100644 (file)
@@ -42,6 +42,7 @@
 </head>
 
 <body {{{ output.body_attributes }}}>
+{{> core/local/toast/wrapper}}
 
 {{{ output.standard_top_of_body_html }}}
 <div id="page">
index 554a5c0..2cbea46 100644 (file)
@@ -32,6 +32,7 @@
 {{> theme_boost/head }}
 
 <body {{{ bodyattributes }}}>
+{{> core/local/toast/wrapper}}
 
 <div id="page-wrapper">
 
index 481314b..f5feac7 100644 (file)
@@ -42,6 +42,7 @@
 </head>
 
 <body {{{ output.body_attributes }}}>
+{{> core/local/toast/wrapper}}
 
 <div id="page-wrapper">
 
index a83cb82..7e812a9 100644 (file)
@@ -37,6 +37,7 @@
 {{> theme_boost/head }}
 
 <body {{{ bodyattributes }}}>
+{{> core/local/toast/wrapper}}
 
 <div id="page-wrapper">
 
index 9567a8f..134f589 100644 (file)
@@ -11800,6 +11800,10 @@ div.editor_atto_toolbar button .icon {
     .emoji-auto-complete .btn.btn-link.btn-icon.emoji-button.active, .emoji-auto-complete .btn.btn-icon.emoji-button.active, .emoji-auto-complete #page-grade-grading-manage .actions .btn-icon.emoji-button.active.action, #page-grade-grading-manage .actions .emoji-auto-complete .btn-icon.emoji-button.active.action, .emoji-auto-complete #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-icon.emoji-button.active, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .emoji-auto-complete input.btn-icon.emoji-button.active, .emoji-auto-complete #rubric-rubric.gradingform_rubric .btn-icon.emoji-button.active.addcriterion, #rubric-rubric.gradingform_rubric .emoji-auto-complete .btn-icon.emoji-button.active.addcriterion {
       background-color: #e9ecef; }
 
+.toast-wrapper {
+  max-width: 350px;
+  z-index: 1051; }
+
 .icon {
   font-size: 16px;
   width: 16px;