UNPKG

@ui5/webcomponents-localization

Version:
539 lines (524 loc) 19.6 kB
/*! * copyright */ import assert from "../../base/assert.js"; import BaseConfig from "../../base/config.js"; import BaseEvent from "../../base/Event.js"; import Eventing from "../../base/Eventing.js"; import Log from "../../base/Log.js"; import Localization from "../../base/i18n/Localization.js"; import deepEqual from "../../base/util/deepEqual.js"; import ThemeHelper from "./theming/ThemeHelper.js"; const oWritableConfig = BaseConfig.getWritableInstance(); const oEventing = new Eventing(); let mChanges; let 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 === "") { const mDefaultThemeInfo = ThemeHelper.getDefaultThemeInfo(); sTheme = `${mDefaultThemeInfo.DEFAULT_THEME}${mDefaultThemeInfo.DARK_MODE ? "_dark" : ""}`; } // 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) { if (sTheme.indexOf("@") !== -1) { throw new TypeError("Providing a theme root as part of the theme parameter is not allowed."); } const bFireChange = !mChanges; mChanges ??= {}; const sOldTheme = Theming.getTheme(); oWritableConfig.set("sapTheme", sTheme); const sNewTheme = Theming.getTheme(); const bThemeChanged = sOldTheme !== sNewTheme; if (bThemeChanged) { mChanges.theme = { "new": sNewTheme, "old": sOldTheme }; } else { mChanges = undefined; } if (bFireChange) { 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 * @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_belize" 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_belize", ["my.own.library"], "https://mythemeserver.com/allThemes"); * </pre> * * This will cause the Belize 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_belize/library.css * https://openui5.hana.ondemand.com/resources/sap/ui/layout/themes/sap_belize/library.css * https://openui5.hana.ondemand.com/resources/sap/m/themes/sap_belize/library.css * https://mythemeserver.com/allThemes/my/own/library/themes/sap_belize/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 * @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 bFireChange = !mChanges; mChanges ??= {}; 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)) { 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(); } else { mChanges = undefined; } if (bFireChange) { fireChange(); } }, /** * 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 language. * @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 (oThemeManager) { if (oThemeManager.themeLoaded) { fnFunction.call(null, new BaseEvent(sId, { theme: Theming.getTheme() })); } else { oEventing.attachEventOnce(sId, fnFunction); } } else { fnFunction.call(null, new BaseEvent(sId, { theme: Theming.getTheme() })); } }, /** * 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 (oThemeManager) { if (oThemeManager.themeLoaded) { fnFunction.call(null, new BaseEvent(sId, { theme: Theming.getTheme() })); } } else { 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 */ /** * The theme applied 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); }, /** * Fired when a scope class has been added or removed on a control/element * by using the custom style class API <code>addStyleClass</code>, * <code>removeStyleClass</code> or <code>toggleStyleClass</code>. * * Scope classes are defined by the library theme parameters coming from the * current theme. * * <b>Note:</b> The event will only be fired after the * <code>sap.ui.core.theming.Parameters</code> module has been loaded. * By default this is not the case. * * @name module:sap/ui/core/Theming.themeScopingChanged * @event * @param {module:sap/ui/core/Theming$ThemeScopingChangedEvent} oEvent * @private * @ui5-restricted SAPUI5 Distribution Layer Libraries * @since 1.118.0 */ /** * The theme scoping change Event. * * @typedef {object} module:sap/ui/core/Theming$ThemeScopingChangedEvent * @property {array} scopes An array containing all changed scopes. * @property {boolean} added Whether the scope was added or removed. * @property {sap.ui.core.Element} element The UI5 element the scope has changed for. * @private * @ui5-restricted sap.ui.core.theming.ThemeManager * @since 1.118.0 */ /** * Attaches the <code>fnFunction</code> event handler to the {@link #event:themeScopingChanged change} event * of <code>sap.ui.core.Theming</code>. * * @param {function(module:sap/ui/core/Theming$ThemeScopingChangedEvent)} fnFunction The function to be called when the event occurs * @private * @ui5-restricted SAPUI5 Distribution Layer Libraries * @since 1.118.0 */ attachThemeScopingChanged: fnFunction => { oEventing.attachEvent("themeScopingChanged", fnFunction); }, /** * Detaches event handler <code>fnFunction</code> from the {@link #event:themeScopingChanged change} event of * this <code>sap.ui.core.Theming</code>. * * @param {function(module:sap/ui/core/Theming$ThemeScopingChangedEvent)} fnFunction Function to be called when the event occurs * @private * @ui5-restricted SAPUI5 Distribution Layer Libraries * @since 1.118.0 */ detachThemeScopingChanged: fnFunction => { oEventing.detachEvent("themeScopingChanged", fnFunction); }, /** * Fire themeScopingChanged event. * * @param {Object<string,array|boolean|sap.ui.core.Element>} mParameters Function to be called when the event occurs * @private * @ui5-restricted SAPUI5 Distribution Layer Libraries * @since 1.118.0 */ fireThemeScopingChanged: mParameters => { oEventing.fireEvent("themeScopingChanged", mParameters); }, /** * Notify content density changes * * @public * @since 1.118.0 */ notifyContentDensityChanged: () => { fireApplied({ theme: Theming.getTheme() }); }, /** Register a ThemeManager instance * @param {sap.ui.core.theming.ThemeManager} oManager The ThemeManager to register. * @private * @ui5-restricted sap.ui.core.theming.ThemeManager * @since 1.118.0 */ registerThemeManager: oManager => { oThemeManager = oManager; oThemeManager._attachThemeApplied(function (oEvent) { fireApplied(BaseEvent.getParameters(oEvent)); }); // handle RTL changes Localization.attachChange(function (oEvent) { var bRTL = oEvent.rtl; if (bRTL !== undefined) { oThemeManager._updateThemeUrls(Theming.getTheme()); } }); } }; function fireChange() { if (mChanges) { oEventing.fireEvent("change", mChanges); mChanges = undefined; } } 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(), globalThis.location.href).origin; } catch (error) { Log.error("[FUTURE FATAL] sapAllowedThemeOrigin provides invalid theme origin: " + sAllowedOrigin); 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, globalThis.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, globalThis.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; } export default Theming;