diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 5da51e8..70425b8 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -233,6 +233,14 @@ drupal.machine-name:
- core/drupalSettings
- core/drupal.form
+drupal.message:
+ version: VERSION
+ js:
+ misc/message.js: {}
+ dependencies:
+ - core/drupal
+ - core/drupal.announce
+
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..cd1ee5b 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.
*
@@ -96,25 +103,24 @@
*
* @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);
+ }
+ else {
+ throw new Error(Drupal.t('"text" passed Drupal.announce must be a string.'));
+ }
};
}(Drupal, Drupal.debounce));
diff --git a/core/misc/message.js b/core/misc/message.js
new file mode 100644
index 0000000..de2ee63
--- /dev/null
+++ b/core/misc/message.js
@@ -0,0 +1,158 @@
+/**
+ * @file
+ * Message API.
+ */
+(function (Drupal) {
+
+ 'use strict';
+
+ var defaultMessageWrapperSelector = '[data-drupal-messages]';
+
+ /**
+ * @typedef {object} Drupal.message~messageDefinition
+ *
+ * @prop {HTMLElement} element
+ * DOM element of the messages wrapper.
+ */
+
+ /**
+ * woot
+ *
+ * @param {HTMLElement?} messageWrapper
+ * The zone where to add messages.
+ *
+ * @return {Drupal.message~messageDefinition}
+ * Object to add and remove messages.
+ */
+ Drupal.message = function (messageWrapper) {
+ if (typeof messageWrapper === 'string') {
+ throw new Error(Drupal.t('Drupal.message() expect an HTMLElement as parameter.'));
+ }
+ if (!messageWrapper) {
+ messageWrapper = document.querySelector(defaultMessageWrapperSelector);
+ if (!messageWrapper) {
+ throw new Error(Drupal.t('There is no @element on the page.', {'%element': defaultMessageWrapperSelector}));
+ }
+ }
+
+ /**
+ * Displays a message on the page.
+ *
+ * @name Drupal.message~messageDefinition.add
+ *
+ * @param {string} message
+ * The message to display
+ * @param {string} [type=status]
+ * Message type, can be either 'status', 'error' or 'warning'.
+ * @param {object} [options]
+ * The context of the message, used for removing messages again.
+ *
+ * @return {string}
+ * Index of message.
+ */
+ function messageAdd(message, type, options) {
+ if (typeof message !== 'string') {
+ throw new Error('Message must be a string.');
+ }
+ type = type || 'status';
+ options = options || {};
+ // Send message to screenreader.
+ announce(message, type, options);
+ // Generate a unique key to allow message deletion.
+ options.index = Math.random().toFixed(15).replace('0.', '');
+ this.element.innerHTML += Drupal.theme('message', {text: message, type: type}, options);
+
+ return options.index;
+ }
+
+ /**
+ * Removes messages from the page.
+ *
+ * @name Drupal.message~messageDefinition.remove
+ *
+ * @param {string|number} [messageIndex]
+ * Index of the message to remove, as returned by
+ * {@link Drupal.message~messageDefinition.add} or a number
+ * corresponding to the CSS index of the element.
+ *
+ * @return {number}
+ * Number of removed messages.
+ */
+ function messageRemove(messageIndex) {
+ var removeSelector = '[data-drupal-message]';
+
+ // If it's a string, select corresponding message.
+ if (typeof messageIndex === 'string') {
+ removeSelector = '[data-drupal-message="' + messageIndex + '"]';
+ }
+ // If the index is numeric remove the element based on the DOM index.
+ else if (typeof messageIndex === 'number') {
+ removeSelector = '[data-drupal-message]:nth-child(' + messageIndex + ')';
+ }
+
+ var remove = this.element.querySelectorAll(removeSelector);
+ var length = remove.length;
+ for (var i = 0; i < length; i += 1) {
+ this.element.removeChild(remove[i]);
+ }
+
+ return length;
+ }
+
+ return {
+ element: messageWrapper,
+ add: messageAdd,
+ remove: messageRemove
+ };
+ };
+
+ /**
+ * Helper to call Drupal.announce() with the right parameters.
+ *
+ * @param {string} message
+ * Displayed message.
+ * @param {string} type
+ * Message type, can be either 'status', 'error' or 'warning'.
+ * @param {object} options
+ * Additional data.
+ * @param {string} [options.announce]
+ * Screen-reader version of the message if necessary. To prevent a message
+ * being sent to Drupal.annonce() this should be `''`.
+ * @param {string} [options.priority]
+ * Priority of the message for Drupal.announce().
+ */
+ function announce(message, type, options) {
+ // Check this. Might be too much.
+ if (!options.priority && type !== 'status') {
+ options.priority = 'assertive';
+ }
+ // If screenreader message is not disabled announce screenreader-specific
+ // text or fallback to the displayed message.
+ if (options.announce !== '') {
+ Drupal.announce(options.announce || message, options.priority);
+ }
+ }
+
+ /**
+ * 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 {object} [options]
+ * The message context.
+ * @param {string} options.index
+ * Index of the message, for reference.
+ *
+ * @return {string}
+ * A string representing a DOM fragment.
+ */
+ Drupal.theme.message = function (message, options) {
+ return '