diff -u b/core/core.libraries.yml b/core/core.libraries.yml --- b/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -238,9 +238,7 @@ js: misc/message.js: {} dependencies: - - core/jquery - core/drupal - - core/drupal.debounce - core/drupal.announce drupal.progress: diff -u b/core/misc/message.js b/core/misc/message.js --- b/core/misc/message.js +++ b/core/misc/message.js @@ -2,92 +2,136 @@ * @file * Message API. */ -(function ($, Drupal, debounce) { +(function (Drupal) { 'use strict'; - var messages = []; - var debouncedProcessMessages; var defaultMessageWrapperSelector = '[data-drupal-messages]'; - - // 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 + * @typedef {object} Drupal.message~messageDefinition + * + * @prop {HTMLElement} element + * DOM element of the messages wrapper. */ - function processMessages() { - var texts = {}; - messages.reverse().forEach(function (message) { - if (!texts.hasOwnProperty(message.wrapperSelector)) { - texts[message.wrapperSelector] = []; + /** + * 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})); } - texts[message.wrapperSelector].unshift(Drupal.theme('message', message)); - }); - for (var wrapperSelector in texts) { - // skip loop if the property is from prototype - if (texts.hasOwnProperty(wrapperSelector)) { - $(wrapperSelector)[0].innerHTML += texts[wrapperSelector].join(''); + } + + /** + * 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; } - // Reset the messages array after the messages are processed. - messages = []; - } + /** + * 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 + ')'; + } - Drupal.message = {}; + var remove = this.element.querySelectorAll(removeSelector); + var length = remove.length; + for (var i = 0; i < length; i += 1) { + this.element.removeChild(remove[i]); + } - /** - * 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. - * @param {string} wrapperSelector - * The CSS selector for the messages wrapper div. - */ - Drupal.message.add = function (message, type, context, wrapperSelector) { - wrapperSelector = wrapperSelector || defaultMessageWrapperSelector; - if (!$(wrapperSelector).length) { - throw new Error(Drupal.t('There is message element is not valid.')); - } - 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', - wrapperSelector: wrapperSelector - }); - var priority = (type === 'status') ? 'polite' : 'assertive'; - Drupal.announce(message, priority); - debouncedProcessMessages(); + return length; } + + return { + element: messageWrapper, + add: messageAdd, + remove: messageRemove + }; }; /** - * Removes messages from the page. + * Helper to call Drupal.announce() with the right parameters. * - * @param {string} context - * The message context to remove + * @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(). */ - Drupal.message.remove = function (context, type) { - var selector = '.js-messages-context--' + (context || 'default'); - if (type) { - selector = '.messages--' + (type || 'status') + selector; + function announce(message, type, options) { + // Check this. Might be too much. + if (!options.priority && type !== 'status') { + options.priority = 'assertive'; } - $(selector).remove(); - }; + // 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. @@ -101,10 +145,14 @@ - * @param {string} message.context + * @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) { - return '
' + message.text + '
'; + Drupal.theme.message = function (message, options) { + return '
' + message.text + '
'; }; -}(jQuery, Drupal, Drupal.debounce)); +}(Drupal)); diff -u b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js --- b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js +++ b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js @@ -15,32 +15,32 @@ */ Drupal.behaviors.js_message_test = { attach: function (context) { + var message = Drupal.message(); + var messageIndex; + var messageList = []; $('.show-link').once('show-msg').on('click', function (e) { e.preventDefault(); var type = e.currentTarget.getAttribute('data-type'); - var context = e.currentTarget.getAttribute('data-context'); - var selector = e.currentTarget.getAttribute('data-selector'); - message.add('Msg-' + context + '-' + type, type, context, selector); + messageIndex = message.add('Msg-' + type, type); }); $('.remove-link').once('remove-msg').on('click', function (e) { e.preventDefault(); var type = e.currentTarget.getAttribute('data-type'); - var context = e.currentTarget.getAttribute('data-context'); - message.remove(context, type); + message.remove(messageIndex); }); $('.show-multiple').once('show-msg').on('click', function (e) { e.preventDefault(); for (var i = 0; i < 10; i++) { - message.add('Msg-' + i, 'status', 'context-' + i); + messageList.push(message.add('Msg-' + i, 'status')); } }); $('.remove-multiple').once('remove-msg').on('click', function (e) { e.preventDefault(); for (var i = 0; i < 10; i++) { - message.remove('context-' + i); + message.remove(messageList[i]); } - + messageList = []; }); } }; only in patch2: unchanged: --- a/core/themes/bartik/css/components/messages.css +++ b/core/themes/bartik/css/components/messages.css @@ -4,10 +4,15 @@ */ .messages__wrapper { - padding: 20px 0 5px 8px; + padding: 0 0 0 8px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 8px 0; +} +.messages__wrapper .messages:first-child { + margin-top: 28px; +} +.messages__wrapper .messages:last-child { + margin-bottom: 13px; } [dir="rtl"] .messages__wrapper { - padding: 20px 8px 5px 0; + padding: 0 8px 0 0; } only in patch2: unchanged: --- a/core/themes/bartik/templates/status-messages.html.twig +++ b/core/themes/bartik/templates/status-messages.html.twig @@ -23,7 +23,7 @@ {% block messages %} {% if message_list is not empty %} {{ attach_library('bartik/messages') }} -
+
{{ parent() }}
{% endif %}