@eightshift/frontend-libs
Version:
A collection of useful frontend utility modules. powered by Eightshift
1,029 lines (872 loc) • 29.4 kB
JavaScript
import React from 'react';
import { subscribe, select, dispatch } from '@wordpress/data';
import { getAttrKey } from './attributes';
import { STORE_NAME } from './store';
import { camelCase, debounce, isEmpty, isObject, isPlainObject, kebabCase } from '@eightshift/ui-components/utilities';
/**
* Get Global manifest.json and return global variables as CSS variables.
*
* @param {object} globalManifest - (Optional) Global variable data. - Deprecated.
*
* @access public
*
* @return {string|void}
*
* Global Manifest:
* ```js
* const manifestGlobal = {
* "globalVariables": {
* "maxCols": 12,
* "breakpoints": {
* "mobile": 479,
* "tablet": 1279,
* "desktop": 1919,
* "large": 1920
* },
* "containers": {
* "default": "1330px"
* },
* "gutters": {
* "none": "0",
* "default": "25px",
* "big": "50px"
* },
* "sectionSpacing": {
* "min": -300,
* "max": 300,
* "step": 10
* },
* "sectionInSpacing": {
* "min": 0,
* "max": 300,
* "step": 10
* },
* "colors": [
* {
* "name": "Infinum",
* "slug": "infinum",
* "color": "#D8262C"
* },
* {
* "name": "Black",
* "slug": "black",
* "color": "#111111"
* }
* ]
* }
* };
* ```
*
* Usage:
* ```js
* import globalSettings from './../../manifest.json';
*
* outputCssVariablesGlobal(globalSettings);
* ```
*
* Output:
* ```js
* <style>
* :root {
* --global-max-cols: 12;
* --global-breakpoints-mobile: 479;
* --global-breakpoints-tablet: 1279;
* --global-breakpoints-desktop: 1919;
* --global-breakpoints-large: 1920;
* --global-containers-default: 1330px;
* --global-gutters-none: 0;
* --global-gutters-default: 25px;
* --global-gutters-big: 50px;
* --global-section-spacing-min: -300;
* --global-section-spacing-max: 300;
* --global-section-spacing-step: 10;
* --global-section-in-spacing-min: 0;
* --global-section-in-spacing-max: 300;
* --global-section-in-spacing-step: 10;
* --global-colors-infinum: #D8262C;
* --global-colors-black: #111111;
* --global-colors-white: #FFFFFF;
* }
* </style>
* ```
*/
export const outputCssVariablesGlobal = (globalManifest = {}) => {
let output = '';
for (const [itemKey, itemValue] of Object.entries(select(STORE_NAME).getSettingsGlobalVariables())) {
const itemKeyInner = kebabCase(itemKey);
if (isObject(itemValue)) {
output += globalInner(itemValue, itemKeyInner);
} else {
output += `--global-${itemKeyInner}: ${itemValue};\n`;
}
}
// Optimize if necessary.
if (select(STORE_NAME).getConfigOutputCssOptimize()) {
output.replace('\n', '');
}
// Prepare output.
const finalOutput = `<style id="${select(STORE_NAME).getConfigOutputCssSelectorName()}-global">:root {${output}}</style>`;
// Set breakpoints cache for optimized load time.
setBreakpointsCacheData();
// If using inline css variables output them.
if (select(STORE_NAME).getConfigOutputCssGlobally()) {
outputCssVariablesInline();
}
// Output style tag.
return document.head.insertAdjacentHTML('afterbegin', finalOutput);
};
/**
* Get component/block options and process them in CSS variables.
*
* @param {array} attributes - Built attributes.
* @param {array} manifest - Component/block manifest data.
* @param {string} unique - Unique key.
* @param {object} globalManifest - (Optional) Global variable data.
* @param {string} customSelector - Output custom selector to use as a style prefix.
*
* @access public
*
* @return {string}
*
* Usage:
* ```js
* import React, { useMemo } from 'react';
*
* const unique = useMemo(() => getUnique(), []);
*
* outputCssVariables(attributes, manifest, unique);
* ```
*/
export const outputCssVariables = (attributes, manifest, unique, globalManifest = {}, customSelector = '') => {
const breakpoints = select(STORE_NAME).getSettingsGlobalVariablesBreakpoints();
// Define variables from manifest.
const variables = manifest?.variables;
const variablesEditor = manifest?.variablesEditor;
const responsiveAttributes = manifest?.responsiveAttributes;
// Sort breakpoints in correct order.
const sortedBreakpoints = Object.entries(breakpoints)
.sort((a, b) => {
return a[1] - b[1]; // Sort from the smallest to the largest breakpoint.
});
const defaultBreakpoints = {
min: sortedBreakpoints?.[0]?.[0] || '',
max: sortedBreakpoints?.[sortedBreakpoints.length - 1]?.[0] || '',
};
// Get the initial data array.
const data = prepareVariableData(sortedBreakpoints);
if (typeof variables !== 'undefined') {
// Iterate each responsiveAttribute from responsiveAttributes that appears in variables field.
if (typeof responsiveAttributes !== 'undefined') {
setVariablesToBreakpoints(attributes, setupResponsiveVariables(responsiveAttributes, variables), data, manifest, defaultBreakpoints);
}
// Iterate each variable from variables field.
setVariablesToBreakpoints(attributes, variables, data, manifest, defaultBreakpoints);
}
// Iterate each responsiveAttribute from responsiveAttributes that appears in variablesEditor field.
if (typeof variablesEditor !== 'undefined') {
if (typeof responsiveAttributes !== 'undefined') {
setVariablesToBreakpoints(attributes, setupResponsiveVariables(responsiveAttributes, variablesEditor), data, manifest, defaultBreakpoints);
}
// Iterate each variable from variablesEditor field.
setVariablesToBreakpoints(attributes, variablesEditor, data, manifest, defaultBreakpoints);
}
// Check if component or block.
let name = manifest.componentClass ?? attributes.blockClass;
// Switch selector name.
if (customSelector !== '') {
name = customSelector;
}
// If default output just echo.
if (!select(STORE_NAME).getConfigOutputCssGlobally()) {
return getCssVariablesTypeDefault(name, data, manifest, unique);
}
// Find if style exists in the store.
const existsIndex = select(STORE_NAME).getStyles().findIndex((item) => item?.name === name && item?.unique === unique);
// Find blockClientId from the attributes.
const blockClientId = attributes?.blockClientId;
// Don't do anything if blockClientId is missing.
if (typeof blockClientId !== 'undefined') {
if (existsIndex !== -1) {
// Update existing styles.
dispatch(STORE_NAME).setStyleByIndex(getCssVariablesTypeInline(name, data, manifest, unique, blockClientId), existsIndex);
} else {
// Add new styles.
dispatch(STORE_NAME).setStyle(getCssVariablesTypeInline(name, data, manifest, unique, blockClientId));
}
}
return null;
};
/**
* Convert hex color into RGB values.
*
* @param {string} input - Input hex color (either 3 or 6 characters).
*
* @access public
*
* @return {string}
*/
export const hexToRgb = (input) => {
let r = 0, g = 0, b = 0;
const hex = input.replace('#', '').trim();
if (hex.length === 3) {
const [r1, g1, b1] = hex;
r = `0x${r1}${r1}`;
g = `0x${g1}${g1}`;
b = `0x${b1}${b1}`;
} else if (hex.length === 6) {
const [r1, r2, g1, g2, b1, b2] = hex;
r = `0x${r1}${r2}`;
g = `0x${g1}${g2}`;
b = `0x${b1}${b2}`;
}
r = Number(r);
g = Number(g);
b = Number(b);
if (isNaN(r) || isNaN(g) || isNaN(b)) {
return '0 0 0';
}
return `${r} ${g} ${b}`;
};
/**
* Returns a unique ID, generally used with CSS variable generation.
*
* @access public
*
* @return {string}
*
* Usage:
* ```js
* getUnique();
* ```
*
* Output:
* ```js
* mg2shbh9
* ```
*/
export const getUnique = () => {
return (Math.random() + 1).toString(36).substring(4);
};
// ------------------------------------------------------------------------------
// PRIVATE METHODS
// ------------------------------------------------------------------------------
// Internal variable for storing caches breakpoint data.
let breakpointsPreparedVariableDataCache = [];
/**
* Output CSS variables as one inline style tag.
*
* @access private
*
* @returns {void}
*/
export const outputCssVariablesInline = () => {
// Subscribe to changes in state.
subscribe(
// Add some debounce for optimizations.
debounce(() => {
// Check if style has changed.
const hasStylesUpdated = select(STORE_NAME).hasStylesUpdated();
if (hasStylesUpdated) {
// Output inline style tag from store data.
outputCssVariablesCombinedInner(select(STORE_NAME).getStyles());
}
}, 50)
);
// Find current tree with all inner blocks and all reusable blocks.
let currentStateBlocks = select('core/block-editor').__unstableGetClientIdsTree(); // eslint-disable-line no-underscore-dangle
// Subscribe to changes in state.
subscribe(() => {
// Find updated state of blocks after render.
const newStateBlocks = select('core/block-editor').__unstableGetClientIdsTree(); // eslint-disable-line no-underscore-dangle
// Make changes only if blocks changed.
if (newStateBlocks !== currentStateBlocks) {
// Make all current and next blocks flat for easy checking.
const next = getAllBlocksFlat(newStateBlocks);
const previous = getAllBlocksFlat(currentStateBlocks);
// Find deleted blocks.
const deleted = getDifference(previous, next);
// If blocks are deleted remove them from the style store.
if (deleted) {
// Loop all styles.
select(STORE_NAME).getStyles().forEach((item, index) => {
// Find index of deleted item by clientId and remove it from the store.
deleted.forEach((element) => {
if (element.clientId === item.blockClientId) {
dispatch(STORE_NAME).unsetStyleByIndex(index);
}
});
});
}
}
// Update current state with the new one.
currentStateBlocks = newStateBlocks;
});
};
/**
* Find difference in two arrays with multiple nesting levels.
*
* @param {array} array1 Array to look.
* @param {array} array2 Array to check.
*
* @access private
*
* @returns {array}
*/
export const getDifference = (array1, array2) => {
return array1.filter(object1 => {
return !array2.some(object2 => {
return object1.clientId === object2.clientId;
});
});
};
/**
* Return CSS variables in default type. On the place where it was called.
*
* @param {string} name - Output css selector name.
* @param {array} data - Data prepared for checking.
* @param {array} manifest - Component/block manifest data.
* @param {string} unique - Unique key.
*
* @access private
*
* @returns {string}
*/
export const getCssVariablesTypeDefault = (name, data, manifest, unique) => {
let output = '';
let uniqueSelector = `[data-id='${unique}']`;
if (!unique) {
uniqueSelector = '';
}
// Loop data and provide correct selectors from data array.
for(const {type, value, variable} of data) {
// If breakpoint value is 0 then don't wrap the media query around it.
if (variable.length === 0) {
continue;
}
const variableOutput = variable?.map((v) => v?.trim()?.endsWith(';') ? v : `${v};`).join('\n');
if (value === 0) {
// No breakpoint outputted.
output += `\n .${name}${uniqueSelector}{\n${variableOutput}\n}`;
} else {
// With breakpoint.
output += `\n @media (${type}-width: ${value}px) {\n.${name}${uniqueSelector}{\n${variableOutput}\n}\n}`;
}
}
// Output manual output from the array of variables.
let manual = '';
const variablesCustom = manifest?.variablesCustom?.map((v) => v?.trim()?.endsWith(';') ? v : `${v};`);
if (typeof variablesCustom !== 'undefined') {
manual = variablesCustom.join(';\n');
}
// Output manual editor output from the array of variables.
let manualEditor = '';
const variablesCustomEditor = manifest?.variablesCustomEditor?.map((v) => v?.trim()?.endsWith(';') ? v : `${v};`);
if (typeof variablesCustomEditor !== 'undefined') {
manualEditor = variablesCustomEditor.join(';\n');
}
// Process CSS variables.
output = processCssVarsRemBaseSize(output);
manual = processCssVarsRemBaseSize(manual);
manualEditor = processCssVarsRemBaseSize(manualEditor);
// Prepare final output for testing.
const fullOutput = `
${output}
${manual}
${manualEditor}
`;
// Check if final output is empty and return if empty string if it is.
if (isEmpty(fullOutput.trim())) {
return '';
}
// Prepare output for manual variables.
const finalManualOutput = manual || manualEditor ? `.${name}${uniqueSelector}{ ${manual} ${manualEditor}}` : '';
// Implement some optimizations if necessary.
if (select(STORE_NAME).getConfigOutputCssOptimize()) {
output.replace('\n', '');
finalManualOutput.replace('\n', '');
}
// Output the style for CSS variables.
return <style dangerouslySetInnerHTML={{__html: `${output} ${finalManualOutput}`}} />;
};
/**
* Get css variables in inline type. In one place in dom.
*
* @param {string} name - Output css selector name.
* @param {array} data - Data prepared for checking.
* @param {array} manifest - Component/block manifest data.
* @param {string} unique - Unique key.
* @param {string} blockClientId - blockClientId key from attributes, corresponds to the original clientId from core.
*
* @access private
*
* @returns {array}
*/
export const getCssVariablesTypeInline = (name, data, manifest, unique, blockClientId) => {
// Prepare output style object.
const styles = {
name,
unique,
blockClientId,
variables: [],
};
// Loop data and provide correct selectors from data array.
for (const {type, value, variable} of data) {
// If breakpoint value is 0 then don't wrap the media query around it.
if (variable.length === 0) {
continue;
}
// Push data to local state.
styles.variables.push({
type,
variable,
value,
});
}
// Output manual output from the array of variables.
const variablesCustom = manifest?.variablesCustom;
if (typeof variablesCustom !== 'undefined') {
styles.variables.push({
type: 'min',
variable: variablesCustom,
value: 0
});
}
// Output manual editor output from the array of variables.
const variablesCustomEditor = manifest?.variablesCustomEditor;
if (typeof variablesCustomEditor !== 'undefined') {
styles.variables.push({
type: 'min',
variable: variablesCustomEditor,
value: 0
});
}
return styles;
};
/**
* Process and return global CSS variables based on the type.
*
* @param {array} itemValues - Values to check.
* @param {string} itemKey - Item key to check.
*
* @access private
*
* @return {string}
*/
export const globalInner = (itemValues, itemKey) => {
let output = '';
for (const [key, value] of Object.entries(itemValues)) {
const innerKey = kebabCase(key);
const itemInnerKey = kebabCase(itemKey);
const {
slug,
color,
gradient,
} = value;
switch (itemInnerKey) {
case 'colors':
if (typeof slug === 'undefined' || typeof color === 'undefined') {
break;
}
output += `--global-${itemInnerKey}-${value.slug}: ${value.color};\n`;
output += `--global-${itemInnerKey}-${value.slug}-values: ${hexToRgb(value.color)};\n`;
break;
case 'gradients':
if ( typeof slug === 'undefined' || typeof gradient === 'undefined') {
break;
}
output += `--global-${itemInnerKey}-${value.slug}: ${value.gradient};\n`;
break;
case 'font-sizes':
if ( typeof slug === 'undefined') {
break;
}
output += `--global-${itemInnerKey}-${value.slug}: ${value.slug};\n`;
break;
default:
output += `--global-${itemInnerKey}-${innerKey}: ${value};\n`;
break;
}
}
return output;
};
/**
* Sets up a breakpoint value to responsive attribute objects from responsiveAttribute object.
*
* @param {array} attributeVariables - Array of attribute variables object.
* @param {string} breakpointName - Breakpoint name from responsiveAttribute's breakpoint in block's/component's manifest.
* @param {number} breakpointIndex - Index of responsiveAttribute's breakpoint in manifest.
* @param {number} numberOfBreakpoints - Number of responsiveAttribute breakpoints in block's/component's manifest.
*
* @return {array}
*/
export const setBreakpointResponsiveVariables = (attributeVariables, breakpointName, breakpointIndex, numberOfBreakpoints) => {
return attributeVariables.map((attributeVariablesObject) => {
// Calculate default breakpoint index based on order of the breakpoint, inverse property and number of properties in responsiveAttributeObject.
const defaultBreakpointIndex = attributeVariablesObject.inverse ? 0 : (numberOfBreakpoints - 1);
// Expanding an object with an additional breakpoint property.
return {
...attributeVariablesObject,
breakpoint: breakpointIndex === defaultBreakpointIndex ? 'default' : breakpointName,
};
});
};
/**
* Iterating through variables matching the keys from responsiveAttributes and translating it to responsive attributes names.
*
* @param {object} responsiveAttributes - Responsive attributes that are read from component's/block's manifest.
* @param {object} variables - Object containing objects with component's/block's attribute variables that are read from manifest.
*
* @return {object} Object prepared for setting all the variables to its breakpoints.
*/
export const setupResponsiveVariables = (responsiveAttributes, variables) => {
// Iterate through responsive attributes.
return Object.entries(responsiveAttributes)
.reduce((responsiveAttributesVariables, [responsiveAttributeName, responsiveAttributeObject]) => {
// If responsive attribute doesn't exist in variables object, skip it.
if (!responsiveAttributeName || isEmpty(variables[responsiveAttributeName])) {
return responsiveAttributesVariables;
}
// Used for determination of default breakpoint.
const numberOfBreakpoints = Object.entries(responsiveAttributeObject).length;
// Iterate each responsive attribute object as breakpoint name is the key of the object,
// and value represents the name of the responsive variable.
const responsiveAttributeVariables = Object.entries(responsiveAttributeObject)
.reduce((responsiveAttribute, [breakpointName, breakpointVariableName], breakpointIndex) => {
let breakpointVariables = {};
if (Array.isArray(variables[responsiveAttributeName])) { // Array represents direct value(default or value).
breakpointVariables = setBreakpointResponsiveVariables(
variables[responsiveAttributeName],
breakpointName,
breakpointIndex,
numberOfBreakpoints
);
return {
...responsiveAttribute,
[breakpointVariableName]: breakpointVariables,
};
}
// Object treatment goes depending on a value inserted(multiple choice, boolean or similar).
// Iterate options/multiple choices/boolean...
breakpointVariables = Object.entries(variables[responsiveAttributeName])
.reduce((acc, [attributeValue, attributeObject]) => {
return {
...acc,
[attributeValue]: setBreakpointResponsiveVariables(attributeObject, breakpointName, breakpointIndex, numberOfBreakpoints),
};
}, {});
// Collect all the values from one responsive attribute to one object.
return {
...responsiveAttribute,
[breakpointVariableName]: breakpointVariables,
};
}, {});
// Merge multiple responsive attributes to one object.
return {...responsiveAttributesVariables, ...responsiveAttributeVariables};
}, {});
};
/**
* Setting defined variables to each breakpoint.
*
* @param {object} attributes - Attributes fetched from manifest.
* @param {object} variables - Variables fetched from manifest.
* @param {object} data - Preset objects separated in breakpoints.
* @param {array} manifest - Component/block manifest data.
* @param {object} defaultBreakpoints - Default breakpoints for mobile/desktop first.
*
* @access private
*
* @return {object} Filled object with variables data separated in breakpoints.
*/
export const setVariablesToBreakpoints = (attributes, variables, data, manifest, defaultBreakpoints) => {
// Iterate each variable.
for (const [variableName, variableValue] of Object.entries(variables)) {
// Constant for attributes set value (in db or default).
const attributeValue = attributes[getAttrKey(variableName, attributes, manifest)];
// Set internal breakpoints variable.
const internalBreakpoints = Array.isArray(variableValue) ? variableValue : (variableValue[attributeValue] ?? []);
// Iterate variable array to check breakpoints.
internalBreakpoints.forEach((breakpointItem) => {
// Define variables from breakpointItem.
const {
breakpoint: itemBreakpoint, // Put in temporary variable before checking the type of breakpointItem.
inverse = false, // If inverse is not set use mobile first.
variable = [],
} = breakpointItem;
// Check if we are using mobile or desktop first. Mobile first is the default.
const type = inverse ? 'max' : 'min';
// If breakpoint is not set or has default breakpoint value use default name.
const breakpoint = (!itemBreakpoint || itemBreakpoint === defaultBreakpoints[type]) ? 'default' : itemBreakpoint;
// Iterate each data array to find the correct breakpoint.
data.some((item, index) => {
// Check if breakpoint and type match.
if (item.name === breakpoint && item.type === type) {
// Merge data variables with the new variables array.
data[index].variable = item.variable.concat(variablesInner(variable, attributeValue, attributes, manifest));
// Exit.
return true;
}
return false;
});
});
}
return data;
};
/**
* Create initial array of data to be able to populate later.
*
* @param {object} globalBreakpoints - Global breakpoints from global manifest to set the correct output.
*
* @access private
*
* @return {array}
*/
export const prepareVariableData = (globalBreakpoints) => {
// Define the min and max arrays.
const min = [];
const max = [];
let minBreakpointValue = 0;
// Loop the global breakpoints and populate the data.
Object.values(globalBreakpoints).forEach(([item, value]) => {
// Initial inner object.
const itemObject = {
name: item,
value: value,
variable: [],
};
// Inner object for min values.
const itemObjectMin = {
...itemObject,
type: 'min',
value: minBreakpointValue,
};
// Inner object for max values.
const itemObjectMax = {
...itemObject,
type: 'max',
};
// Transfer value to a larger breakpoint.
minBreakpointValue = value;
// Push both min and max to the defined arrays.
min.push(itemObjectMin);
max.push(itemObjectMax);
});
// Pop largest breakpoint out of min array.
min.shift();
// Add default object to the top of the array.
min.unshift({
type: 'min',
name: 'default',
value: 0,
variable: [],
});
// Reverse order of max array.
max.reverse();
// Throwout the largest.
max.shift();
// Add default object to the top of the array.
max.unshift({
type: 'max',
name: 'default',
value: 0,
variable: [],
});
// Merge both arrays.
return min.concat(max);
};
/**
* Internal helper to loop CSS Variables from array.
*
* @param {array} variables - Array of variables of CSS variables.
* @param {mixed} attributeValue - Original attribute value used in magic variable.
* @param {object} attributes - Attributes fetched from manifest.
* @param {array} manifest - Component/block manifest data.
*
* @access private
*
* @returns {array}
*/
export const variablesInner = (variables, attributeValue, attributes, manifest) => {
let output = [];
// Bailout if provided variables is not an object or if attribute value is empty or undefined, used to unset/reset value..
if (typeof attributeValue === 'undefined' || !isPlainObject(variables)) {
return output;
}
// Iterate each attribute and make corrections.
for (const [variableKey, variableValue] of Object.entries(variables)) {
let value = variableValue;
// If value contains magic variable swap that variable with original attribute value.
if (variableValue.includes('%value%')) {
value = variableValue.replace('%value%', attributeValue);
}
for (const [attrKey, attrValue] of Object.entries(attributes)) {
let key = attrKey;
if (attributes?.prefix) {
key = key.replace(attributes.prefix, camelCase(manifest.componentName));
}
if (variableValue.includes(`%attr-${key}%`)) {
value = variableValue.replace(`%attr-${key}%`, attrValue);
}
}
// Bailout if value is empty or undefined.
if (value === 'undefined' || isEmpty(value)) {
continue;
}
// Output the custom CSS variable by adding the attribute key + custom object key.
output.push(`--${kebabCase(variableKey)}: ${value};`);
}
return output;
};
/**
* Set breakpoints cache for optimized load time.
*
* @access private
*
* @returns {void}
*/
export const setBreakpointsCacheData = () => {
// Prepare breakpoints.
const breakpoints = select(STORE_NAME).getSettingsGlobalVariablesBreakpoints();
const breakpointsCache = Object.entries(breakpoints).sort((a, b) => {
return a[1] - b[1]; // Sort from the smallest to the largest breakpoint.
});
// Prepare breakpoints data to output combined css variables.
const breakpointsMin = breakpointsCache.map((item) => {
return {
type: 'min',
value: item[1],
};
});
breakpointsMin.unshift({
type: 'min',
value: 0,
});
const breakpointsMax = breakpointsCache.reverse().map((item) => {
return {
type: 'max',
value: item[1],
};
});
breakpointsMax.unshift({
type: 'max',
value: 0,
});
breakpointsPreparedVariableDataCache = breakpointsMin.concat(breakpointsMax);
};
/**
* Get all blacks inner-blocks recursively in one flat array.
*
* @param {array} blocks - Array of blocks.
*
* @access private
*
* @returns {array}
*/
export const getAllBlocksFlat = (blocks) => {
let output = [];
// Loop all blocks.
blocks.forEach((block) => {
const {
innerBlocks,
} = block;
// Internal output.
let innerOutput = [];
// Add current block to the state.
output.push(block);
// If inner blocks are listed do recursive add.
if (innerBlocks.length > 0) {
innerOutput = getAllBlocksFlat(innerBlocks);
}
// Output all.
output = output.concat(innerOutput);
});
return output;
};
/**
* Output css variables as a one inline style tag - inner.
*
* @param {string} styles - CSS variables to process.
*
* @access private
*
* @returns {string}
*/
const processCssVarsRemBaseSize = (styles) => {
const remRegex = /([0-9.-]+rem)/g;
const remReplacement = 'calc($1 * var(--base-font-size, 1))';
return select(STORE_NAME).getConfigUseRemBaseSize() ? styles.replaceAll(remRegex, remReplacement) : styles;
};
/**
* Output css variables as a one inline style tag - inner.
*
* @access private
*
* @returns {string}
*/
export const outputCssVariablesCombinedInner = (styles) => {
const breakpoints = [];
// Loop styles.
for (const {name, unique, variables} of styles) {
// Bailout if variables are missing.
if (variables.length === 0) {
continue;
}
let uniqueSelector = `[data-id='${unique}']`;
if (!unique) {
uniqueSelector = '';
}
// Loop inner variables.
for (const {type, value, variable} of variables) {
// Bailout if variable is missing.
if (variable.length === 0) {
continue;
}
// Set breakpoint to empty if it is missing initially.
if (typeof breakpoints[`${type}---${value}`] === 'undefined') {
breakpoints[`${type}---${value}`] = '';
}
const variableOutput = variable?.map((v) => v?.trim()?.endsWith(';') ? v : `${v};`).join('\n');
// Populate breakpoints output.
breakpoints[`${type}---${value}`] += `\n.${name}${uniqueSelector}{\n${variableOutput}\n} `;
}
}
// Prepare final output.
let output = '';
// Loop all breakpoints prepared for output.
breakpointsPreparedVariableDataCache.forEach(({type, value}) => {
const breakpointValue = breakpoints[`${type}---${value}`] ?? '';
// Bailout if breakpoint is missing.
if (breakpointValue === '') {
return;
}
// Wrap media queries with correct selectors.
if (value === 0) {
output += `${breakpointValue}\n`;
} else {
output += `\n@media (${type}-width:${value}px){${breakpointValue}}\n `;
}
});
// Do optimizations if necessary.
if (select(STORE_NAME).getConfigOutputCssOptimize()) {
output = output.replace(/\n|\r/g, '');
}
// Add additional style from config settings.
const additionalStyles = select(STORE_NAME).getConfigOutputCssGloballyAdditionalStyles();
let additionalStylesOutput = '';
if (typeof additionalStyles !== 'undefined') {
additionalStylesOutput = additionalStyles.join(';\n');
}
// Get style id name from store.
const selector = select(STORE_NAME).getConfigOutputCssSelectorName();
// Detect if style tag is present in dom.
const styleTag = document.getElementById(selector);
// Process styles.
output = processCssVarsRemBaseSize(output);
additionalStylesOutput = processCssVarsRemBaseSize(additionalStylesOutput);
if (!styleTag) {
document.body.insertAdjacentHTML('beforeend', `<style id="${selector}">${output} ${additionalStylesOutput}</style>`);
} else {
styleTag.innerHTML = `${output} ${additionalStylesOutput}`;
}
// Reset state to original.
dispatch(STORE_NAME).unsetStylesUpdated();
};