29
0
mirror of https://github.com/joomla/joomla-cms.git synced 2024-06-25 14:53:01 +00:00

[4.0] Decouple messages from core.js (#32747)

This commit is contained in:
Dimitris Grammatikogiannis 2021-04-19 08:26:56 +02:00 committed by GitHub
parent 21965f7cf7
commit 56f88c45f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 223 additions and 124 deletions

View File

@ -23,6 +23,29 @@
"defer": true
}
},
{
"name": "messages-legacy",
"type": "script",
"uri": "system/messages-es5.min.js",
"attributes": {
"nomodule": true,
"defer": true
},
"dependencies": [
"core"
]
},
{
"name": "messages",
"type": "script",
"uri": "system/messages.min.js",
"attributes": {
"type": "module"
},
"dependencies": [
"messages-legacy"
]
},
{
"name": "template.active",
"type": "style",

View File

@ -3,6 +3,45 @@
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
import { sanitizeHtml } from 'bootstrap/js/src/util/sanitizer.js';
const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i;
const DATA_ATTRIBUTE_PATTERN = /^data-[\w-]*$/i;
const DefaultAllowlist = {
// Global attributes allowed on any supplied element below.
'*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN, DATA_ATTRIBUTE_PATTERN],
a: ['target', 'href', 'title', 'rel'],
area: [],
b: [],
br: [],
col: [],
code: [],
div: [],
em: [],
hr: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
i: [],
img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],
li: [],
ol: [],
p: [],
pre: [],
s: [],
small: [],
span: [],
sub: [],
sup: [],
strong: [],
u: [],
ul: [],
};
// Only define the Joomla namespace if not defined.
window.Joomla = window.Joomla || {};
@ -625,112 +664,17 @@ window.Joomla.Modal = window.Joomla.Modal || {
};
/**
* Render messages send via JSON
* Used by some javascripts such as validate.js
* PLEASE NOTE: do NOT use user supplied input in messages as potential HTML markup is NOT
* sanitized!
*
* @param {object} messages JavaScript object containing the messages to render.
* Example:
* const messages = {
* "message": ["This will be a green message", "So will this"],
* "error": ["This will be a red message", "So will this"],
* "info": ["This will be a blue message", "So will this"],
* "notice": ["This will be same as info message", "So will this"],
* "warning": ["This will be a orange message", "So will this"],
* "my_custom_type": ["This will be same as info message", "So will this"]
* };
* @param {string} selector The selector of the container where the message will be rendered
* @param {bool} keepOld If we shall discard old messages
* @param {int} timeout The milliseconds before the message self destruct
* @return void
* @param {string} unsafeHtml The html for sanitization
* @param {object} allowList The list of HTMLElements with an array of allowed attributes
* @param {function} sanitizeFn A custom sanitization function
*
* @return string
*/
Joomla.renderMessages = (messages, selector, keepOld, timeout) => {
let messageContainer;
let typeMessages;
let messagesBox;
let title;
let titleWrapper;
let messageWrapper;
let alertClass;
if (typeof selector === 'undefined' || (selector && selector === '#system-message-container')) {
messageContainer = document.getElementById('system-message-container');
} else {
messageContainer = document.querySelector(selector);
}
if (typeof keepOld === 'undefined' || (keepOld && keepOld === false)) {
Joomla.removeMessages(messageContainer);
}
[].slice.call(Object.keys(messages)).forEach((type) => {
// Array of messages of this type
typeMessages = messages[type];
messagesBox = document.createElement('joomla-alert');
if (['notice', 'message', 'error', 'warning'].indexOf(type) > -1) {
alertClass = (type === 'notice') ? 'info' : type;
alertClass = (type === 'message') ? 'success' : alertClass;
alertClass = (type === 'error') ? 'danger' : alertClass;
alertClass = (type === 'warning') ? 'warning' : alertClass;
} else {
alertClass = 'info';
}
messagesBox.setAttribute('type', alertClass);
messagesBox.setAttribute('dismiss', 'true');
if (timeout && parseInt(timeout, 10) > 0) {
messagesBox.setAttribute('auto-dismiss', timeout);
}
// Title
title = Joomla.Text._(type);
// Skip titles with untranslated strings
if (typeof title !== 'undefined') {
titleWrapper = document.createElement('div');
titleWrapper.className = 'alert-heading';
titleWrapper.innerHTML = `<span class="${type}"></span><span class="visually-hidden">${Joomla.Text._(type) ? Joomla.Text._(type) : type}</span>`;
messagesBox.appendChild(titleWrapper);
}
// Add messages to the message box
messageWrapper = document.createElement('div');
messageWrapper.className = 'alert-wrapper';
typeMessages.forEach((typeMessage) => {
messageWrapper.innerHTML += `<div class="alert-message">${typeMessage}</div>`;
});
messagesBox.appendChild(messageWrapper);
messageContainer.appendChild(messagesBox);
});
};
/**
* Remove messages
*
* @param {element} container The element of the container of the message
* to be removed
*
* @return {void}
*/
Joomla.removeMessages = (container) => {
let messageContainer;
if (container) {
messageContainer = container;
} else {
messageContainer = document.getElementById('system-message-container');
}
const alerts = [].slice.call(messageContainer.querySelectorAll('joomla-alert'));
if (alerts.length) {
alerts.forEach((alert) => {
alert.close();
});
}
Joomla.sanitizeHtml = (unsafeHtml, allowList, sanitizeFn) => {
const allowed = (allowList === undefined || allowList === null)
? DefaultAllowlist : { ...DefaultAllowlist, ...allowList };
return sanitizeHtml(unsafeHtml, allowed, sanitizeFn);
};
/**

View File

@ -0,0 +1,123 @@
/**
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Import the Alert Custom Element
import 'joomla-ui-custom-elements/src/js/alert/alert.js';
/**
* Returns the container of the Messages
*
* @param {string|HTMLElement} container The container
*
* @returns {HTMLElement}
*/
const getMessageContainer = (container) => {
let messageContainer;
if (container instanceof HTMLElement) {
return container;
}
if (typeof container === 'undefined' || (container && container === '#system-message-container')) {
messageContainer = document.getElementById('system-message-container');
} else {
messageContainer = document.querySelector(container);
}
return messageContainer;
};
/**
* Render messages send via JSON
* Used by some javascripts such as validate.js
*
* @param {object} messages JavaScript object containing the messages to render.
* Example:
* const messages = {
* "message": ["This will be a green message", "So will this"],
* "error": ["This will be a red message", "So will this"],
* "info": ["This will be a blue message", "So will this"],
* "notice": ["This will be same as info message", "So will this"],
* "warning": ["This will be a orange message", "So will this"],
* "my_custom_type": ["This will be same as info message", "So will this"]
* };
* @param {string} selector The selector of the container where the message will be rendered
* @param {bool} keepOld If we shall discard old messages
* @param {int} timeout The milliseconds before the message self destruct
* @return void
*/
Joomla.renderMessages = (messages, selector, keepOld, timeout) => {
const messageContainer = getMessageContainer(selector);
if (typeof keepOld === 'undefined' || (keepOld && keepOld === false)) {
Joomla.removeMessages(messageContainer);
}
[].slice.call(Object.keys(messages)).forEach((type) => {
let alertClass = type;
// Array of messages of this type
const typeMessages = messages[type];
const messagesBox = document.createElement('joomla-alert');
if (['success', 'info', 'danger', 'warning'].indexOf(type) < 0) {
alertClass = (type === 'notice') ? 'info' : type;
alertClass = (type === 'message') ? 'success' : alertClass;
alertClass = (type === 'error') ? 'danger' : alertClass;
alertClass = (type === 'warning') ? 'warning' : alertClass;
}
messagesBox.setAttribute('type', alertClass);
messagesBox.setAttribute('dismiss', true);
if (timeout && parseInt(timeout, 10) > 0) {
messagesBox.setAttribute('auto-dismiss', timeout);
}
// Title
const title = Joomla.Text._(type);
// Skip titles with untranslated strings
if (typeof title !== 'undefined') {
const titleWrapper = document.createElement('div');
titleWrapper.className = 'alert-heading';
titleWrapper.innerHTML = Joomla.sanitizeHtml(`<span class="${type}"></span><span class="visually-hidden">${Joomla.Text._(type) ? Joomla.Text._(type) : type}</span>`);
messagesBox.appendChild(titleWrapper);
}
// Add messages to the message box
const messageWrapper = document.createElement('div');
messageWrapper.className = 'alert-wrapper';
typeMessages.forEach((typeMessage) => {
messageWrapper.innerHTML += Joomla.sanitizeHtml(`<div class="alert-message">${typeMessage}</div>`);
});
messagesBox.appendChild(messageWrapper);
messageContainer.appendChild(messagesBox);
});
};
/**
* Remove messages
*
* @param {element} container The element of the container of the message
* to be removed
*
* @return {void}
*/
Joomla.removeMessages = (container) => {
const messageContainer = getMessageContainer(container);
const alerts = [].slice.call(messageContainer.querySelectorAll('joomla-alert'));
if (alerts.length) {
alerts.forEach((alert) => {
alert.close();
});
}
};
document.addEventListener('DOMContentLoaded', () => {
const messages = Joomla.getOptions('joomla.messages');
if (messages) {
Object.keys(messages)
.map((message) => Joomla.renderMessages(messages[message], undefined, true, undefined));
}
});

View File

@ -21,7 +21,7 @@ $this->getWebAssetManager()
$this->getWebAssetManager()
->useStyle('webcomponent.joomla-alert')
->useScript('webcomponent.joomla-alert');
->useScript('messages');
// Add script options
$this->addScriptOptions('system.installation', ['url' => Route::_('index.php')]);

View File

@ -25,7 +25,7 @@ $this->getWebAssetManager()
$this->getWebAssetManager()
->useStyle('webcomponent.joomla-alert')
->useScript('webcomponent.joomla-alert')
->useScript('messages')
->useScript('webcomponent.core-loader');

View File

@ -13,10 +13,11 @@ use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
/* @var $displayData array */
$msgList = $displayData['msgList'];
$document = Factory::getDocument();
$msgOutput = '';
$alert = [
$alert = [
CMSApplication::MSG_EMERGENCY => 'danger',
CMSApplication::MSG_ALERT => 'danger',
CMSApplication::MSG_CRITICAL => 'danger',
@ -40,26 +41,34 @@ Text::script('JOK');
Text::script('JOPEN');
// Alerts progressive enhancement
Factory::getDocument()->getWebAssetManager()
$document->getWebAssetManager()
->useStyle('webcomponent.joomla-alert')
->useScript('webcomponent.joomla-alert');
->useScript('messages');
if (is_array($msgList) && !empty($msgList)) :
foreach ($msgList as $type => $msgs) :
$msgOutput .= '<joomla-alert type="' . ($alert[$type] ?? $type) . '" dismiss="true">';
if (!empty($msgs)) :
$msgOutput .= '<div class="alert-heading">';
$msgOutput .= '<span class="' . $type . '"></span>';
$msgOutput .= '<span class="visually-hidden">' . Text::_($type) . '</span>';
$msgOutput .= '</div>';
$msgOutput .= '<div class="alert-wrapper">';
if (is_array($msgList) && !empty($msgList))
{
$messages = [];
foreach ($msgList as $type => $msgs)
{
// JS loaded messages
$messages[] = [$alert[$type] ?? $type => $msgs];
// Noscript fallback
if (!empty($msgs)) {
$msgOutput .= '<div class="alert alert-' . ($alert[$type] ?? $type) . '">';
foreach ($msgs as $msg) :
$msgOutput .= '<div class="alert-message">' . $msg . '</div>';
$msgOutput .= $msg;
endforeach;
$msgOutput .= '</div>';
endif;
$msgOutput .= '</joomla-alert>';
endforeach;
endif;
}
}
if ($msgOutput !== '')
{
$msgOutput = '<noscript>' . $msgOutput . '</noscript>';
}
$document->addScriptOptions('joomla.messages', $messages);
}
?>
<div id="system-message-container" aria-live="polite"><?php echo $msgOutput; ?></div>