@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
648 lines (591 loc) • 22.1 kB
JavaScript
/*!
* copyright
*/
sap.ui.define([
"sap/base/assert",
"sap/base/config",
"sap/base/Event",
"sap/base/Eventing",
"sap/base/future",
"sap/base/Log",
"sap/base/util/deepEqual",
"sap/base/util/LoaderExtensions",
"sap/ui/base/OwnStatics",
"sap/ui/core/theming/ThemeHelper"
], function(
assert,
BaseConfig,
BaseEvent,
Eventing,
future,
Log,
deepEqual,
LoaderExtensions,
OwnStatics,
ThemeHelper
) {
"use strict";
const oWritableConfig = BaseConfig.getWritableInstance();
const oEventing = new Eventing();
// Remember the initial favicon path in case there was already a favicon provided
const sInitialFaviconPath = document.querySelector("link[rel=icon]")?.getAttribute("href");
let pThemeManager, oThemeManager;
/**
* Provides theming related API
*
* @alias module:sap/ui/core/Theming
* @namespace
* @public
* @since 1.118
*/
const Theming = {
/**
* Returns the theme name
* @return {string} the theme name
* @public
* @since 1.118
*/
getTheme: () => {
// analyze theme parameter
let sTheme = oWritableConfig.get({
name: "sapTheme",
type: oWritableConfig.Type.String,
defaultValue: oWritableConfig.get({
name: "sapUiTheme",
type: oWritableConfig.Type.String,
external: true
}),
external: true
});
// Empty string is a valid value wrt. the <String> type.
// An empty string is equivalent to "no theme given" here.
// We apply the default, but also automatically detect the dark mode.
if (sTheme === "") {
sTheme = ThemeHelper.validateAndFallbackTheme();
}
// It's only possible to provide a themeroot via theme parameter using
// the initial config provider such as Global-, Bootstrap-, Meta- and
// URLConfigurationProvider. The themeroot is also only validated against
// allowedThemeOrigin in this case.
const iIndex = sTheme.indexOf("@");
if (iIndex >= 0) {
const sThemeRoot = validateThemeRoot(sTheme.slice(iIndex + 1));
sTheme = iIndex > 0 ? sTheme.slice(0, iIndex) : sTheme;
if (sThemeRoot !== Theming.getThemeRoot(sTheme)) {
Theming.setThemeRoot(sTheme, sThemeRoot);
}
}
// validate theme and fallback to the fixed default, in case the configured theme is not valid
sTheme = ThemeHelper.validateAndFallbackTheme(sTheme, Theming.getThemeRoot(sTheme));
return sTheme;
},
/**
* Allows setting the theme name
* @param {string} sTheme the theme name
* @public
* @since 1.118
*/
setTheme: (sTheme) => {
if (sTheme && sTheme.indexOf("@") !== -1) {
throw new TypeError("Providing a theme root as part of the theme parameter is not allowed.");
}
const sOldTheme = Theming.getTheme();
oWritableConfig.set("sapTheme", sTheme);
const sNewTheme = Theming.getTheme();
const bThemeChanged = sOldTheme !== sNewTheme;
if (bThemeChanged) {
const mChanges = {
theme: {
"new": sNewTheme,
"old": sOldTheme
}
};
fireChange(mChanges);
}
if (!oThemeManager && bThemeChanged) {
fireApplied({theme: sNewTheme});
}
},
/**
*
* @param {string} sTheme The Theme
* @param {string} [sLib] An optional library name
* @returns {string} The themeRoot if configured
* @private
* @ui5-restricted sap.ui.core.theming.ThemeManager, sap.ushell
* @since 1.118
*/
getThemeRoot: (sTheme, sLib) => {
const oThemeRoots = oWritableConfig.get({
name: "sapUiThemeRoots",
type: oWritableConfig.Type.MergedObject
});
let sThemeRoot;
sTheme ??= Theming.getTheme();
if (oThemeRoots[sTheme] && typeof oThemeRoots[sTheme] === "string") {
sThemeRoot = oThemeRoots[sTheme];
} else if (oThemeRoots[sTheme] && typeof oThemeRoots[sTheme] === "object") {
sThemeRoot = oThemeRoots[sTheme][sLib] || oThemeRoots[sTheme][""];
}
return sThemeRoot;
},
/**
* Defines the root directory from below which UI5 should load the theme with the given name.
* Optionally allows restricting the setting to parts of a theme covering specific control libraries.
*
* Example:
* <pre>
* Theming.setThemeRoot("my_theme", "https://mythemeserver.com/allThemes");
* Theming.setTheme("my_theme");
* </pre>
*
* will cause the following file to be loaded (assuming that the bootstrap is configured to load
* libraries <code>sap.m</code> and <code>sap.ui.layout</code>):
* <pre>
* https://mythemeserver.com/allThemes/sap/ui/core/themes/my_theme/library.css
* https://mythemeserver.com/allThemes/sap/ui/layout/themes/my_theme/library.css
* https://mythemeserver.com/allThemes/sap/m/themes/my_theme/library.css
* </pre>
*
* If parts of the theme are at different locations (e.g. because you provide a standard theme
* like "sap_horizon" for a custom control library and this self-made part of the standard theme is at a
* different location than the UI5 resources), you can also specify for which control libraries the setting
* should be used, by giving an array with the names of the respective control libraries as second parameter:
* <pre>
* Theming.setThemeRoot("sap_horizon", ["my.own.library"], "https://mythemeserver.com/allThemes");
* </pre>
*
* This will cause the Horizon theme to be loaded from the UI5 location for all standard libraries.
* Resources for styling the <code>my.own.library</code> controls will be loaded from the configured
* location:
* <pre>
* https://openui5.hana.ondemand.com/resources/sap/ui/core/themes/sap_horizon/library.css
* https://openui5.hana.ondemand.com/resources/sap/ui/layout/themes/sap_horizon/library.css
* https://openui5.hana.ondemand.com/resources/sap/m/themes/sap_horizon/library.css
* https://mythemeserver.com/allThemes/my/own/library/themes/sap_horizon/library.css
* </pre>
*
* If the custom theme should be loaded initially (via bootstrap attribute), the <code>themeRoots</code>
* property of the <code>window["sap-ui-config"]</code> object must be used instead of calling
* <code>Theming.setThemeRoot(...)</code> in order to configure the theme location early enough.
*
* @param {string} sThemeName Name of the theme for which to configure the location
* @param {string} sThemeBaseUrl Base URL below which the CSS file(s) will be loaded from
* @param {string[]} [aLibraryNames] Optional library names to which the configuration should be restricted
* @param {boolean} [bForceUpdate=false] Force updating URLs of currently loaded theme
* @private
* @ui5-restricted sap.ui.core.Core, sap.ushell
* @since 1.118
*/
setThemeRoot: (sThemeName, sThemeBaseUrl, aLibraryNames, bForceUpdate) => {
assert(typeof sThemeName === "string", "sThemeName must be a string");
assert(typeof sThemeBaseUrl === "string", "sThemeBaseUrl must be a string");
const oThemeRootConfigParam = {
name: "sapUiThemeRoots",
type: oWritableConfig.Type.MergedObject
};
// Use get twice, for a deep copy of themeRoots object
// we add a new default "empty object" with each call, so we don't accidentally share it
const mOldThemeRoots = oWritableConfig.get(Object.assign(oThemeRootConfigParam, {defaultValue: {}}));
const mNewThemeRoots = oWritableConfig.get(Object.assign(oThemeRootConfigParam, {defaultValue: {}}));
// normalize parameters
if (typeof aLibraryNames === "boolean") {
bForceUpdate = aLibraryNames;
aLibraryNames = undefined;
}
mNewThemeRoots[sThemeName] ??= {};
// Normalize theme-roots to an object in case it was initially given as a string.
// We only check newThemeRoots, since both old and new are identical at this point.
if (typeof mNewThemeRoots[sThemeName] === "string") {
mNewThemeRoots[sThemeName] = { "": mNewThemeRoots[sThemeName]};
mOldThemeRoots[sThemeName] = { "": mOldThemeRoots[sThemeName]};
}
if (aLibraryNames) {
// registration of URL for several libraries
for (let i = 0; i < aLibraryNames.length; i++) {
const lib = aLibraryNames[i];
mNewThemeRoots[sThemeName][lib] = sThemeBaseUrl;
}
} else {
// registration of theme default base URL
mNewThemeRoots[sThemeName][""] = sThemeBaseUrl;
}
if (!deepEqual(mOldThemeRoots, mNewThemeRoots)) {
const mChanges = {};
oWritableConfig.set("sapUiThemeRoots", mNewThemeRoots);
if (aLibraryNames) {
mChanges["themeRoots"] = {
"new": Object.assign({}, mNewThemeRoots[sThemeName]),
"old": Object.assign({}, mOldThemeRoots[sThemeName])
};
} else {
mChanges["themeRoots"] = {
"new": sThemeBaseUrl,
"old": mOldThemeRoots[sThemeName]?.[""]
};
}
mChanges["themeRoots"].forceUpdate = bForceUpdate && sThemeName === Theming.getTheme();
fireChange(mChanges);
}
},
/**
* Derives the favicon path based on the configuration and the current theme.
*
* @returns {Promise<string>} The favicon path
* @private
* @since 1.135
*/
getFavicon: async () => {
const sDefaultFavicon = sInitialFaviconPath || sap.ui.require.toUrl("sap/ui/core/themes/base/icons/favicon.ico");
const sFaviconFromConfig = oWritableConfig.get({
name: "sapUiFavicon",
type: (vValue) => {
const sValue = vValue.toString();
if (["", "false"].includes(sValue.toLowerCase())) {
return false;
} else if (["x", "true"].includes(sValue.toLowerCase())) {
return true;
}
if (!vValue || (new URL(vValue, window.location.origin)).href.startsWith(window.location.origin)) {
return sValue;
} else {
Log.error("Absolute URLs are not allowed for favicon. The configured favicon will be ignored.", undefined, "sap.ui.core.theming.Theming");
return true;
}
}
});
if (!sFaviconFromConfig) {
return sFaviconFromConfig;
}
if (typeof sFaviconFromConfig === "string") {
return sFaviconFromConfig;
} else if (oThemeManager && !ThemeHelper.isStandardTheme(Theming.getTheme())) {
const sFaviconPath = await new Promise((res) => {
sap.ui.require(["sap/ui/core/theming/Parameters"], (Parameters) => {
const sFavicon = Parameters.get({
name: "sapUiFavicon",
_restrictedParseUrls: true,
callback: (sFavicon) => {
res(sFavicon);
}
});
if (sFavicon !== undefined) {
res(sFavicon);
}
});
});
return sFaviconPath || sDefaultFavicon;
}
return sDefaultFavicon;
},
/**
* Sets the favicon. The path must be relative to the current origin. Absolute URLs are not allowed.
*
* @param {string|boolean|undefined} vFavicon A string containing a specific relative path to the favicon,
* 'true' to use a favicon from custom theme or the default favicon in case no custom favicon is maintained,
* 'false' or undefined to disable the favicon
* @returns {Promise<undefined>} A promise that resolves when the favicon has been set with undefined
* @public
* @since 1.135
*/
setFavicon: async (vFavicon) => {
if (typeof vFavicon === "string" && !(new URL(vFavicon, window.location.origin)).href.startsWith(window.location.origin)) {
throw new TypeError("Path to favicon must be relative to the current origin");
}
oWritableConfig.set("sapUiFavicon", vFavicon);
const sNewFaviconPath = await Theming.getFavicon();
if (sNewFaviconPath) {
await new Promise((res, rej) => {
sap.ui.require(["sap/ui/util/Mobile"], (Mobile) => {
Mobile.setIcons({
favicon: sNewFaviconPath
});
res();
}, rej);
});
} else {
document.querySelector("link[rel=icon]")?.remove();
}
},
/**
* Fired after a theme has been applied.
*
* More precisely, this event is fired when any of the following conditions is met:
* <ul>
* <li>the initially configured theme has been applied after core init</li>
* <li>the theme has been changed and is now applied (see {@link #applyTheme})</li>
* <li>a library has been loaded dynamically after core init (e.g. with
* <code>sap.ui.core.Lib.load(...)</code> and the current theme
* has been applied for it</li>
* </ul>
*
* For the event parameters please refer to {@link module:sap/ui/core/Theming$AppliedEvent}.
*
* @name module:sap/ui/core/Theming.applied
* @event
* @param {module:sap/ui/core/Theming$AppliedEvent} oEvent
* @public
* @since 1.118.0
*/
/**
* The theme applied Event.
*
* @typedef {object} module:sap/ui/core/Theming$AppliedEvent
* @property {string} theme The newly set theme.
* @public
* @since 1.118.0
*/
/**
* Attaches event handler <code>fnFunction</code> to the {@link #event:applied applied} event
*
* The given handler is called when the the applied event is fired. If the theme is already applied
* the handler will be called immediately.
*
* @param {function(module:sap/ui/core/Theming$AppliedEvent)} fnFunction The function to be called, when the event occurs
* @private
* @ui5-restricted sap.ui.core.Core
* @since 1.118.0
*/
attachAppliedOnce: (fnFunction) => {
const sId = "applied";
if (!pThemeManager || oThemeManager?.themeLoaded) {
fnFunction.call(null, new BaseEvent(sId, {theme: Theming.getTheme()}));
} else {
oEventing.attachEventOnce(sId, fnFunction);
}
},
/**
* Attaches event handler <code>fnFunction</code> to the {@link #event:applied applied} event.
*
* The given handler is called when the the applied event is fired. If the theme is already applied
* the handler will be called immediately. The handler stays attached to the applied event for future
* theme changes.
*
* @param {function(module:sap/ui/core/Theming$AppliedEvent)} fnFunction The function to be called, when the event occurs
* @public
* @since 1.118.0
*/
attachApplied: (fnFunction) => {
const sId = "applied";
oEventing.attachEvent(sId, fnFunction);
if (!pThemeManager || oThemeManager?.themeLoaded) {
fnFunction.call(null, new BaseEvent(sId, {theme: Theming.getTheme()}));
}
},
/**
* Detaches event handler <code>fnFunction</code> from the {@link #event:applied applied} event
*
* The passed function must match the one used for event registration.
*
* @param {function(module:sap/ui/core/Theming$AppliedEvent)} fnFunction The function to be called, when the event occurs
* @public
* @since 1.118.0
*/
detachApplied: (fnFunction) => {
oEventing.detachEvent("applied", fnFunction);
},
/**
* The <code>change</code> event is fired, when the configuration options are changed.
*
* @name module:sap/ui/core/Theming.change
* @event
* @param {module:sap/ui/core/Theming$ChangeEvent} oEvent
* @private
* @ui5-restricted sap.ui.core.theming.ThemeManager
* @since 1.118.0
*/
/**
* Notify content density changes
*
* @public
* @since 1.118.0
*/
notifyContentDensityChanged: () => {
fireApplied({theme: Theming.getTheme()});
}
};
function fireChange(mChanges) {
if (mChanges) {
oEventing.fireEvent("change", mChanges);
}
}
function fireApplied(oTheme) {
oEventing.fireEvent("applied", oTheme);
}
function validateThemeOrigin(sOrigin, bNoProtocol) {
const sAllowedOrigins = oWritableConfig.get({name: "sapAllowedThemeOrigins", type: oWritableConfig.Type.String});
return !!sAllowedOrigins?.split(",").some((sAllowedOrigin) => {
try {
sAllowedOrigin = bNoProtocol && !sAllowedOrigin.startsWith("//") ? "//" + sAllowedOrigin : sAllowedOrigin;
return sAllowedOrigin === "*" || sOrigin === new URL(sAllowedOrigin.trim(), window.location.href).origin;
} catch (error) {
future.errorThrows("sapAllowedThemeOrigins provides invalid theme origin: " + sAllowedOrigin, {cause: error});
return false;
}
});
}
function validateThemeRoot(sThemeRoot) {
const bNoProtocol = sThemeRoot.startsWith("//");
let oThemeRoot,
sPath;
try {
// Remove search query as they are not supported for themeRoots/resourceRoots
oThemeRoot = new URL(sThemeRoot, window.location.href);
oThemeRoot.search = "";
// If the URL is absolute, validate the origin
if (oThemeRoot.origin && validateThemeOrigin(oThemeRoot.origin, bNoProtocol)) {
sPath = oThemeRoot.toString();
} else {
// For relative URLs or not allowed origins
// ensure same origin and resolve relative paths based on origin
oThemeRoot = new URL(oThemeRoot.pathname, window.location.href);
sPath = oThemeRoot.toString();
}
// legacy compatibility: support for "protocol-less" urls (previously handled by URI.js)
if (bNoProtocol) {
sPath = sPath.replace(oThemeRoot.protocol, "");
}
sPath += (sPath.endsWith('/') ? '' : '/') + "UI5/";
} catch (e) {
// malformed URL are also not accepted
}
return sPath;
}
/**
* Makes sure to register the correct module path for the given library and theme
* in case a themeRoot has been defined.
*
* @param {string} sLibName Library name (dot separated)
* @param {string} sThemeName Theme name
*
* @returns {string} libThemePath Returns the path for a library theme
*/
function ensureThemeRoot(sLibName, sThemeName) {
let sThemeRoot = Theming.getThemeRoot(sThemeName, sLibName);
const libThemePath = `${sLibName}.themes.${sThemeName}`.replace(/\./g, "/");
if (sThemeRoot) {
// check whether for this combination (theme+lib) a URL is registered or for this theme a default location is registered
sThemeRoot += `${sThemeRoot.slice( -1) == "/" ? "" : "/"}${libThemePath}/`;
LoaderExtensions.registerResourcePath(libThemePath, sThemeRoot);
}
return libThemePath;
}
function loadThemeManager() {
pThemeManager ??= new Promise(function (resolve, reject) {
sap.ui.require([
"sap/ui/core/theming/ThemeManager"
], function (ThemeManager) {
oThemeManager = ThemeManager;
resolve(ThemeManager);
}, reject);
});
return pThemeManager;
}
Theming.attachApplied(() => {
Theming.getFavicon().then((favicon) => {
if (favicon) {
sap.ui.require(["sap/ui/util/Mobile"], (Mobile) => {
Mobile.setIcons({ favicon });
});
}
});
});
OwnStatics.set(Theming, {
/**
* Includes a library theme into the current page (if a variant is specified it
* will include the variant library theme) and ensure theme root
* @param {object} [oLibThemingInfo] to be used only by the Core
* @private
* @ui5-restricted sap.ui.core
*/
includeLibraryTheme: function(oLibThemingInfo) {
const { libName } = oLibThemingInfo;
// ensure to register correct library theme module path even when "preloadLibCss" prevents
// including the library theme as controls might use it to calculate theme-specific URLs
ensureThemeRoot(libName, Theming.getTheme());
// also ensure correct theme root for the library's base theme which might be relevant in some cases
// (e.g. IconPool which includes font files from sap.ui.core base theme)
ensureThemeRoot(libName, "base");
if (oThemeManager) {
fireChange({
library: oLibThemingInfo
});
} else {
loadThemeManager().then(() => {
fireChange({
library: oLibThemingInfo
});
});
}
},
/**
* Returns the URL of the folder in which the CSS file for the given theme and the given library is located.
*
* @param {string} sLibName Library name (dot separated)
* @param {string} sThemeName Theme name
* @returns {string} module path URL (ends with a slash)
* @private
* @ui5-restricted sap.ui.core,sap.ui.support.supportRules.report.DataCollector
*/
getThemePath: function(sLibName, sThemeName) {
// make sure to register correct theme module path in case themeRoots are defined
const libThemePath = ensureThemeRoot(sLibName, sThemeName);
// use the library location as theme location
return sap.ui.require.toUrl(`${libThemePath}/`);
},
/**
* The theme change Event.
*
* @typedef {object} module:sap/ui/core/Theming$ChangeEvent
* @property {Object<string,string>} [theme] Theme object containing the old and the new theme
* @property {string} [theme.new] The new theme.
* @property {string} [theme.old] The old theme.
* @property {Object<string,Object<string,string>|boolean>} [themeRoots] ThemeRoots object containing the old and the new ThemeRoots
* @property {object} [themeRoots.new] The new ThemeRoots.
* @property {object} [themeRoots.old] The old ThemeRoots.
* @property {boolean} [themeRoots.forceUpdate] Whether an update of currently loaded theme URLS should be forced
* @private
* @ui5-restricted sap.ui.core.theming.ThemeManager
* @since 1.118.0
*/
/**
* Attaches the <code>fnFunction</code> event handler to the {@link #event:change change} event
* of <code>sap/ui/core/Theming</code>.
*
* @param {function(module:sap/ui/core/Theming$ChangeEvent)} fnFunction The function to be called when the event occurs
* @private
* @ui5-restricted sap.ui.core.theming.ThemeManager
* @since 1.118.0
*/
attachChange: (fnFunction) => {
oEventing.attachEvent("change", fnFunction);
},
/**
* Detaches event handler <code>fnFunction</code> from the {@link #event:change change} event of
* this <code>sap/ui/core/Theming</code>.
*
* @param {function(module:sap/ui/core/Theming$ChangeEvent)} fnFunction Function to be called when the event occurs
* @private
* @ui5-restricted sap.ui.core.theming.ThemeManager
* @since 1.118.0
*/
detachChange: (fnFunction) => {
oEventing.detachEvent("change", fnFunction);
},
/**
* Register a ThemeManager instance
* @param {function} attachThemeApplied Callback function to register fireThemeApplied.
* @private
* @ui5-restricted sap.ui.core.theming.ThemeManager
* @since 1.118.0
*/
registerThemeManager: (attachThemeApplied) => {
loadThemeManager().then(() => {
attachThemeApplied(function(oEvent) {
fireApplied(BaseEvent.getParameters(oEvent));
});
});
}
});
return Theming;
});