@c8y/bootstrap
Version:
Bootstrap layer
411 lines • 15.8 kB
JavaScript
import { forEach, get, reduce, union, cloneDeep } from 'lodash';
import chroma from 'chroma-js';
import { applyThemePreferenceAndListenForChanges, applyOptionsToTheming } from '../theming/theming';
import { applyPreviewOptions } from '../branding-preview/branding-preview';
let staticOptionsCache;
let urlOptionsCache;
export function mergeOptions(inputOptions) {
const { urlOptions, staticOptions, dynamicOptions, localDynamicOptions, loadedLoginOptions, previewOptions } = inputOptions;
const languages = {
...(staticOptions.languages || {}),
...(localDynamicOptions.languages || {}),
...(dynamicOptions.languages || {}),
...(urlOptions.languages || {})
};
const remotes = getAllMFRemotes([
staticOptions,
localDynamicOptions,
dynamicOptions,
urlOptions,
previewOptions
]);
const options = {
versions: {
ng1: __VERSION_NG1__,
ngx: __VERSION_NGX__,
package: __VERSION_PACKAGE__
},
...staticOptions,
...localDynamicOptions,
...dynamicOptions,
...urlOptions,
...previewOptions,
remotes,
languages,
remoteModules: [],
...loadedLoginOptions
};
options.C8Y_INSTANCE_OPTIONS = { ...options }; // for compatability with c8yBase.getOptions in ng1-modules
// If no shades are defined, we auto generate shades. This was implemented with the
// re-design in 10.17. Branding editor should later define the shades. So long we
// auto generate shades out of the primary color and based on the referenceShades.
const shouldApplyShadeColors = hasBrandPrimary(options.brandingCssVars) && !hasAnyBrandShade(options.brandingCssVars);
if (shouldApplyShadeColors) {
options.brandingCssVars = getShadeColorBrandingCssVars(options.brandingCssVars);
}
return options;
}
export async function loadOptions() {
applyThemePreferenceAndListenForChanges();
const urlOptions = loadUrlOptions(); // used for debugging or preview
const staticOptions = loadStaticOptions();
const [dynamicOptions, localDynamicOptions, loadedLoginOptions] = await Promise.all([
loadDynamicOptions(staticOptions),
loadLocalDynamicOptions(),
loginOptions()
]);
const previewOptions = loadPreviewOptions();
window.C8Y_OPTIONS_TO_MERGE = {
urlOptions,
staticOptions,
dynamicOptions,
localDynamicOptions,
loadedLoginOptions,
previewOptions
};
const options = mergeOptions(getOptionsToMerge());
return options;
}
export function getOptionsToMerge() {
if (window.C8Y_OPTIONS_TO_MERGE === null) {
return null;
}
return cloneDeep(window.C8Y_OPTIONS_TO_MERGE);
}
function getShadeColorBrandingCssVars(brandingCssVars) {
const shades = generateShades(brandingCssVars['brand-primary']);
let i = 1;
for (const shade of shades) {
brandingCssVars['c8y-brand-' + i + '0'] = shade;
i++;
}
return brandingCssVars;
}
function generateShades(inputColor) {
const referenceShades = [
'#134158',
'#1C5569',
'#058192', // primary color
'#22A6AA',
'#3CC1B7',
'#8ADBD5',
'#C5EDEA',
'#EBF9F8'
];
// Calculate the luminance of the reference shades
const referenceLuminances = referenceShades.map(color => chroma(color).luminance());
// Generate shades of the input color with the same luminance as the reference shades
const generatedShades = referenceLuminances.map(luminance => chroma(inputColor).luminance(luminance).hex());
// Calculate the distance between the input color and each color in the generatedShades array
const distances = generatedShades.map(color => chroma.deltaE(inputColor, color));
// Find the index of the color with the smallest distance
const index = distances.indexOf(Math.min(...distances));
generatedShades[index] = inputColor;
return generatedShades;
}
function hasBrandPrimary(brandingCssVars) {
return !!brandingCssVars?.['brand-primary'];
}
function hasAnyBrandShade(brandingCssVars) {
if (!brandingCssVars) {
return false;
}
return !!Object.keys(brandingCssVars).some(value => /brand-[1-8]0/.test(value));
}
export function getAllMFRemotes(options) {
return reduce(options, (allRemotes, mfRemote) => {
const { remotes } = mfRemote;
forEach(remotes, (remoteModules, remoteContextPath) => {
const currentRemotes = get(allRemotes, remoteContextPath, []);
allRemotes[remoteContextPath] = union(currentRemotes, remoteModules);
});
return allRemotes;
}, {});
}
function loadStaticOptions() {
if (!staticOptionsCache) {
staticOptionsCache =
JSON.parse(document.querySelector('#static-options').innerText) || {};
}
return { ...staticOptionsCache, ...loadUrlOptions() };
}
async function loginOptions() {
const hostName = location.origin;
return await requestRemoteOptions(hostName + '/tenant/loginOptions');
}
export function loadUrlOptions() {
if (!urlOptionsCache) {
const query = location.search.substring(1).split('&');
urlOptionsCache = query.reduce((options, keyValuePair) => {
if (!keyValuePair) {
return options;
}
if (keyValuePair.match(/=/)) {
const [key, value] = keyValuePair.split(/=/);
try {
options[key] = JSON.parse(decodeURIComponent(value));
}
catch (error) {
console.warn(`Failed to parse option ${key}: ${error}`);
options[key] = value;
}
}
else {
options[keyValuePair] = true;
}
return options;
}, {});
}
return urlOptionsCache;
}
export function clearUrlOptionsCache() {
urlOptionsCache = undefined;
}
async function loadLocalDynamicOptions() {
const remoteOptions = await requestRemoteOptions(`cumulocity.json?nocache=${new Date().getTime()}`);
return remoteOptions;
}
function getContextPathFromLocation() {
try {
const path = window.location.pathname;
const [, , contextPath] = path.match(/^\/apps\/(public\/)?([^\/]+)\//);
if (contextPath) {
return contextPath;
}
}
catch (e) {
console.warn('Failed to get context path from location');
}
return null;
}
async function loadDynamicOptions(staticOptions) {
const contextPathFromLocation = getContextPathFromLocation();
const { dynamicOptionsUrl, contextPath } = staticOptions;
let remoteOptions = {};
if (dynamicOptionsUrl === false) {
return remoteOptions;
}
let actualDynamicOptionsUrl;
if (!dynamicOptionsUrl || dynamicOptionsUrl === true) {
actualDynamicOptionsUrl = `/apps/public/public-options@app-${contextPathFromLocation || contextPath}/options.json`;
}
else {
actualDynamicOptionsUrl = dynamicOptionsUrl;
}
actualDynamicOptionsUrl = actualDynamicOptionsUrl.match(/\?/)
? actualDynamicOptionsUrl
: `${actualDynamicOptionsUrl}?nocache=${new Date().getTime()}`;
remoteOptions = await requestRemoteOptions(actualDynamicOptionsUrl);
return remoteOptions;
}
function loadPreviewOptions() {
if (window.C8Y_PREVIEW) {
return window.C8Y_PREVIEW;
}
return {};
}
async function requestRemoteOptions(url) {
let options = {};
try {
const response = await fetch(url);
if (response.ok) {
try {
options = await response.json();
}
catch (e) {
// do nothing in case of failing JSON parsing
// fallback to empty object
// keeping compatibility with previous implementation
}
}
}
catch (e) {
console.warn(`Failed to load remote options from ${url}:`, e);
}
Object.entries(options).forEach(([key, value]) => {
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object') {
options[key] = parsed;
}
}
catch (e) {
// do nothing
}
}
});
return options;
}
/**
* Update the window object with the given options and modules.
* @param windowObj - The global window object.
* @param c8yAppVarName - The variable name used for the C8Y_APP object.
* @param options - An object containing configuration options for the C8Y_APP object.
* @param modulesToCopy - An array of angularjs modules to be added to the C8Y_APP object.
* @returns - The updated window object.
*/
export function updateWindowObject(windowObj, options) {
const c8yAppVarName = options['c8yAppVarName'] || 'C8Y_APP';
const finalOptions = { modules: [], ...options };
windowObj.C8Y_APP = windowObj[c8yAppVarName] = finalOptions;
return finalOptions;
}
/**
* Apply the given options to the C8Y_APP object in the window and update the document elements.
* @param options - An object containing configuration options for the C8Y_APP object.
* @returns - The updated options object.
*/
export function applyOptions(options) {
options = updateWindowObject(window, options);
applyBrandingOptions(options);
setVersion(options);
updateBrandingUrl(options, document);
updateTranslations(options);
applyPreviewOptions(options);
setImportMap(options);
return options;
}
export function setImportMap(options) {
if (!options.importMap) {
return;
}
const script = document.createElement('script');
script.setAttribute('type', 'importmap');
// getting the base path like /apps/cockpit/ or /apps/cockpit-clone/
const basePath = window.location.pathname.split('/').slice(0, -1).join('/') + '/';
const imports = Object.entries(options.importMap).reduce((acc, [key, value]) => {
acc[key] = value.startsWith('http') ? value : basePath + value;
return acc;
}, {});
script.innerHTML = JSON.stringify({
imports
});
// set the import map
document.querySelector('head').appendChild(script);
}
export function applyBrandingOptions(options) {
applyOptionsToTheming(options);
updateTitle(options, document);
updateFavicon(options, document);
updateCss(options, document);
}
export function setVersion(options) {
const { c8yVersionName = 'UI_VERSION' } = options;
window[c8yVersionName] = options.versions.ng1 || options.versions.ngx;
}
export function updateTitle({ globalTitle }, document) {
if (!globalTitle) {
return;
}
const titleEl = document.querySelector('title');
const currentTitle = titleEl.innerText;
const withoutCurrentlyAppliedTitle = currentTitle.replace(/^.*\s-\s/, '');
titleEl.innerText = `${globalTitle} - ${withoutCurrentlyAppliedTitle}`;
}
export function updateFavicon({ faviconUrl = 'favicon.ico' }, document) {
const brandingFaviconIdentifier = 'branding-favicon';
const existingLink = document.getElementById(brandingFaviconIdentifier);
const link = existingLink || document.createElement('link');
link.setAttribute('rel', 'icon');
link.setAttribute('href', faviconUrl);
link.setAttribute('id', brandingFaviconIdentifier);
if (!existingLink) {
document.querySelector('head').appendChild(link);
}
}
export function updateBrandingUrl({ brandingUrl }, document) {
if (brandingUrl) {
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', brandingUrl);
document.querySelector('head').appendChild(link);
}
}
export function updateCss({ brandingCssVars, extraCssUrls, extraCss }, document) {
const extraCssUrlsClassIdentifier = 'branding-extra-css-urls';
const existingElements = Array.from(document.getElementsByClassName(extraCssUrlsClassIdentifier));
let urlNeededToBeAdded = [...(extraCssUrls || [])];
if (brandingCssVars && brandingCssVars['font-url']) {
urlNeededToBeAdded.push(brandingCssVars['font-url']);
}
for (const element of existingElements) {
if (!urlNeededToBeAdded.includes(element.href)) {
element.remove();
}
else {
urlNeededToBeAdded = urlNeededToBeAdded.filter(url => url !== element.href);
}
}
for (const url of urlNeededToBeAdded) {
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', url);
link.setAttribute('class', extraCssUrlsClassIdentifier);
document.querySelector('head').appendChild(link);
}
const extraCSSClassIdentifier = 'branding-extra-css';
const existingExtraCSSElements = Array.from(document.getElementsByClassName(extraCSSClassIdentifier));
for (const element of existingExtraCSSElements) {
element.remove();
}
if (extraCss && typeof extraCss === 'string') {
const styleTag = document.createElement('style');
styleTag.setAttribute('class', extraCSSClassIdentifier);
styleTag.setAttribute('type', 'text/css');
styleTag.appendChild(document.createTextNode(extraCss));
document.querySelector('head').appendChild(styleTag);
}
applyBrandingVars(brandingCssVars);
}
export function applyBrandingVars(brandingCssVars) {
if (!brandingCssVars) {
brandingCssVars = {};
}
if (!brandingCssVars['display-main-logo']) {
brandingCssVars['display-main-logo'] = 'block';
}
const darkThemePrefix = 'dark-';
const darkThemeSelector = '.c8y-dark-theme';
const lightThemeSelector = ':root, .c8y-light-theme';
const systemThemeMediaQuery = '@media (prefers-color-scheme: dark)';
const systemThemeSelector = '.c8y-system-theme';
const brandingVarsIdentifier = 'branding-vars';
document.getElementById(brandingVarsIdentifier)?.remove();
if (brandingCssVars) {
const darkVars = new Array();
const lightVars = new Array();
for (const key of Object.keys(brandingCssVars)) {
let varsToAddTo = lightVars;
let keyToSet = key;
if (key.startsWith(darkThemePrefix)) {
varsToAddTo = darkVars;
keyToSet = keyToSet.replace(darkThemePrefix, '');
}
varsToAddTo.push(`--${keyToSet}: ${brandingCssVars[key]};`);
}
const lightThemeCss = buildBrandingCss(lightThemeSelector, lightVars);
const darkThemeCss = buildBrandingCss(darkThemeSelector, darkVars);
const systemDarkThemeCss = buildCssRule(systemThemeMediaQuery, buildBrandingCss(systemThemeSelector, darkVars));
const style = document.createElement('style');
style.setAttribute('id', brandingVarsIdentifier);
style.appendChild(document.createTextNode(`${lightThemeCss}\n\n${darkThemeCss}\n\n${systemDarkThemeCss}`));
document.querySelector('body').appendChild(style);
}
}
function buildBrandingCss(selector, vars) {
const joinedVars = buildCssVars(vars);
return buildCssRule(selector, joinedVars);
}
function buildCssVars(vars) {
return vars.join('\n');
}
function buildCssRule(selector, innerCss) {
return `${selector} {\n${innerCss}\n}`;
}
export function updateTranslations(options) {
if (options?.i18nExtra) {
options.langsDetails = { ...options.langsDetails, ...options.i18nExtra };
}
}
//# sourceMappingURL=options-resolver.js.map