Chris@18: /** Chris@18: * @file Chris@18: * Message API. Chris@18: */ Chris@18: (Drupal => { Chris@18: /** Chris@18: * @typedef {class} Drupal.Message~messageDefinition Chris@18: */ Chris@18: Chris@18: /** Chris@18: * Constructs a new instance of the Drupal.Message class. Chris@18: * Chris@18: * This provides a uniform interface for adding and removing messages to a Chris@18: * specific location on the page. Chris@18: * Chris@18: * @param {HTMLElement} messageWrapper Chris@18: * The zone where to add messages. If no element is provided an attempt is Chris@18: * made to determine a default location. Chris@18: * Chris@18: * @return {Drupal.Message~messageDefinition} Chris@18: * Class to add and remove messages. Chris@18: */ Chris@18: Drupal.Message = class { Chris@18: constructor(messageWrapper = null) { Chris@18: this.messageWrapper = messageWrapper; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Attempt to determine the default location for Chris@18: * inserting JavaScript messages or create one if needed. Chris@18: * Chris@18: * @return {HTMLElement} Chris@18: * The default destination for JavaScript messages. Chris@18: */ Chris@18: static defaultWrapper() { Chris@18: let wrapper = document.querySelector('[data-drupal-messages]'); Chris@18: if (!wrapper) { Chris@18: wrapper = document.querySelector('[data-drupal-messages-fallback]'); Chris@18: wrapper.removeAttribute('data-drupal-messages-fallback'); Chris@18: wrapper.setAttribute('data-drupal-messages', ''); Chris@18: wrapper.removeAttribute('class'); Chris@18: } Chris@18: return wrapper.innerHTML === '' Chris@18: ? Drupal.Message.messageInternalWrapper(wrapper) Chris@18: : wrapper.firstElementChild; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Provide an object containing the available message types. Chris@18: * Chris@18: * @return {Object} Chris@18: * An object containing message type strings. Chris@18: */ Chris@18: static getMessageTypeLabels() { Chris@18: return { Chris@18: status: Drupal.t('Status message'), Chris@18: error: Drupal.t('Error message'), Chris@18: warning: Drupal.t('Warning message'), Chris@18: }; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Sequentially adds a message to the message area. Chris@18: * Chris@18: * @name Drupal.Message~messageDefinition.add Chris@18: * Chris@18: * @param {string} message Chris@18: * The message to display Chris@18: * @param {object} [options] Chris@18: * The context of the message. Chris@18: * @param {string} [options.id] Chris@18: * The message ID, it can be a simple value: `'filevalidationerror'` Chris@18: * or several values separated by a space: `'mymodule formvalidation'` Chris@18: * which can be used as an explicit selector for a message. Chris@18: * @param {string} [options.type=status] Chris@18: * Message type, can be either 'status', 'error' or 'warning'. Chris@18: * @param {string} [options.announce] Chris@18: * Screen-reader version of the message if necessary. To prevent a message Chris@18: * being sent to Drupal.announce() this should be an emptry string. Chris@18: * @param {string} [options.priority] Chris@18: * Priority of the message for Drupal.announce(). Chris@18: * Chris@18: * @return {string} Chris@18: * ID of message. Chris@18: */ Chris@18: add(message, options = {}) { Chris@18: if (!this.messageWrapper) { Chris@18: this.messageWrapper = Drupal.Message.defaultWrapper(); Chris@18: } Chris@18: if (!options.hasOwnProperty('type')) { Chris@18: options.type = 'status'; Chris@18: } Chris@18: Chris@18: if (typeof message !== 'string') { Chris@18: throw new Error('Message must be a string.'); Chris@18: } Chris@18: Chris@18: // Send message to screen reader. Chris@18: Drupal.Message.announce(message, options); Chris@18: /** Chris@18: * Use the provided index for the message or generate a pseudo-random key Chris@18: * to allow message deletion. Chris@18: */ Chris@18: options.id = options.id Chris@18: ? String(options.id) Chris@18: : `${options.type}-${Math.random() Chris@18: .toFixed(15) Chris@18: .replace('0.', '')}`; Chris@18: Chris@18: // Throw an error if an unexpected message type is used. Chris@18: if (!Drupal.Message.getMessageTypeLabels().hasOwnProperty(options.type)) { Chris@18: throw new Error( Chris@18: `The message type, ${ Chris@18: options.type Chris@18: }, is not present in Drupal.Message.getMessageTypeLabels().`, Chris@18: ); Chris@18: } Chris@18: Chris@18: this.messageWrapper.appendChild( Chris@18: Drupal.theme('message', { text: message }, options), Chris@18: ); Chris@18: Chris@18: return options.id; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Select a message based on id. Chris@18: * Chris@18: * @name Drupal.Message~messageDefinition.select Chris@18: * Chris@18: * @param {string} id Chris@18: * The message id to delete from the area. Chris@18: * Chris@18: * @return {Element} Chris@18: * Element found. Chris@18: */ Chris@18: select(id) { Chris@18: return this.messageWrapper.querySelector( Chris@18: `[data-drupal-message-id^="${id}"]`, Chris@18: ); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Removes messages from the message area. Chris@18: * Chris@18: * @name Drupal.Message~messageDefinition.remove Chris@18: * Chris@18: * @param {string} id Chris@18: * Index of the message to remove, as returned by Chris@18: * {@link Drupal.Message~messageDefinition.add}. Chris@18: * Chris@18: * @return {number} Chris@18: * Number of removed messages. Chris@18: */ Chris@18: remove(id) { Chris@18: return this.messageWrapper.removeChild(this.select(id)); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Removes all messages from the message area. Chris@18: * Chris@18: * @name Drupal.Message~messageDefinition.clear Chris@18: */ Chris@18: clear() { Chris@18: Array.prototype.forEach.call( Chris@18: this.messageWrapper.querySelectorAll('[data-drupal-message-id]'), Chris@18: message => { Chris@18: this.messageWrapper.removeChild(message); Chris@18: }, Chris@18: ); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Helper to call Drupal.announce() with the right parameters. Chris@18: * Chris@18: * @param {string} message Chris@18: * Displayed message. Chris@18: * @param {object} options Chris@18: * Additional data. Chris@18: * @param {string} [options.announce] Chris@18: * Screen-reader version of the message if necessary. To prevent a message Chris@18: * being sent to Drupal.announce() this should be `''`. Chris@18: * @param {string} [options.priority] Chris@18: * Priority of the message for Drupal.announce(). Chris@18: * @param {string} [options.type] Chris@18: * Message type, can be either 'status', 'error' or 'warning'. Chris@18: */ Chris@18: static announce(message, options) { Chris@18: if ( Chris@18: !options.priority && Chris@18: (options.type === 'warning' || options.type === 'error') Chris@18: ) { Chris@18: options.priority = 'assertive'; Chris@18: } Chris@18: /** Chris@18: * If screen reader message is not disabled announce screen reader Chris@18: * specific text or fallback to the displayed message. Chris@18: */ Chris@18: if (options.announce !== '') { Chris@18: Drupal.announce(options.announce || message, options.priority); Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * Function for creating the internal message wrapper element. Chris@18: * Chris@18: * @param {HTMLElement} messageWrapper Chris@18: * The message wrapper. Chris@18: * Chris@18: * @return {HTMLElement} Chris@18: * The internal wrapper DOM element. Chris@18: */ Chris@18: static messageInternalWrapper(messageWrapper) { Chris@18: const innerWrapper = document.createElement('div'); Chris@18: innerWrapper.setAttribute('class', 'messages__wrapper'); Chris@18: messageWrapper.insertAdjacentElement('afterbegin', innerWrapper); Chris@18: return innerWrapper; Chris@18: } Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Theme function for a message. Chris@18: * Chris@18: * @param {object} message Chris@18: * The message object. Chris@18: * @param {string} message.text Chris@18: * The message text. Chris@18: * @param {object} options Chris@18: * The message context. Chris@18: * @param {string} options.type Chris@18: * The message type. Chris@18: * @param {string} options.id Chris@18: * ID of the message, for reference. Chris@18: * Chris@18: * @return {HTMLElement} Chris@18: * A DOM Node. Chris@18: */ Chris@18: Drupal.theme.message = ({ text }, { type, id }) => { Chris@18: const messagesTypes = Drupal.Message.getMessageTypeLabels(); Chris@18: const messageWrapper = document.createElement('div'); Chris@18: Chris@18: messageWrapper.setAttribute('class', `messages messages--${type}`); Chris@18: messageWrapper.setAttribute( Chris@18: 'role', Chris@18: type === 'error' || type === 'warning' ? 'alert' : 'status', Chris@18: ); Chris@18: messageWrapper.setAttribute('data-drupal-message-id', id); Chris@18: messageWrapper.setAttribute('data-drupal-message-type', type); Chris@18: Chris@18: messageWrapper.setAttribute('aria-label', messagesTypes[type]); Chris@18: Chris@18: messageWrapper.innerHTML = `${text}`; Chris@18: Chris@18: return messageWrapper; Chris@18: }; Chris@18: })(Drupal);