@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
560 lines (455 loc) • 21.5 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
'./InstanceManager',
'sap/ui/base/DataType',
'sap/ui/core/AnimationMode',
'sap/ui/core/ControlBehavior',
'sap/ui/core/Popup',
'sap/ui/core/library',
'sap/ui/core/Control',
'sap/ui/core/Element',
'sap/ui/core/UIArea',
'sap/ui/Device',
'sap/base/Log',
'sap/ui/thirdparty/jquery'
],
function(InstanceManager, DataType, AnimationMode, ControlBehavior, Popup, coreLibrary, Control, Element, UIArea, Device, Log, jQuery) {
"use strict";
var Dock = DataType.getType("sap.ui.core.Popup.Dock");
// shortcut for sap.ui.core.CSSSize
var CSSSize = coreLibrary.CSSSize;
/**
* @class
* A small, non-disruptive popup for messages.
* <h3>Overview</h3>
* A message toast is a small, non-disruptive popup for success or information messages that disappears automatically after a few seconds.
* Toasts automatically disappear after a timeout unless the user moves the mouse over the toast or taps on it.
* <h4>Notes:</h4>
* <ul>
* <li>If the configured message contains HTML code or script tags, those will be escaped.</li>
* <li>Line breaks (\r\n, \n\r, \r, \n) will be visualized.</li>
* <li>Only one message toast can be shown at a time in the same place.</li>
* </ul>
* <h4>Example:</h4>
* Here is an example of a MessageToast with all default options:
* <pre>
* sap.m.MessageToast.show("This message should appear in the message toast", {
* duration: 3000, // default
* width: "15em", // default
* my: "CenterBottom", // default
* at: "CenterBottom", // default
* of: window, // default
* offset: "0 0", // default
* collision: "fit fit", // default
* onClose: null, // default
* autoClose: true, // default
* animationTimingFunction: "ease", // default
* animationDuration: 1000, // default
* closeOnBrowserNavigation: true // default
* });
* </pre>
* <h3>Usage</h3>
* <h4>When to use:</h4>
* <ul>
* <li>You want to display a short success of information message.</li>
* <li>You do not want to interrupt users while they are performing an action.</li>
* <li>You want to confirm a successful action.</li>
* </ul>
* <h4>When not to use:</h4>
* <ul>
* <li>You want to display an error or warning message.</li>
* <li>You want to interrupt users while they are performing an action.</li>
* <li>You want to make sure that users read the message before they leave the page.</li>
* <li>You want users to be able to copy some part of the message text. (In this case, show a success {@link sap.m.Dialog Message Dialog}.)</li>
* </ul>
* <h3>Responsive Behavior</h3>
* The message toast has the same behavior on all devices.
* However, you can adjust the width of the control depending on the device you're using, for example desktop.
* Note that the width can be customized up to a maximum of 15rem.
*
* @author SAP SE
* @version 1.146.0
*
* @namespace
* @public
* @since 1.9.2
* @alias sap.m.MessageToast
* @see {@link fiori:https://experience.sap.com/fiori-design-web/message-toast/ Message Toast}
*/
var MessageToast = {};
/* =========================================================== */
/* Internal methods and properties */
/* =========================================================== */
var OFFSET = "0 -64",
CSSCLASS = "sapMMessageToast",
ENABLESELECTIONCLASS = "sapUiSelectable",
BELIZECONTRAST = "sapContrast",
BELIZECONTRASTPLUS = "sapContrastPlus";
MessageToast._mSettings = {
duration: 3000,
width: "15em",
my: "CenterBottom",
at: "CenterBottom",
of: document.defaultView,
offset: "0 0",
collision: "fit fit",
onClose: null,
animationTimingFunction: "ease",
animationDuration: 1000,
autoClose: true,
closeOnBrowserNavigation: true
};
MessageToast._aPopups = [];
MessageToast._iOpenedPopups = 0;
MessageToast._bBoundedEvents = false;
MessageToast._mOptions = {};
MessageToast._sMessage = "";
MessageToast._validateSettings = function(mSettings) {
// duration
MessageToast._isFiniteInteger(mSettings.duration);
// width
MessageToast._validateWidth(mSettings.width);
// my
MessageToast._validateDockPosition(mSettings.my);
// at
MessageToast._validateDockPosition(mSettings.at);
// of
MessageToast._validateOf(mSettings.of);
// offset
MessageToast._validateOffset(mSettings.offset);
// collision
MessageToast._validateCollision(mSettings.collision);
// onClose
MessageToast._validateOnClose(mSettings.onClose);
// autoClose
MessageToast._validateAutoClose(mSettings.autoClose);
// animationTimingFunction
MessageToast._validateAnimationTimingFunction(mSettings.animationTimingFunction);
// animationDuration
MessageToast._isFiniteInteger(mSettings.animationDuration);
};
MessageToast._isFiniteInteger = function(iNumber) {
if (typeof iNumber !== "number" || !isFinite(iNumber) || !(Math.floor(iNumber) === iNumber) || iNumber <= 0) {
Log.error('"iNumber" needs to be a finite positive nonzero integer on ' + MessageToast + "._isFiniteInteger");
}
};
MessageToast._validateWidth = function(sWidth) {
if (!CSSSize.isValid(sWidth)) {
Log.error(sWidth + ' is not of type ' + '"sap.ui.core.CSSSize" for property "width" on ' + MessageToast + "._validateWidth");
}
};
MessageToast._validateDockPosition = function(sDock) {
if (!Dock.isValid(sDock)) {
Log.error('"' + sDock + '"' + ' is not of type ' + '"sap.ui.core.Popup.Dock" on ' + MessageToast + "._validateDockPosition");
}
};
MessageToast._validateOf = function(vElement) {
if (!(vElement instanceof jQuery) &&
!(vElement && vElement.nodeType === 1) &&
!(vElement instanceof Control) &&
vElement !== window) {
Log.error('"of" needs to be an instance of sap.ui.core.Control or an Element or a jQuery object or the window on ' + MessageToast + "._validateOf");
}
};
MessageToast._validateOffset = function(sOffset) {
if (typeof sOffset !== "string") {
Log.error(sOffset + ' is of type ' + typeof sOffset + ', expected "string" for property "offset" on ' + MessageToast + "._validateOffset");
}
};
MessageToast._validateCollision = function(sCollision) {
var rValidCollisions = /^(fit|flip|none|flipfit|flipflip|flip flip|flip fit|fitflip|fitfit|fit fit|fit flip)$/i;
if (!rValidCollisions.test(sCollision)) {
Log.error('"collision" needs to be a single value “fit”, “flip”, or “none”, or a pair for horizontal and vertical e.g. "fit flip”, "fit none", "flipfit" on ' + MessageToast + "._validateOffset");
}
};
MessageToast._validateOnClose = function(fn) {
if (typeof fn !== "function" && fn !== null) {
Log.error('"onClose" should be a function or null on ' + MessageToast + "._validateOnClose");
}
};
MessageToast._validateAutoClose = function(b) {
if (typeof b !== "boolean") {
Log.error('"autoClose" should be a boolean on ' + MessageToast + "._validateAutoClose");
}
};
MessageToast._validateAnimationTimingFunction = function(sTimingFunction) {
var rValidTimingFn = /^(ease|linear|ease-in|ease-out|ease-in-out)$/i;
if (!rValidTimingFn.test(sTimingFunction)) {
Log.error('"animationTimingFunction" should be a string, expected values: ' + "ease, linear, ease-in, ease-out, ease-in-out on " + MessageToast + "._validateAnimationTimingFunction");
}
};
function hasDefaultPosition(mOptions) {
for (var aPositionOptions = ["my", "at", "of", "offset"], i = 0; i < aPositionOptions.length; i++) {
if (mOptions[aPositionOptions[i]] !== undefined) {
return false;
}
}
return true;
}
function createHTMLMarkup(mSettings) {
var oMessageToastDomRef = document.createElement("div");
oMessageToastDomRef.className = CSSCLASS + " " + ENABLESELECTIONCLASS + " " + BELIZECONTRAST + " " + BELIZECONTRASTPLUS;
if (ControlBehavior.isAccessibilityEnabled()) {
oMessageToastDomRef.setAttribute("role", "alert");
}
oMessageToastDomRef.style.width = mSettings.width;
oMessageToastDomRef.appendChild(document.createTextNode(mSettings.message));
return oMessageToastDomRef;
}
function normalizeOptions(mOptions) {
if (mOptions) {
// if no position options are provided
if (hasDefaultPosition(mOptions)) {
// change the default offset
mOptions.offset = OFFSET;
}
// if the document object is provided as an option, replace it with the window object,
// the message toast should be showed relative to the visual viewport instead to the layout viewport
if (mOptions.of && mOptions.of.nodeType === 9) {
mOptions.of = document.defaultView;
}
} else {
mOptions = {
// if no options are provided, change the default offset
offset: OFFSET
};
}
return mOptions;
}
/* =========================================================== */
/* Event handlers */
/* =========================================================== */
MessageToast._handleResizeEvent = function() {
if (Device.system.phone || Device.system.tablet) {
MessageToast._resetPosition(MessageToast._aPopups);
}
setTimeout(MessageToast["_applyPositions"].bind(MessageToast, MessageToast._aPopups), 0);
};
MessageToast._handleMouseDownEvent = function(oEvent) {
var bIsMessageToast = oEvent.target.hasAttribute("class") &&
oEvent.target.getAttribute("class").indexOf(CSSCLASS) !== -1;
if (bIsMessageToast || oEvent.isMarked("delayedMouseEvent")) {
return;
}
MessageToast._aPopups.forEach(function(oPopup) {
oPopup && oPopup.__bAutoClose && oPopup.close();
});
};
MessageToast._resetPosition = function(aPopups) {
for (var i = 0, oMessageToastDomRef; i < aPopups.length; i++) {
oMessageToastDomRef = aPopups[i] && aPopups[i].getContent();
if (oMessageToastDomRef) {
oMessageToastDomRef.style.visibility = "hidden";
oMessageToastDomRef.style.left = 0;
}
}
};
MessageToast._applyPositions = function(aPopups) {
for (var i = 0, oPopup, mPosition; i < aPopups.length; i++) {
oPopup = aPopups[i];
if (oPopup) {
mPosition = oPopup._oPosition; // TODO _oPosition is a private property
if (Device.system.phone || Device.system.tablet) {
setTimeout(MessageToast["_applyPosition"].bind(MessageToast, oPopup, mPosition), 0);
} else {
oPopup.setPosition(mPosition.my, mPosition.at, mPosition.of, mPosition.offset);
}
}
}
};
MessageToast._applyPosition = function(oPopup, mPosition) {
mPosition = mPosition || oPopup._oPosition;
var oMessageToastDomRef = oPopup.getContent();
oPopup.setPosition(mPosition.my, mPosition.at, mPosition.of, mPosition.offset);
oMessageToastDomRef.style.visibility = "visible";
};
MessageToast._setCloseAnimation = function($MessageToastDomRef, iDuration, fnClose, mSettings) {
var sCssTransition = "opacity " + mSettings.animationTimingFunction + " " + mSettings.animationDuration + "ms",
sTransitionEnd = "webkitTransitionEnd." + CSSCLASS + " transitionend." + CSSCLASS,
sAnimationMode = ControlBehavior.getAnimationMode(),
bHasAnimations = sAnimationMode !== AnimationMode.none && sAnimationMode !== AnimationMode.minimal;
if (bHasAnimations && mSettings.animationDuration > 0) {
$MessageToastDomRef[0].style.webkitTransition = sCssTransition;
$MessageToastDomRef[0].style.transition = sCssTransition;
$MessageToastDomRef[0].style.opacity = 0;
$MessageToastDomRef.on(sTransitionEnd, function handleMTTransitionEnd() {
// unbound the event handler after its first invocation
$MessageToastDomRef.off(sTransitionEnd);
// handleMTClosed() function is called
fnClose();
});
} else {
fnClose();
}
};
// Display the message permanently if the desired key combination is pressed
MessageToast._fnKeyDown = function(oEvent) {
var oFocusableElement;
var oPopup = this._aPopups[0];
var bShift = oEvent.shiftKey;
var bMetaKey = Device.os.macintosh ? oEvent.metaKey : oEvent.ctrlKey;
if (oPopup && oPopup.isOpen() && bShift && bMetaKey && oEvent.code === "KeyM") {
oEvent.preventDefault();
oFocusableElement = document.querySelector(".sapMMessageToastHiddenFocusable");
oPopup.getContent().classList.add("sapMFocus");
oFocusableElement.focus();
clearTimeout(this._iCloseTimeoutId);
}
};
// Close the message when in permanent display mode
function handleKbdClose(oEvent) {
var bShift = oEvent.shiftKey;
var bMetaKey = Device.os.macintosh ? oEvent.metaKey : oEvent.ctrlKey;
var oPopup = this._aPopups[0];
if (oEvent.code === "Escape" || (bShift && bMetaKey && oEvent.code === "KeyM")) {
oEvent.preventDefault();
setTimeout(function() {
this._mSettings.opener && this._mSettings.opener.focus();
}.bind(this), 0);
oPopup.close();
}
}
/* =========================================================== */
/* API methods */
/* =========================================================== */
/**
* Creates and displays a simple message toast notification message with the given text, and optionally other options.
*
* The only mandatory parameter is <code>sMessage</code>.
*
* @param {string} sMessage The message to be displayed.
* @param {object} [mOptions] Object which can contain all other options. Not all entries in this object are required. This property is optional.
* @param {int} [mOptions.duration=3000] Time in milliseconds before the close animation starts. Needs to be a finite positive nonzero integer.
* @param {sap.ui.core.CSSSize} [mOptions.width='15em'] The width of the message toast, this value can be provided in %, em, px and all possible CSS measures.
* @param {sap.ui.core.Popup.Dock} [mOptions.my='CenterBottom'] Specifies which point of the message toast should be aligned (e.g. <code>Dock.LeftTop</code> To use as align point the left top corner of the message toast).
* @param {sap.ui.core.Popup.Dock} [mOptions.at='CenterBottom'] Specifies the point of the reference element to which the message toast should be aligned (e.g. <code>Dock.RightBottom</code> To position the message toast according to the bottom right corner of the reference element).
* @param {sap.ui.core.Control|Element|jQuery|Window} [mOptions.of=window] Specifies the reference element to which the message toast should be aligned, by default it is aligned to the browser visual viewport.
* @param {string} [mOptions.offset='0 0'] The offset relative to the docking point, specified as a string with space-separated pixel values (e.g. "10 5" to move the message toast 10 pixels to the right and 5 pixels to the bottom).
* @param {string} [mOptions.collision='fit fit'] Specifies how the position of the message toast should be adjusted in case it overflows the screen in some direction. Possible values “fit”, “flip”, “none”, or a pair for horizontal and vertical e.g. "fit flip”, "fit none".
* @param {function} [mOptions.onClose=null] Function to be called when the message toast closes.
* @param {boolean} [mOptions.autoClose=true] Specify whether the message toast should close as soon as the end user touches the screen.
* @param {string} [mOptions.animationTimingFunction='ease'] Describes how the close animation will progress. Possible values "ease", "linear", "ease-in", "ease-out", "ease-in-out".
* @param {int} [mOptions.animationDuration=1000] Time in milliseconds that the close animation takes to complete. Needs to be a finite positive integer. For not animation set to 0.
* @param {boolean} [mOptions.closeOnBrowserNavigation=true] Specifies if the message toast closes on browser navigation.
* @public
*/
MessageToast.show = function(sMessage, mOptions) {
// disable opening of toasts then notoasts is set to true
// required for performance measurements
if (/sap-ui-xx-no-toasts=true/.test(document.location.search)) {
return;
}
var oOpener = Element.closestTo(document.activeElement);
var oUI5Area = oOpener && oOpener.getUIArea && oOpener.getUIArea();
var oAccSpan;
var that = MessageToast,
mSettings = jQuery.extend({}, MessageToast._mSettings, { message: sMessage }),
oPopup = new Popup(),
iPos,
oMessageToastDomRef,
sPointerEvents = "mousedown." + CSSCLASS + " touchstart." + CSSCLASS,
iMouseLeaveTimeoutId;
MessageToast._mSettings.opener = oOpener;
// Find the upper-most parent to attach the keyboard shortcut as we need to be
// able to open the message no matter where the focus is currently
if (!this._oRootNode || (this._oRootNode && oUI5Area && oUI5Area.getRootNode() !== this._oRootNode)) {
this._oRootNode = oUI5Area ? oUI5Area.getRootNode() : document.documentElement;
}
mOptions = normalizeOptions(mOptions);
// merge mOptions into mSettings
jQuery.extend(mSettings, mOptions);
// validate all settings
MessageToast._validateSettings(mSettings);
// create the message toast HTML markup
oMessageToastDomRef = createHTMLMarkup(mSettings);
// save MessageToast pop-up instance and the position,
// to be used inside fnMTAttachClosed closure
iPos = MessageToast._aPopups.push(oPopup) - 1;
// sets the content of the pop-up
oPopup.setContent(oMessageToastDomRef);
// sets the position of the pop-up
oPopup.setPosition(mSettings.my, mSettings.at, mSettings.of, mSettings.offset, mSettings.collision);
// sets the animation functions to use for opening and closing the message toast
oPopup.setAnimations(function fnMessageToastOpen($MessageToast, iDuration, fnOpened) {
fnOpened();
}, function fnMessageToastClose($MessageToastDomRef, iDuration, fnClose) {
that._setCloseAnimation($MessageToastDomRef, iDuration, fnClose, mSettings);
});
oPopup.setShadow(false);
oPopup.__bAutoClose = mSettings.autoClose;
if (mSettings.closeOnBrowserNavigation) {
// add the pop-up instance to the InstanceManager to handle browser back navigation
InstanceManager.addPopoverInstance(oPopup);
}
// do not bind if already bound
if (!MessageToast._bBoundedEvents) {
// bind to the resize event to handle orientation change and resize events
jQuery(window).on("resize." + CSSCLASS, MessageToast._handleResizeEvent.bind(MessageToast));
jQuery(document).on(sPointerEvents, MessageToast._handleMouseDownEvent.bind(MessageToast));
MessageToast._bBoundedEvents = true;
}
// Focus invisible span to avoid double announcement in NVDA
oAccSpan = document.createElement("span");
oAccSpan.setAttribute("tabIndex", 0);
oAccSpan.setAttribute("class", "sapMMessageToastHiddenFocusable");
oPopup.getContent().prepend(oAccSpan);
if (this._oRootNode) {
this._oRootNode.removeEventListener("keydown", that._fnKeyDown.bind(that));
this._oRootNode.addEventListener("keydown", that._fnKeyDown.bind(that));
oAccSpan.addEventListener("keydown", handleKbdClose.bind(this));
}
// opens the popup's content at the position specified via #setPosition
oPopup.open();
MessageToast._iOpenedPopups++;
function handleMTClosed() {
InstanceManager.removePopoverInstance(that._aPopups[iPos]);
jQuery(that._aPopups[iPos].getContent()).remove();
that._aPopups[iPos].detachClosed(handleMTClosed);
that._aPopups[iPos].destroy();
that._aPopups[iPos] = null;
that._iOpenedPopups--;
if (that._iOpenedPopups === 0) {
that._aPopups = [];
jQuery(window).off("resize." + CSSCLASS);
jQuery(document).off(sPointerEvents);
that._bBoundedEvents = false;
}
if (typeof mSettings.onClose === "function") {
mSettings.onClose.call(that);
}
}
// attach event handler fnFunction to the "closed" event
oPopup.attachClosed(handleMTClosed);
// close the message toast
this._iCloseTimeoutId = setTimeout(oPopup["close"].bind(oPopup), mSettings.duration);
function fnClearTimeout() {
clearTimeout(that._iCloseTimeoutId);
that._iCloseTimeoutId = null;
function fnMouseLeave() {
iMouseLeaveTimeoutId = setTimeout(oPopup["close"].bind(oPopup), mSettings.duration);
oPopup.getContent().removeEventListener("mouseleave", fnMouseLeave);
}
oPopup.getContent().addEventListener("mouseleave", fnMouseLeave);
clearTimeout(iMouseLeaveTimeoutId);
iMouseLeaveTimeoutId = null;
}
oPopup.getContent().addEventListener("touchstart", fnClearTimeout);
oPopup.getContent().addEventListener("mouseover", fnClearTimeout);
// WP 8.1 fires mouseleave event on tap
if (Device.system.desktop) {
oPopup.getContent().addEventListener("mouseleave", function () {
if (document.activeElement !== oPopup.getContent()) {
this._iCloseTimeoutId = setTimeout(oPopup["close"].bind(oPopup), mSettings.duration);
}
});
}
};
MessageToast.toString = function() {
return "sap.m.MessageToast";
};
return MessageToast;
}, /* bExport= */ true);