Skip to content

Commit 280995f

Browse files
committed
Issue #77245 by drpal, tedbow, nod_, phenaproxima, Wim Leers, googletorp, rteijeiro, vineet.osscube, tim.plunkett, idflood, joelpittet, pk188, lauriii, BarisW, lokapujya, chr.fritsch, droplet, andrewmacpherson, dmsmidt, dawehner, alexpott, jessebeach, NickWilde, DuaelFr, Cottser, seutje, samuel.mortenson: Provide a common API for displaying JavaScript messages
1 parent 3257ff1 commit 280995f

File tree

24 files changed

+1009
-20
lines changed

24 files changed

+1009
-20
lines changed

core/core.libraries.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,14 @@ drupal.machine-name:
233233
- core/drupalSettings
234234
- core/drupal.form
235235

236+
drupal.message:
237+
version: VERSION
238+
js:
239+
misc/message.js: {}
240+
dependencies:
241+
- core/drupal
242+
- core/drupal.announce
243+
236244
drupal.progress:
237245
version: VERSION
238246
js:

core/lib/Drupal/Core/Render/Element/StatusMessages.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public function getInfo() {
3232
'#pre_render' => [
3333
get_class() . '::generatePlaceholder',
3434
],
35+
'#include_fallback' => FALSE,
3536
];
3637
}
3738

@@ -45,15 +46,25 @@ public function getInfo() {
4546
* The updated renderable array containing the placeholder.
4647
*/
4748
public static function generatePlaceholder(array $element) {
48-
$element = [
49+
$build = [
4950
'#lazy_builder' => [get_class() . '::renderMessages', [$element['#display']]],
5051
'#create_placeholder' => TRUE,
5152
];
5253

5354
// Directly create a placeholder as we need this to be placeholdered
5455
// regardless if this is a POST or GET request.
5556
// @todo remove this when https://www.drupal.org/node/2367555 lands.
56-
return \Drupal::service('render_placeholder_generator')->createPlaceholder($element);
57+
$build = \Drupal::service('render_placeholder_generator')->createPlaceholder($build);
58+
59+
if ($element['#include_fallback']) {
60+
return [
61+
'fallback' => [
62+
'#markup' => '<div data-drupal-messages-fallback class="hidden"></div>',
63+
],
64+
'messages' => $build,
65+
];
66+
}
67+
return $build;
5768
}
5869

5970
/**

core/lib/Drupal/Core/Render/Plugin/DisplayVariant/SimplePageVariant.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public function build() {
5454
'messages' => [
5555
'#type' => 'status_messages',
5656
'#weight' => -1000,
57+
'#include_fallback' => TRUE,
5758
],
5859
'page_title' => [
5960
'#type' => 'page_title',

core/misc/message.es6.js

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/**
2+
* @file
3+
* Message API.
4+
*/
5+
(Drupal => {
6+
/**
7+
* @typedef {class} Drupal.Message~messageDefinition
8+
*/
9+
10+
/**
11+
* Constructs a new instance of the Drupal.Message class.
12+
*
13+
* This provides a uniform interface for adding and removing messages to a
14+
* specific location on the page.
15+
*
16+
* @param {HTMLElement} messageWrapper
17+
* The zone where to add messages. If no element is provided an attempt is
18+
* made to determine a default location.
19+
*
20+
* @return {Drupal.Message~messageDefinition}
21+
* Class to add and remove messages.
22+
*/
23+
Drupal.Message = class {
24+
constructor(messageWrapper = null) {
25+
this.messageWrapper = messageWrapper;
26+
}
27+
28+
/**
29+
* Attempt to determine the default location for
30+
* inserting JavaScript messages or create one if needed.
31+
*
32+
* @return {HTMLElement}
33+
* The default destination for JavaScript messages.
34+
*/
35+
static defaultWrapper() {
36+
let wrapper = document.querySelector('[data-drupal-messages]');
37+
if (!wrapper) {
38+
wrapper = document.querySelector('[data-drupal-messages-fallback]');
39+
wrapper.removeAttribute('data-drupal-messages-fallback');
40+
wrapper.setAttribute('data-drupal-messages', '');
41+
wrapper.removeAttribute('class');
42+
}
43+
return wrapper.innerHTML === ''
44+
? Drupal.Message.messageInternalWrapper(wrapper)
45+
: wrapper.firstElementChild;
46+
}
47+
48+
/**
49+
* Provide an object containing the available message types.
50+
*
51+
* @return {Object}
52+
* An object containing message type strings.
53+
*/
54+
static getMessageTypeLabels() {
55+
return {
56+
status: Drupal.t('Status message'),
57+
error: Drupal.t('Error message'),
58+
warning: Drupal.t('Warning message'),
59+
};
60+
}
61+
62+
/**
63+
* Sequentially adds a message to the message area.
64+
*
65+
* @name Drupal.Message~messageDefinition.add
66+
*
67+
* @param {string} message
68+
* The message to display
69+
* @param {object} [options]
70+
* The context of the message.
71+
* @param {string} [options.id]
72+
* The message ID, it can be a simple value: `'filevalidationerror'`
73+
* or several values separated by a space: `'mymodule formvalidation'`
74+
* which can be used as an explicit selector for a message.
75+
* @param {string} [options.type=status]
76+
* Message type, can be either 'status', 'error' or 'warning'.
77+
* @param {string} [options.announce]
78+
* Screen-reader version of the message if necessary. To prevent a message
79+
* being sent to Drupal.announce() this should be an emptry string.
80+
* @param {string} [options.priority]
81+
* Priority of the message for Drupal.announce().
82+
*
83+
* @return {string}
84+
* ID of message.
85+
*/
86+
add(message, options = {}) {
87+
if (!this.messageWrapper) {
88+
this.messageWrapper = Drupal.Message.defaultWrapper();
89+
}
90+
if (!options.hasOwnProperty('type')) {
91+
options.type = 'status';
92+
}
93+
94+
if (typeof message !== 'string') {
95+
throw new Error('Message must be a string.');
96+
}
97+
98+
// Send message to screen reader.
99+
Drupal.Message.announce(message, options);
100+
/**
101+
* Use the provided index for the message or generate a pseudo-random key
102+
* to allow message deletion.
103+
*/
104+
options.id = options.id
105+
? String(options.id)
106+
: `${options.type}-${Math.random()
107+
.toFixed(15)
108+
.replace('0.', '')}`;
109+
110+
// Throw an error if an unexpected message type is used.
111+
if (!Drupal.Message.getMessageTypeLabels().hasOwnProperty(options.type)) {
112+
throw new Error(
113+
`The message type, ${
114+
options.type
115+
}, is not present in Drupal.Message.getMessageTypeLabels().`,
116+
);
117+
}
118+
119+
this.messageWrapper.appendChild(
120+
Drupal.theme('message', { text: message }, options),
121+
);
122+
123+
return options.id;
124+
}
125+
126+
/**
127+
* Select a message based on id.
128+
*
129+
* @name Drupal.Message~messageDefinition.select
130+
*
131+
* @param {string} id
132+
* The message id to delete from the area.
133+
*
134+
* @return {Element}
135+
* Element found.
136+
*/
137+
select(id) {
138+
return this.messageWrapper.querySelector(
139+
`[data-drupal-message-id^="${id}"]`,
140+
);
141+
}
142+
143+
/**
144+
* Removes messages from the message area.
145+
*
146+
* @name Drupal.Message~messageDefinition.remove
147+
*
148+
* @param {string} id
149+
* Index of the message to remove, as returned by
150+
* {@link Drupal.Message~messageDefinition.add}.
151+
*
152+
* @return {number}
153+
* Number of removed messages.
154+
*/
155+
remove(id) {
156+
return this.messageWrapper.removeChild(this.select(id));
157+
}
158+
159+
/**
160+
* Removes all messages from the message area.
161+
*
162+
* @name Drupal.Message~messageDefinition.clear
163+
*/
164+
clear() {
165+
Array.prototype.forEach.call(
166+
this.messageWrapper.querySelectorAll('[data-drupal-message-id]'),
167+
message => {
168+
this.messageWrapper.removeChild(message);
169+
},
170+
);
171+
}
172+
173+
/**
174+
* Helper to call Drupal.announce() with the right parameters.
175+
*
176+
* @param {string} message
177+
* Displayed message.
178+
* @param {object} options
179+
* Additional data.
180+
* @param {string} [options.announce]
181+
* Screen-reader version of the message if necessary. To prevent a message
182+
* being sent to Drupal.announce() this should be `''`.
183+
* @param {string} [options.priority]
184+
* Priority of the message for Drupal.announce().
185+
* @param {string} [options.type]
186+
* Message type, can be either 'status', 'error' or 'warning'.
187+
*/
188+
static announce(message, options) {
189+
if (
190+
!options.priority &&
191+
(options.type === 'warning' || options.type === 'error')
192+
) {
193+
options.priority = 'assertive';
194+
}
195+
/**
196+
* If screen reader message is not disabled announce screen reader
197+
* specific text or fallback to the displayed message.
198+
*/
199+
if (options.announce !== '') {
200+
Drupal.announce(options.announce || message, options.priority);
201+
}
202+
}
203+
204+
/**
205+
* Function for creating the internal message wrapper element.
206+
*
207+
* @param {HTMLElement} messageWrapper
208+
* The message wrapper.
209+
*
210+
* @return {HTMLElement}
211+
* The internal wrapper DOM element.
212+
*/
213+
static messageInternalWrapper(messageWrapper) {
214+
const innerWrapper = document.createElement('div');
215+
innerWrapper.setAttribute('class', 'messages__wrapper');
216+
messageWrapper.insertAdjacentElement('afterbegin', innerWrapper);
217+
return innerWrapper;
218+
}
219+
};
220+
221+
/**
222+
* Theme function for a message.
223+
*
224+
* @param {object} message
225+
* The message object.
226+
* @param {string} message.text
227+
* The message text.
228+
* @param {object} options
229+
* The message context.
230+
* @param {string} options.type
231+
* The message type.
232+
* @param {string} options.id
233+
* ID of the message, for reference.
234+
*
235+
* @return {HTMLElement}
236+
* A DOM Node.
237+
*/
238+
Drupal.theme.message = ({ text }, { type, id }) => {
239+
const messagesTypes = Drupal.Message.getMessageTypeLabels();
240+
const messageWrapper = document.createElement('div');
241+
242+
messageWrapper.setAttribute('class', `messages messages--${type}`);
243+
messageWrapper.setAttribute(
244+
'role',
245+
type === 'error' || type === 'warning' ? 'alert' : 'status',
246+
);
247+
messageWrapper.setAttribute('data-drupal-message-id', id);
248+
messageWrapper.setAttribute('data-drupal-message-type', type);
249+
250+
messageWrapper.setAttribute('aria-label', messagesTypes[type]);
251+
252+
messageWrapper.innerHTML = `${text}`;
253+
254+
return messageWrapper;
255+
};
256+
})(Drupal);

0 commit comments

Comments
 (0)