UNPKG

@c8y/bootstrap

Version:

Bootstrap layer

411 lines 15.8 kB
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