diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 5da51e8..cc3fd71 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -233,6 +233,15 @@ drupal.machine-name: - core/drupalSettings - core/drupal.form +drupal.message: + version: VERSION + js: + misc/message.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupal.debounce + drupal.progress: version: VERSION js: diff --git a/core/lib/Drupal/Core/Render/Element/StatusMessages.php b/core/lib/Drupal/Core/Render/Element/StatusMessages.php index 530457f..cd3bf52 100644 --- a/core/lib/Drupal/Core/Render/Element/StatusMessages.php +++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php @@ -75,19 +75,17 @@ public static function generatePlaceholder(array $element) { public static function renderMessages($type) { $render = []; $messages = drupal_get_messages($type); - if ($messages) { - // Render the messages. - $render = [ - '#theme' => 'status_messages', - // @todo Improve when https://www.drupal.org/node/2278383 lands. - '#message_list' => $messages, - '#status_headings' => [ - 'status' => t('Status message'), - 'error' => t('Error message'), - 'warning' => t('Warning message'), - ], - ]; - } + // Render the messages. + $render = [ + '#theme' => 'status_messages', + // @todo Improve when https://www.drupal.org/node/2278383 lands. + '#message_list' => $messages, + '#status_headings' => [ + 'status' => t('Status message'), + 'error' => t('Error message'), + 'warning' => t('Warning message'), + ], + ]; return $render; } diff --git a/core/misc/announce.js b/core/misc/announce.js index acf850a..7425a43 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -50,21 +50,23 @@ /** * Concatenates announcements to a single string; appends to the live region. */ - function announce() { + function processAnnounce() { var text = []; var priority = 'polite'; var announcement; - // Create an array of announcement strings to be joined and appended to the - // aria live region. - var il = announcements.length; - for (var i = 0; i < il; i++) { - announcement = announcements.pop(); - text.unshift(announcement.text); - // If any of the announcements has a priority of assertive then the group - // of joined announcements will have this priority. - if (announcement.priority === 'assertive') { - priority = 'assertive'; + if (announcements.length) { + // Create an array of announcement strings to be joined and appended to + // the aria live region. + var il = announcements.length; + for (var i = 0; i < il; i++) { + announcement = announcements.pop(); + text.unshift(announcement.text); + // If any of the announcements has a priority of assertive then the + // group of joined announcements will have this priority. + if (announcement.priority === 'assertive') { + priority = 'assertive'; + } } } @@ -81,8 +83,13 @@ // The live text area is updated. Allow the AT to announce the text. liveElement.setAttribute('aria-busy', 'false'); } + } + // 200 ms is right at the cusp where humans notice a pause, so we will wait + // at most this much time before the set of queued announcements is read. + var debouncedProcessAnnounce = debounce(processAnnounce, 200); + /** * Triggers audio UAs to read the supplied text. * @@ -94,27 +101,23 @@ * messages. These messages are then joined and append to the aria-live region * as one text node. * - * @param {string} text + * @param {String} text * A string to be read by the UA. - * @param {string} [priority='polite'] + * @param {String} priority * A string to indicate the priority of the message. Can be either * 'polite' or 'assertive'. * - * @return {function} - * The return of the call to debounce. - * * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops */ Drupal.announce = function (text, priority) { - // Save the text and priority into a closure variable. Multiple simultaneous - // announcements will be concatenated and read in sequence. - announcements.push({ - text: text, - priority: priority - }); - // Immediately invoke the function that debounce returns. 200 ms is right at - // the cusp where humans notice a pause, so we will wait - // at most this much time before the set of queued announcements is read. - return (debounce(announce, 200)()); + if (typeof text === 'string') { + // Save the text and priority into a closure variable. Multiple + // simultaneous announcements will be concatenated and read in sequence. + announcements.push({ + text: text, + priority: priority + }); + debouncedProcessAnnounce(announcements); + } }; }(Drupal, Drupal.debounce)); diff --git a/core/misc/message.js b/core/misc/message.js new file mode 100644 index 0000000..9505ad3 --- /dev/null +++ b/core/misc/message.js @@ -0,0 +1,109 @@ +/** + * @file + * Message API. + */ +(function ($, Drupal, debounce) { + + 'use strict'; + + var messages = []; + var messagesElement; + var debouncedProcessMessages; + var messageWrapperSelector = '[data-drupal-messages]'; + + /** + * Builds a div element with the aria-live attribute and attaches it to the DOM. + */ + Drupal.behaviors.drupalMessage = { + attach: function () { + if (!messagesElement) { + var $messagesWrapper = $(messageWrapperSelector); + if (!$messagesWrapper.length) { + throw new Error(Drupal.t('There is no element with a [data-drupal-messages] attribute')); + } + messagesElement = $messagesWrapper[0]; + } + } + }; + + // Debounce the function in case Drupal.message() is used in a loop. + debouncedProcessMessages = debounce(processMessages, 100); + + /** + * Displays all queued messages in one pass instead of one after the other. + * @see debouncedProcessMessages + */ + function processMessages() { + var text = []; + + messages.reverse().forEach(function (message) { + text.unshift(Drupal.theme('message', message)); + }); + if (text.length) { + messagesElement.innerHTML += text.join(''); + } + // Reset the messages array after the messages are processed. + messages = []; + + } + + Drupal.message = {}; + + /** + * Displays a message on the page. + * + * @param {string} message + * The message to display + * @param {string} type + * Message type, can be either 'status', 'error' or 'warning'. + * Default to 'status' + * @param {string} context + * The context of the message, used for removing messages again. + */ + Drupal.message.add = function (message, type, context) { + if (typeof message === 'string') { + // Save the text and priority into a closure variable. Multiple simultaneous + // announcements will be concatenated and read in sequence. + messages.push({ + context: context || 'default', + text: message, + type: type || 'status' + }); + debouncedProcessMessages(messages); + } + }; + + /** + * Removes messages from the page. + * + * @param {string} context + * The message context to remove + * @param {string} type + * Message type, can be either 'status', 'error' or 'warning'. + * Default to 'status' + */ + Drupal.message.remove = function (context, type) { + context = context || 'default'; + type = type || 'status'; + $('.messages--' + type + '.js-messages-context--' + context).remove(); + }; + + /** + * Theme function for a message. + * + * @param {object} message + * The message object. + * @param {string} message.text + * The message text. + * @param {string} message.type + * The message type. + * @param {string} message.context + * The message context. + * @return {string} + * A string representing a DOM fragment. + */ + Drupal.theme.message = function (message) { + return '
'; + }; + +}(jQuery, Drupal, Drupal.debounce)); diff --git a/core/modules/system/templates/status-messages.html.twig b/core/modules/system/templates/status-messages.html.twig index 73bd9b7..e8f64e0 100644 --- a/core/modules/system/templates/status-messages.html.twig +++ b/core/modules/system/templates/status-messages.html.twig @@ -23,25 +23,27 @@ * @ingroup themeable */ #} +