@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
437 lines (357 loc) • 14.6 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides class sap.ui.core.ThemeCheck
sap.ui.define([
'sap/ui/Device',
'sap/ui/base/Object',
"sap/base/Log",
"sap/ui/dom/includeStylesheet",
"sap/ui/thirdparty/jquery"
],
function(Device, BaseObject, Log, includeStylesheet, jQuery) {
"use strict";
var maxThemeCheckCycles = 150;
/**
* Creates a new ThemeCheck object.
*
* @class Helper class used by the UI5 Core to check whether the themes are applied correctly.
*
* It could happen that e.g. in onAfterRendering not all themes are available. In these cases the
* check waits until the CSS is applied and fires an onThemeChanged event.
*
* @extends sap.ui.base.Object
* @since 1.10.0
* @author SAP SE
* @private
* @alias sap.ui.core.ThemeCheck
*/
var ThemeCheck = BaseObject.extend("sap.ui.core.ThemeCheck", /** @lends sap.ui.core.ThemeCheck.prototype */ {
constructor : function(oCore) {
this._oCore = oCore;
this._iCount = 0; // Prevent endless loop
this._CUSTOMCSSCHECK = /\.sapUiThemeDesignerCustomCss/i;
this._CUSTOMID = "sap-ui-core-customcss";
this._customCSSAdded = false;
this._themeCheckedForCustom = null;
this._sFallbackTheme = null;
this._mThemeFallback = {};
this._oThemeMetaDataCheckElement = null;
},
getInterface : function() {
return this;
},
fireThemeChangedEvent : function(bOnlyOnInitFail) {
clear(this);
delayedCheckTheme.apply(this, [true]);
// Do not fire the event when the theme is already applied initially.
// bOnlyOnInitFail=true should only be passed from Core#init.
if (!bOnlyOnInitFail && !this._sThemeCheckId) {
this._oCore.fireThemeChanged({theme: this._oCore.getConfiguration().getTheme()});
}
}
});
ThemeCheck.themeLoaded = false;
function safeAccessSheetCssRules(sheet) {
try {
return sheet.cssRules;
} catch (e) {
// Firefox throws a SecurityError or InvalidAccessError if "sheet.cssRules"
// is accessed on a stylesheet with 404 response code.
// Most browsers also throw when accessing from a different origin (CORS).
return null;
}
}
function hasSheetCssRules(sheet) {
var aCssRules = safeAccessSheetCssRules(sheet);
return !!aCssRules && aCssRules.length > 0;
}
ThemeCheck.checkStyle = function(sId, bLog) {
var oStyle = document.getElementById(sId);
try {
var bNoLinkElement = false,
bLinkElementFinishedLoading = false,
bSheet = false,
bInnerHtml = false;
// Check if <link> element is missing (e.g. misconfigured library)
bNoLinkElement = !oStyle;
// Check if <link> element has finished loading (see jQuery.sap.includeStyleSheet)
bLinkElementFinishedLoading = !!(oStyle && (oStyle.getAttribute("data-sap-ui-ready") === "true" || oStyle.getAttribute("data-sap-ui-ready") === "false"));
// Check for "sheet" object and if rules are available
bSheet = !!(oStyle && oStyle.sheet && oStyle.sheet.href === oStyle.href && hasSheetCssRules(oStyle.sheet));
// Check for "innerHTML" content
bInnerHtml = !!(oStyle && oStyle.innerHTML && oStyle.innerHTML.length > 0);
// One of the previous four checks need to be successful
var bResult = bNoLinkElement || bSheet || bInnerHtml || bLinkElementFinishedLoading;
if (bLog) {
Log.debug("ThemeCheck: " + sId + ": " + bResult + " (noLinkElement: " + bNoLinkElement + ", sheet: " + bSheet + ", innerHtml: " + bInnerHtml + ", linkElementFinishedLoading: " + bLinkElementFinishedLoading + ")");
}
return bResult;
} catch (e) {
if (bLog) {
Log.error("ThemeCheck: " + sId + ": Error during check styles '" + sId + "'", e);
}
}
return false;
};
function clear(oThemeCheck){
ThemeCheck.themeLoaded = false;
if (oThemeCheck._sThemeCheckId) {
clearTimeout(oThemeCheck._sThemeCheckId);
oThemeCheck._sThemeCheckId = null;
oThemeCheck._iCount = 0;
oThemeCheck._sFallbackTheme = null;
oThemeCheck._mThemeFallback = {};
if (oThemeCheck._oThemeMetaDataCheckElement && oThemeCheck._oThemeMetaDataCheckElement.parentNode) {
oThemeCheck._oThemeMetaDataCheckElement.parentNode.removeChild(oThemeCheck._oThemeMetaDataCheckElement);
oThemeCheck._oThemeMetaDataCheckElement = null;
}
}
}
function checkTheme(oThemeCheck) {
var mLibs = oThemeCheck._oCore.getLoadedLibraries();
var sThemeName = oThemeCheck._oCore.getConfiguration().getTheme();
var sPath = oThemeCheck._oCore._getThemePath("sap.ui.core", sThemeName) + "custom.css";
var bIsStandardTheme = sThemeName.indexOf("sap_") === 0 || sThemeName === "base";
var res = true;
var aFailedLibs = [];
if (!!oThemeCheck._customCSSAdded && oThemeCheck._themeCheckedForCustom === sThemeName) {
// include custom style sheet here because it has already been added using jQuery.sap.includeStyleSheet
// hence, needs to be checked for successful inclusion, too
mLibs[oThemeCheck._CUSTOMID] = {};
}
function checkAndRemoveStyle(sPrefix, sLib) {
var currentRes = ThemeCheck.checkStyle(sPrefix + sLib, true);
if (currentRes) {
// removes all old stylesheets (multiple could exist if theme change was triggered
// twice in a short timeframe) once the new stylesheet has been loaded
var aOldStyles = document.querySelectorAll("link[data-sap-ui-foucmarker='" + sPrefix + sLib + "']");
if (aOldStyles.length > 0) {
for (var i = 0, l = aOldStyles.length; i < l; i++) {
aOldStyles[i].parentNode.removeChild(aOldStyles[i]);
}
Log.debug("ThemeCheck: Old stylesheets removed for library: " + sLib);
}
}
return currentRes;
}
function checkLib(lib) {
var sStyleId = "sap-ui-theme-" + lib;
var currentRes = checkAndRemoveStyle("sap-ui-theme-", lib);
if (currentRes && document.getElementById("sap-ui-themeskeleton-" + lib)) {
// remove also the skeleton if present in the DOM
currentRes = checkAndRemoveStyle("sap-ui-themeskeleton-", lib);
}
res = res && currentRes;
if (res) {
/* as soon as css has been loaded, look if there is a flag for custom css inclusion inside, but only
* if this has not been checked successfully before for the same theme
*/
if (oThemeCheck._themeCheckedForCustom != sThemeName) {
// custom css is supported for custom themes, so this check is skipped for standard themes
if (!bIsStandardTheme && checkCustom(oThemeCheck, lib)) {
// load custom css available at sap/ui/core/themename/custom.css
var sCustomCssPath = sPath;
// check for configured query parameters and add them if available
var sLibCssQueryParams = oThemeCheck._oCore._getLibraryCssQueryParams(mLibs["sap.ui.core"]);
if (sLibCssQueryParams) {
sCustomCssPath += sLibCssQueryParams;
}
includeStylesheet(sCustomCssPath, oThemeCheck._CUSTOMID);
oThemeCheck._customCSSAdded = true;
Log.debug("ThemeCheck: delivered custom CSS needs to be loaded, Theme not yet applied");
oThemeCheck._themeCheckedForCustom = sThemeName;
res = false;
return false;
} else {
// remove stylesheet once the particular class is not available (e.g. after theme switch)
/*check for custom theme was not successful, so we need to make sure there are no custom style sheets attached*/
var customCssLink = jQuery("LINK[id='" + oThemeCheck._CUSTOMID + "']");
if (customCssLink.length > 0) {
customCssLink.remove();
Log.debug("ThemeCheck: Custom CSS removed");
}
oThemeCheck._customCSSAdded = false;
}
}
}
// Collect all libs that failed to load and no fallback has been applied, yet.
// The fallback relies on custom theme metadata, so it is not done for standard themes
if (!bIsStandardTheme && currentRes && !oThemeCheck._mThemeFallback[lib]) {
var oStyle = document.getElementById(sStyleId);
// Check for error marker (data-sap-ui-ready=false) and that there are no rules
// to be sure the stylesheet couldn't be loaded at all.
// E.g. in case an @import within the stylesheet fails, the error marker will
// also be set, but in this case no fallback should be done as there is a (broken) theme
if (oStyle && oStyle.getAttribute("data-sap-ui-ready") === "false" &&
!(oStyle.sheet && hasSheetCssRules(oStyle.sheet))
) {
aFailedLibs.push(lib);
}
}
}
jQuery.each(mLibs, checkLib);
// Try to load a fallback theme for all libs that couldn't be loaded
if (aFailedLibs.length > 0) {
// Only retrieve the fallback theme once per ThemeCheck cycle
if (!oThemeCheck._sFallbackTheme) {
if (!oThemeCheck._oThemeMetaDataCheckElement) {
// Create dummy element to retrieve custom theme metadata which is applied
// via background-image data-uri
oThemeCheck._oThemeMetaDataCheckElement = document.createElement("style");
jQuery.each(mLibs, function(sLib) {
var sClassName = "sapThemeMetaData-UI5-" + sLib.replace(/\./g, "-");
oThemeCheck._oThemeMetaDataCheckElement.classList.add(sClassName);
});
document.head.appendChild(oThemeCheck._oThemeMetaDataCheckElement);
}
oThemeCheck._sFallbackTheme = getFallbackTheme(oThemeCheck._oThemeMetaDataCheckElement);
}
if (oThemeCheck._sFallbackTheme) {
aFailedLibs.forEach(function(lib) {
var sStyleId = "sap-ui-theme-" + lib;
var oStyle = document.getElementById(sStyleId);
Log.warning(
"ThemeCheck: Custom theme '" + sThemeName + "' could not be loaded for library '" + lib + "'. " +
"Falling back to its base theme '" + oThemeCheck._sFallbackTheme + "'."
);
// Change the URL to load the fallback theme
oThemeCheck._oCore._updateThemeUrl(oStyle, oThemeCheck._sFallbackTheme);
// remember the lib to prevent doing the fallback multiple times
// (if the fallback also can't be loaded)
oThemeCheck._mThemeFallback[lib] = true;
});
// Make sure to wait for the fallback themes to be loaded
res = false;
}
}
if (!res) {
Log.debug("ThemeCheck: Theme not yet applied.");
} else {
oThemeCheck._themeCheckedForCustom = sThemeName;
}
return res;
}
function getFallbackTheme(oThemeMetaDataCheckElement) {
function getThemeMetaData() {
var sDataUri = window.getComputedStyle(oThemeMetaDataCheckElement).getPropertyValue("background-image");
var aDataUriMatch = /\(["']?data:text\/plain;utf-8,(.*?)['"]?\)/i.exec(sDataUri);
if (!aDataUriMatch || aDataUriMatch.length < 2) {
return null;
}
var sMetaData = aDataUriMatch[1];
// decode only if necessary
if (sMetaData.charAt(0) !== "{" && sMetaData.charAt(sMetaData.length - 1) !== "}") {
try {
sMetaData = decodeURI(sMetaData);
} catch (ex) {
// ignore
}
}
// Remove superfluous escaping of double quotes
sMetaData = sMetaData.replace(/\\"/g, '"');
// Replace encoded spaces
sMetaData = sMetaData.replace(/%20/g, " ");
try {
return JSON.parse(sMetaData);
} catch (ex) {
return null;
}
}
var oThemeMetaData = getThemeMetaData();
if (oThemeMetaData && oThemeMetaData.Extends && oThemeMetaData.Extends[0]) {
return oThemeMetaData.Extends[0];
} else {
return null;
}
}
/* checks if a particular class is available
*/
function checkCustom (oThemeCheck, lib){
var cssFile = window.document.getElementById("sap-ui-theme-" + lib);
if (!cssFile) {
return false;
}
/*
Check if custom.css indication rule is applied to <link> element
The rule looks like this:
link[id^="sap-ui-theme-"]::after,
.sapUiThemeDesignerCustomCss {
content: '{"customcss" : true}';
}
First selector is to apply it to the <link> elements,
the second one for the Safari workaround (see below).
*/
var style = window.getComputedStyle(cssFile, ':after');
var content = style ? style.getPropertyValue('content') : null;
if (!content && Device.browser.safari) {
// Safari has a bug which prevents reading properties of hidden pseudo elements
// As a workaround: Add "sapUiThemeDesignerCustomCss" class on html element
// in order to get the computed "content" value and remove it again.
var html = document.documentElement;
html.classList.add("sapUiThemeDesignerCustomCss");
content = window.getComputedStyle(html, ":after").getPropertyValue("content");
html.classList.remove("sapUiThemeDesignerCustomCss");
}
if (content && content !== "none") {
try {
// Strip surrounding quotes (single or double depending on browser)
if (content[0] === "'" || content[0] === '"') {
content = content.substring(1, content.length - 1);
}
// Cast to boolean (returns true if string equals "true", otherwise false)
return content === "true";
} catch (e) {
// parsing error
Log.error("Custom check: Error parsing JSON string for custom.css indication.", e);
}
}
//***********************************
// Fallback legacy customcss check
//***********************************
/*
* checks if a particular class is available at the beginning of the stylesheet
*/
var aRules = cssFile.sheet ? safeAccessSheetCssRules(cssFile.sheet) : null;
if (!aRules || aRules.length === 0) {
Log.warning("Custom check: Failed retrieving a CSS rule from stylesheet " + lib);
return false;
}
// we should now have some rule name ==> try to match against custom check
for (var i = 0; (i < 2 && i < aRules.length) ; i++) {
if (oThemeCheck._CUSTOMCSSCHECK.test(aRules[i].selectorText)) {
return true;
}
}
return false;
}
function delayedCheckTheme(bFirst) {
this._iCount++;
var bEmergencyExit = this._iCount > maxThemeCheckCycles;
if (!checkTheme(this) && !bEmergencyExit) {
// Use dynamic delay to have a fast check for most use cases
// but not cause too much CPU usage for long running css requests
var iDelay;
if (this._iCount <= 100) {
iDelay = 2; // 1. Initial interval
} else if (this._iCount <= 110) {
iDelay = 500; // 2. After 100 cycles
} else {
iDelay = 1000; // 3. After another 10 cycles (about 5 seconds)
}
this._sThemeCheckId = setTimeout(delayedCheckTheme.bind(this), iDelay);
} else if (!bFirst) {
clear(this);
ThemeCheck.themeLoaded = true;
this._oCore.fireThemeChanged({theme: this._oCore.getConfiguration().getTheme()});
if (bEmergencyExit) {
Log.error("ThemeCheck: max. check cycles reached.");
}
} else {
ThemeCheck.themeLoaded = true;
}
}
return ThemeCheck;
});