UNPKG

@wordpress/block-editor

Version:
1,064 lines (1,015 loc) 44.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getBlockSelectors = void 0; exports.getLayoutStyles = getLayoutStyles; exports.getNodesWithStyles = exports.getNodesWithSettings = void 0; exports.getStylesDeclarations = getStylesDeclarations; exports.processCSSNesting = processCSSNesting; exports.toStyles = exports.toCustomProperties = void 0; exports.toSvgFilters = toSvgFilters; exports.useGlobalStylesOutput = useGlobalStylesOutput; exports.useGlobalStylesOutputWithConfig = useGlobalStylesOutputWithConfig; var _blocks = require("@wordpress/blocks"); var _data = require("@wordpress/data"); var _element = require("@wordpress/element"); var _styleEngine = require("@wordpress/style-engine"); var _components = require("@wordpress/components"); var _utils = require("./utils"); var _getBlockCssSelector = require("./get-block-css-selector"); var _typographyUtils = require("./typography-utils"); var _context = require("./context"); var _hooks = require("./hooks"); var _utils2 = require("../duotone/utils"); var _gap = require("../../hooks/gap"); var _background = require("../../hooks/background"); var _store = require("../../store"); var _definitions = require("../../layouts/definitions"); var _object = require("../../utils/object"); var _lockUnlock = require("../../lock-unlock"); /** * WordPress dependencies */ /** * Internal dependencies */ // Elements that rely on class names in their selectors. const ELEMENT_CLASS_NAMES = { button: 'wp-element-button', caption: 'wp-element-caption' }; // List of block support features that can have their related styles // generated under their own feature level selector rather than the block's. const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = { __experimentalBorder: 'border', color: 'color', spacing: 'spacing', typography: 'typography' }; const { kebabCase } = (0, _lockUnlock.unlock)(_components.privateApis); /** * Transform given preset tree into a set of style declarations. * * @param {Object} blockPresets * @param {Object} mergedSettings Merged theme.json settings. * * @return {Array<Object>} An array of style declarations. */ function getPresetsDeclarations(blockPresets = {}, mergedSettings) { return _utils.PRESET_METADATA.reduce((declarations, { path, valueKey, valueFunc, cssVarInfix }) => { const presetByOrigin = (0, _object.getValueFromObjectPath)(blockPresets, path, []); ['default', 'theme', 'custom'].forEach(origin => { if (presetByOrigin[origin]) { presetByOrigin[origin].forEach(value => { if (valueKey && !valueFunc) { declarations.push(`--wp--preset--${cssVarInfix}--${kebabCase(value.slug)}: ${value[valueKey]}`); } else if (valueFunc && typeof valueFunc === 'function') { declarations.push(`--wp--preset--${cssVarInfix}--${kebabCase(value.slug)}: ${valueFunc(value, mergedSettings)}`); } }); } }); return declarations; }, []); } /** * Transform given preset tree into a set of preset class declarations. * * @param {?string} blockSelector * @param {Object} blockPresets * @return {string} CSS declarations for the preset classes. */ function getPresetsClasses(blockSelector = '*', blockPresets = {}) { return _utils.PRESET_METADATA.reduce((declarations, { path, cssVarInfix, classes }) => { if (!classes) { return declarations; } const presetByOrigin = (0, _object.getValueFromObjectPath)(blockPresets, path, []); ['default', 'theme', 'custom'].forEach(origin => { if (presetByOrigin[origin]) { presetByOrigin[origin].forEach(({ slug }) => { classes.forEach(({ classSuffix, propertyName }) => { const classSelectorToUse = `.has-${kebabCase(slug)}-${classSuffix}`; const selectorToUse = blockSelector.split(',') // Selector can be "h1, h2, h3" .map(selector => `${selector}${classSelectorToUse}`).join(','); const value = `var(--wp--preset--${cssVarInfix}--${kebabCase(slug)})`; declarations += `${selectorToUse}{${propertyName}: ${value} !important;}`; }); }); } }); return declarations; }, ''); } function getPresetsSvgFilters(blockPresets = {}) { return _utils.PRESET_METADATA.filter( // Duotone are the only type of filters for now. metadata => metadata.path.at(-1) === 'duotone').flatMap(metadata => { const presetByOrigin = (0, _object.getValueFromObjectPath)(blockPresets, metadata.path, {}); return ['default', 'theme'].filter(origin => presetByOrigin[origin]).flatMap(origin => presetByOrigin[origin].map(preset => (0, _utils2.getDuotoneFilter)(`wp-duotone-${preset.slug}`, preset.colors))).join(''); }); } function flattenTree(input = {}, prefix, token) { let result = []; Object.keys(input).forEach(key => { const newKey = prefix + kebabCase(key.replace('/', '-')); const newLeaf = input[key]; if (newLeaf instanceof Object) { const newPrefix = newKey + token; result = [...result, ...flattenTree(newLeaf, newPrefix, token)]; } else { result.push(`${newKey}: ${newLeaf}`); } }); return result; } /** * Gets variation selector string from feature selector. * * @param {string} featureSelector The feature selector. * * @param {string} styleVariationSelector The style variation selector. * @return {string} Combined selector string. */ function concatFeatureVariationSelectorString(featureSelector, styleVariationSelector) { const featureSelectors = featureSelector.split(','); const combinedSelectors = []; featureSelectors.forEach(selector => { combinedSelectors.push(`${styleVariationSelector.trim()}${selector.trim()}`); }); return combinedSelectors.join(', '); } /** * Generate style declarations for a block's custom feature and subfeature * selectors. * * NOTE: The passed `styles` object will be mutated by this function. * * @param {Object} selectors Custom selectors object for a block. * @param {Object} styles A block's styles object. * * @return {Object} Style declarations. */ const getFeatureDeclarations = (selectors, styles) => { const declarations = {}; Object.entries(selectors).forEach(([feature, selector]) => { // We're only processing features/subfeatures that have styles. if (feature === 'root' || !styles?.[feature]) { return; } const isShorthand = typeof selector === 'string'; // If we have a selector object instead of shorthand process it. if (!isShorthand) { Object.entries(selector).forEach(([subfeature, subfeatureSelector]) => { // Don't process root feature selector yet or any // subfeature that doesn't have a style. if (subfeature === 'root' || !styles?.[feature][subfeature]) { return; } // Create a temporary styles object and build // declarations for subfeature. const subfeatureStyles = { [feature]: { [subfeature]: styles[feature][subfeature] } }; const newDeclarations = getStylesDeclarations(subfeatureStyles); // Merge new declarations in with any others that // share the same selector. declarations[subfeatureSelector] = [...(declarations[subfeatureSelector] || []), ...newDeclarations]; // Remove the subfeature's style now it will be // included under its own selector not the block's. delete styles[feature][subfeature]; }); } // Now subfeatures have been processed and removed, we can // process root, or shorthand, feature selectors. if (isShorthand || selector.root) { const featureSelector = isShorthand ? selector : selector.root; // Create temporary style object and build declarations for feature. const featureStyles = { [feature]: styles[feature] }; const newDeclarations = getStylesDeclarations(featureStyles); // Merge new declarations with any others that share the selector. declarations[featureSelector] = [...(declarations[featureSelector] || []), ...newDeclarations]; // Remove the feature from the block's styles now as it will be // included under its own selector not the block's. delete styles[feature]; } }); return declarations; }; /** * Transform given style tree into a set of style declarations. * * @param {Object} blockStyles Block styles. * * @param {string} selector The selector these declarations should attach to. * * @param {boolean} useRootPaddingAlign Whether to use CSS custom properties in root selector. * * @param {Object} tree A theme.json tree containing layout definitions. * * @param {boolean} disableRootPadding Whether to force disable the root padding styles. * @return {Array} An array of style declarations. */ function getStylesDeclarations(blockStyles = {}, selector = '', useRootPaddingAlign, tree = {}, disableRootPadding = false) { const isRoot = _utils.ROOT_BLOCK_SELECTOR === selector; const output = Object.entries(_blocks.__EXPERIMENTAL_STYLE_PROPERTY).reduce((declarations, [key, { value, properties, useEngine, rootOnly }]) => { if (rootOnly && !isRoot) { return declarations; } const pathToValue = value; if (pathToValue[0] === 'elements' || useEngine) { return declarations; } const styleValue = (0, _object.getValueFromObjectPath)(blockStyles, pathToValue); // Root-level padding styles don't currently support strings with CSS shorthand values. // This may change: https://github.com/WordPress/gutenberg/issues/40132. if (key === '--wp--style--root--padding' && (typeof styleValue === 'string' || !useRootPaddingAlign)) { return declarations; } if (properties && typeof styleValue !== 'string') { Object.entries(properties).forEach(entry => { const [name, prop] = entry; if (!(0, _object.getValueFromObjectPath)(styleValue, [prop], false)) { // Do not create a declaration // for sub-properties that don't have any value. return; } const cssProperty = name.startsWith('--') ? name : kebabCase(name); declarations.push(`${cssProperty}: ${(0, _styleEngine.getCSSValueFromRawStyle)((0, _object.getValueFromObjectPath)(styleValue, [prop]))}`); }); } else if ((0, _object.getValueFromObjectPath)(blockStyles, pathToValue, false)) { const cssProperty = key.startsWith('--') ? key : kebabCase(key); declarations.push(`${cssProperty}: ${(0, _styleEngine.getCSSValueFromRawStyle)((0, _object.getValueFromObjectPath)(blockStyles, pathToValue))}`); } return declarations; }, []); /* * Preprocess background image values. * * Note: As we absorb more and more styles into the engine, we could simplify this function. * A refactor is for the style engine to handle ref resolution (and possibly defaults) * via a public util used internally and externally. Theme.json tree and defaults could be passed * as options. */ if (!!blockStyles.background) { /* * Resolve dynamic values before they are compiled by the style engine, * which doesn't (yet) resolve dynamic values. */ if (blockStyles.background?.backgroundImage) { blockStyles.background.backgroundImage = (0, _utils.getResolvedValue)(blockStyles.background.backgroundImage, tree); } /* * Set default values for block background styles. * Top-level styles are an exception as they are applied to the body. */ if (!isRoot && !!blockStyles.background?.backgroundImage?.id) { blockStyles = { ...blockStyles, background: { ...blockStyles.background, ...(0, _background.setBackgroundStyleDefaults)(blockStyles.background) } }; } } const extraRules = (0, _styleEngine.getCSSRules)(blockStyles); extraRules.forEach(rule => { // Don't output padding properties if padding variables are set or if we're not editing a full template. if (isRoot && (useRootPaddingAlign || disableRootPadding) && rule.key.startsWith('padding')) { return; } const cssProperty = rule.key.startsWith('--') ? rule.key : kebabCase(rule.key); let ruleValue = (0, _utils.getResolvedValue)(rule.value, tree, null); // Calculate fluid typography rules where available. if (cssProperty === 'font-size') { /* * getTypographyFontSizeValue() will check * if fluid typography has been activated and also * whether the incoming value can be converted to a fluid value. * Values that already have a "clamp()" function will not pass the test, * and therefore the original $value will be returned. */ ruleValue = (0, _typographyUtils.getTypographyFontSizeValue)({ size: ruleValue }, tree?.settings); } // For aspect ratio to work, other dimensions rules (and Cover block defaults) must be unset. // This ensures that a fixed height does not override the aspect ratio. if (cssProperty === 'aspect-ratio') { output.push('min-height: unset'); } output.push(`${cssProperty}: ${ruleValue}`); }); return output; } /** * Get generated CSS for layout styles by looking up layout definitions provided * in theme.json, and outputting common layout styles, and specific blockGap values. * * @param {Object} props * @param {Object} props.layoutDefinitions Layout definitions, keyed by layout type. * @param {Object} props.style A style object containing spacing values. * @param {string} props.selector Selector used to group together layout styling rules. * @param {boolean} props.hasBlockGapSupport Whether or not the theme opts-in to blockGap support. * @param {boolean} props.hasFallbackGapSupport Whether or not the theme allows fallback gap styles. * @param {?string} props.fallbackGapValue An optional fallback gap value if no real gap value is available. * @return {string} Generated CSS rules for the layout styles. */ function getLayoutStyles({ layoutDefinitions = _definitions.LAYOUT_DEFINITIONS, style, selector, hasBlockGapSupport, hasFallbackGapSupport, fallbackGapValue }) { let ruleset = ''; let gapValue = hasBlockGapSupport ? (0, _gap.getGapCSSValue)(style?.spacing?.blockGap) : ''; // Ensure a fallback gap value for the root layout definitions, // and use a fallback value if one is provided for the current block. if (hasFallbackGapSupport) { if (selector === _utils.ROOT_BLOCK_SELECTOR) { gapValue = !gapValue ? '0.5em' : gapValue; } else if (!hasBlockGapSupport && fallbackGapValue) { gapValue = fallbackGapValue; } } if (gapValue && layoutDefinitions) { Object.values(layoutDefinitions).forEach(({ className, name, spacingStyles }) => { // Allow outputting fallback gap styles for flex layout type when block gap support isn't available. if (!hasBlockGapSupport && 'flex' !== name && 'grid' !== name) { return; } if (spacingStyles?.length) { spacingStyles.forEach(spacingStyle => { const declarations = []; if (spacingStyle.rules) { Object.entries(spacingStyle.rules).forEach(([cssProperty, cssValue]) => { declarations.push(`${cssProperty}: ${cssValue ? cssValue : gapValue}`); }); } if (declarations.length) { let combinedSelector = ''; if (!hasBlockGapSupport) { // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles. combinedSelector = selector === _utils.ROOT_BLOCK_SELECTOR ? `:where(.${className}${spacingStyle?.selector || ''})` : `:where(${selector}.${className}${spacingStyle?.selector || ''})`; } else { combinedSelector = selector === _utils.ROOT_BLOCK_SELECTOR ? `:root :where(.${className})${spacingStyle?.selector || ''}` : `:root :where(${selector}-${className})${spacingStyle?.selector || ''}`; } ruleset += `${combinedSelector} { ${declarations.join('; ')}; }`; } }); } }); // For backwards compatibility, ensure the legacy block gap CSS variable is still available. if (selector === _utils.ROOT_BLOCK_SELECTOR && hasBlockGapSupport) { ruleset += `${_utils.ROOT_CSS_PROPERTIES_SELECTOR} { --wp--style--block-gap: ${gapValue}; }`; } } // Output base styles if (selector === _utils.ROOT_BLOCK_SELECTOR && layoutDefinitions) { const validDisplayModes = ['block', 'flex', 'grid']; Object.values(layoutDefinitions).forEach(({ className, displayMode, baseStyles }) => { if (displayMode && validDisplayModes.includes(displayMode)) { ruleset += `${selector} .${className} { display:${displayMode}; }`; } if (baseStyles?.length) { baseStyles.forEach(baseStyle => { const declarations = []; if (baseStyle.rules) { Object.entries(baseStyle.rules).forEach(([cssProperty, cssValue]) => { declarations.push(`${cssProperty}: ${cssValue}`); }); } if (declarations.length) { const combinedSelector = `.${className}${baseStyle?.selector || ''}`; ruleset += `${combinedSelector} { ${declarations.join('; ')}; }`; } }); } }); } return ruleset; } const STYLE_KEYS = ['border', 'color', 'dimensions', 'spacing', 'typography', 'filter', 'outline', 'shadow', 'background']; function pickStyleKeys(treeToPickFrom) { if (!treeToPickFrom) { return {}; } const entries = Object.entries(treeToPickFrom); const pickedEntries = entries.filter(([key]) => STYLE_KEYS.includes(key)); // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it const clonedEntries = pickedEntries.map(([key, style]) => [key, JSON.parse(JSON.stringify(style))]); return Object.fromEntries(clonedEntries); } const getNodesWithStyles = (tree, blockSelectors) => { var _tree$styles$blocks; const nodes = []; if (!tree?.styles) { return nodes; } // Top-level. const styles = pickStyleKeys(tree.styles); if (styles) { nodes.push({ styles, selector: _utils.ROOT_BLOCK_SELECTOR, // Root selector (body) styles should not be wrapped in `:root where()` to keep // specificity at (0,0,1) and maintain backwards compatibility. skipSelectorWrapper: true }); } Object.entries(_blocks.__EXPERIMENTAL_ELEMENTS).forEach(([name, selector]) => { if (tree.styles?.elements?.[name]) { nodes.push({ styles: tree.styles?.elements?.[name], selector, // Top level elements that don't use a class name should not receive the // `:root :where()` wrapper to maintain backwards compatibility. skipSelectorWrapper: !ELEMENT_CLASS_NAMES[name] }); } }); // Iterate over blocks: they can have styles & elements. Object.entries((_tree$styles$blocks = tree.styles?.blocks) !== null && _tree$styles$blocks !== void 0 ? _tree$styles$blocks : {}).forEach(([blockName, node]) => { var _node$elements; const blockStyles = pickStyleKeys(node); if (node?.variations) { const variations = {}; Object.entries(node.variations).forEach(([variationName, variation]) => { var _variation$elements, _variation$blocks; variations[variationName] = pickStyleKeys(variation); if (variation?.css) { variations[variationName].css = variation.css; } const variationSelector = blockSelectors[blockName]?.styleVariationSelectors?.[variationName]; // Process the variation's inner element styles. // This comes before the inner block styles so the // element styles within the block type styles take // precedence over these. Object.entries((_variation$elements = variation?.elements) !== null && _variation$elements !== void 0 ? _variation$elements : {}).forEach(([element, elementStyles]) => { if (elementStyles && _blocks.__EXPERIMENTAL_ELEMENTS[element]) { nodes.push({ styles: elementStyles, selector: (0, _utils.scopeSelector)(variationSelector, _blocks.__EXPERIMENTAL_ELEMENTS[element]) }); } }); // Process the variations inner block type styles. Object.entries((_variation$blocks = variation?.blocks) !== null && _variation$blocks !== void 0 ? _variation$blocks : {}).forEach(([variationBlockName, variationBlockStyles]) => { var _variationBlockStyles; const variationBlockSelector = (0, _utils.scopeSelector)(variationSelector, blockSelectors[variationBlockName]?.selector); const variationDuotoneSelector = (0, _utils.scopeSelector)(variationSelector, blockSelectors[variationBlockName]?.duotoneSelector); const variationFeatureSelectors = (0, _utils.scopeFeatureSelectors)(variationSelector, blockSelectors[variationBlockName]?.featureSelectors); const variationBlockStyleNodes = pickStyleKeys(variationBlockStyles); if (variationBlockStyles?.css) { variationBlockStyleNodes.css = variationBlockStyles.css; } nodes.push({ selector: variationBlockSelector, duotoneSelector: variationDuotoneSelector, featureSelectors: variationFeatureSelectors, fallbackGapValue: blockSelectors[variationBlockName]?.fallbackGapValue, hasLayoutSupport: blockSelectors[variationBlockName]?.hasLayoutSupport, styles: variationBlockStyleNodes }); // Process element styles for the inner blocks // of the variation. Object.entries((_variationBlockStyles = variationBlockStyles.elements) !== null && _variationBlockStyles !== void 0 ? _variationBlockStyles : {}).forEach(([variationBlockElement, variationBlockElementStyles]) => { if (variationBlockElementStyles && _blocks.__EXPERIMENTAL_ELEMENTS[variationBlockElement]) { nodes.push({ styles: variationBlockElementStyles, selector: (0, _utils.scopeSelector)(variationBlockSelector, _blocks.__EXPERIMENTAL_ELEMENTS[variationBlockElement]) }); } }); }); }); blockStyles.variations = variations; } if (blockSelectors?.[blockName]?.selector) { nodes.push({ duotoneSelector: blockSelectors[blockName].duotoneSelector, fallbackGapValue: blockSelectors[blockName].fallbackGapValue, hasLayoutSupport: blockSelectors[blockName].hasLayoutSupport, selector: blockSelectors[blockName].selector, styles: blockStyles, featureSelectors: blockSelectors[blockName].featureSelectors, styleVariationSelectors: blockSelectors[blockName].styleVariationSelectors }); } Object.entries((_node$elements = node?.elements) !== null && _node$elements !== void 0 ? _node$elements : {}).forEach(([elementName, value]) => { if (value && blockSelectors?.[blockName] && _blocks.__EXPERIMENTAL_ELEMENTS[elementName]) { nodes.push({ styles: value, selector: blockSelectors[blockName]?.selector.split(',').map(sel => { const elementSelectors = _blocks.__EXPERIMENTAL_ELEMENTS[elementName].split(','); return elementSelectors.map(elementSelector => sel + ' ' + elementSelector); }).join(',') }); } }); }); return nodes; }; exports.getNodesWithStyles = getNodesWithStyles; const getNodesWithSettings = (tree, blockSelectors) => { var _tree$settings$blocks; const nodes = []; if (!tree?.settings) { return nodes; } const pickPresets = treeToPickFrom => { let presets = {}; _utils.PRESET_METADATA.forEach(({ path }) => { const value = (0, _object.getValueFromObjectPath)(treeToPickFrom, path, false); if (value !== false) { presets = (0, _object.setImmutably)(presets, path, value); } }); return presets; }; // Top-level. const presets = pickPresets(tree.settings); const custom = tree.settings?.custom; if (Object.keys(presets).length > 0 || custom) { nodes.push({ presets, custom, selector: _utils.ROOT_CSS_PROPERTIES_SELECTOR }); } // Blocks. Object.entries((_tree$settings$blocks = tree.settings?.blocks) !== null && _tree$settings$blocks !== void 0 ? _tree$settings$blocks : {}).forEach(([blockName, node]) => { const blockPresets = pickPresets(node); const blockCustom = node.custom; if (Object.keys(blockPresets).length > 0 || blockCustom) { nodes.push({ presets: blockPresets, custom: blockCustom, selector: blockSelectors[blockName]?.selector }); } }); return nodes; }; exports.getNodesWithSettings = getNodesWithSettings; const toCustomProperties = (tree, blockSelectors) => { const settings = getNodesWithSettings(tree, blockSelectors); let ruleset = ''; settings.forEach(({ presets, custom, selector }) => { const declarations = getPresetsDeclarations(presets, tree?.settings); const customProps = flattenTree(custom, '--wp--custom--', '--'); if (customProps.length > 0) { declarations.push(...customProps); } if (declarations.length > 0) { ruleset += `${selector}{${declarations.join(';')};}`; } }); return ruleset; }; exports.toCustomProperties = toCustomProperties; const toStyles = (tree, blockSelectors, hasBlockGapSupport, hasFallbackGapSupport, disableLayoutStyles = false, disableRootPadding = false, styleOptions = undefined) => { // These allow opting out of certain sets of styles. const options = { blockGap: true, blockStyles: true, layoutStyles: true, marginReset: true, presets: true, rootPadding: true, variationStyles: false, ...styleOptions }; const nodesWithStyles = getNodesWithStyles(tree, blockSelectors); const nodesWithSettings = getNodesWithSettings(tree, blockSelectors); const useRootPaddingAlign = tree?.settings?.useRootPaddingAwareAlignments; const { contentSize, wideSize } = tree?.settings?.layout || {}; const hasBodyStyles = options.marginReset || options.rootPadding || options.layoutStyles; let ruleset = ''; if (options.presets && (contentSize || wideSize)) { ruleset += `${_utils.ROOT_CSS_PROPERTIES_SELECTOR} {`; ruleset = contentSize ? ruleset + ` --wp--style--global--content-size: ${contentSize};` : ruleset; ruleset = wideSize ? ruleset + ` --wp--style--global--wide-size: ${wideSize};` : ruleset; ruleset += '}'; } if (hasBodyStyles) { /* * Reset default browser margin on the body element. * This is set on the body selector **before** generating the ruleset * from the `theme.json`. This is to ensure that if the `theme.json` declares * `margin` in its `spacing` declaration for the `body` element then these * user-generated values take precedence in the CSS cascade. * @link https://github.com/WordPress/gutenberg/issues/36147. */ ruleset += ':where(body) {margin: 0;'; // Root padding styles should be output for full templates, patterns and template parts. if (options.rootPadding && useRootPaddingAlign) { /* * These rules reproduce the ones from https://github.com/WordPress/gutenberg/blob/79103f124925d1f457f627e154f52a56228ed5ad/lib/class-wp-theme-json-gutenberg.php#L2508 * almost exactly, but for the selectors that target block wrappers in the front end. This code only runs in the editor, so it doesn't need those selectors. */ ruleset += `padding-right: 0; padding-left: 0; padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom) } .has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } .has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); } .has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; } .has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; `; } ruleset += '}'; } if (options.blockStyles) { nodesWithStyles.forEach(({ selector, duotoneSelector, styles, fallbackGapValue, hasLayoutSupport, featureSelectors, styleVariationSelectors, skipSelectorWrapper }) => { // Process styles for block support features with custom feature level // CSS selectors set. if (featureSelectors) { const featureDeclarations = getFeatureDeclarations(featureSelectors, styles); Object.entries(featureDeclarations).forEach(([cssSelector, declarations]) => { if (declarations.length) { const rules = declarations.join(';'); ruleset += `:root :where(${cssSelector}){${rules};}`; } }); } // Process duotone styles. if (duotoneSelector) { const duotoneStyles = {}; if (styles?.filter) { duotoneStyles.filter = styles.filter; delete styles.filter; } const duotoneDeclarations = getStylesDeclarations(duotoneStyles); if (duotoneDeclarations.length) { ruleset += `${duotoneSelector}{${duotoneDeclarations.join(';')};}`; } } // Process blockGap and layout styles. if (!disableLayoutStyles && (_utils.ROOT_BLOCK_SELECTOR === selector || hasLayoutSupport)) { ruleset += getLayoutStyles({ style: styles, selector, hasBlockGapSupport, hasFallbackGapSupport, fallbackGapValue }); } // Process the remaining block styles (they use either normal block class or __experimentalSelector). const styleDeclarations = getStylesDeclarations(styles, selector, useRootPaddingAlign, tree, disableRootPadding); if (styleDeclarations?.length) { const generalSelector = skipSelectorWrapper ? selector : `:root :where(${selector})`; ruleset += `${generalSelector}{${styleDeclarations.join(';')};}`; } if (styles?.css) { ruleset += processCSSNesting(styles.css, `:root :where(${selector})`); } if (options.variationStyles && styleVariationSelectors) { Object.entries(styleVariationSelectors).forEach(([styleVariationName, styleVariationSelector]) => { const styleVariations = styles?.variations?.[styleVariationName]; if (styleVariations) { // If the block uses any custom selectors for block support, add those first. if (featureSelectors) { const featureDeclarations = getFeatureDeclarations(featureSelectors, styleVariations); Object.entries(featureDeclarations).forEach(([baseSelector, declarations]) => { if (declarations.length) { const cssSelector = concatFeatureVariationSelectorString(baseSelector, styleVariationSelector); const rules = declarations.join(';'); ruleset += `:root :where(${cssSelector}){${rules};}`; } }); } // Otherwise add regular selectors. const styleVariationDeclarations = getStylesDeclarations(styleVariations, styleVariationSelector, useRootPaddingAlign, tree); if (styleVariationDeclarations.length) { ruleset += `:root :where(${styleVariationSelector}){${styleVariationDeclarations.join(';')};}`; } if (styleVariations?.css) { ruleset += processCSSNesting(styleVariations.css, `:root :where(${styleVariationSelector})`); } } }); } // Check for pseudo selector in `styles` and handle separately. const pseudoSelectorStyles = Object.entries(styles).filter(([key]) => key.startsWith(':')); if (pseudoSelectorStyles?.length) { pseudoSelectorStyles.forEach(([pseudoKey, pseudoStyle]) => { const pseudoDeclarations = getStylesDeclarations(pseudoStyle); if (!pseudoDeclarations?.length) { return; } // `selector` may be provided in a form // where block level selectors have sub element // selectors appended to them as a comma separated // string. // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`; // Split and append pseudo selector to create // the proper rules to target the elements. const _selector = selector.split(',').map(sel => sel + pseudoKey).join(','); // As pseudo classes such as :hover, :focus etc. have class-level // specificity, they must use the `:root :where()` wrapper. This. // caps the specificity at `0-1-0` to allow proper nesting of variations // and block type element styles. const pseudoRule = `:root :where(${_selector}){${pseudoDeclarations.join(';')};}`; ruleset += pseudoRule; }); } }); } if (options.layoutStyles) { /* Add alignment / layout styles */ ruleset = ruleset + '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; ruleset = ruleset + '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; ruleset = ruleset + '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; } if (options.blockGap && hasBlockGapSupport) { // Use fallback of `0.5em` just in case, however if there is blockGap support, there should nearly always be a real value. const gapValue = (0, _gap.getGapCSSValue)(tree?.styles?.spacing?.blockGap) || '0.5em'; ruleset = ruleset + `:root :where(.wp-site-blocks) > * { margin-block-start: ${gapValue}; margin-block-end: 0; }`; ruleset = ruleset + ':root :where(.wp-site-blocks) > :first-child { margin-block-start: 0; }'; ruleset = ruleset + ':root :where(.wp-site-blocks) > :last-child { margin-block-end: 0; }'; } if (options.presets) { nodesWithSettings.forEach(({ selector, presets }) => { if (_utils.ROOT_BLOCK_SELECTOR === selector || _utils.ROOT_CSS_PROPERTIES_SELECTOR === selector) { // Do not add extra specificity for top-level classes. selector = ''; } const classes = getPresetsClasses(selector, presets); if (classes.length > 0) { ruleset += classes; } }); } return ruleset; }; exports.toStyles = toStyles; function toSvgFilters(tree, blockSelectors) { const nodesWithSettings = getNodesWithSettings(tree, blockSelectors); return nodesWithSettings.flatMap(({ presets }) => { return getPresetsSvgFilters(presets); }); } const getSelectorsConfig = (blockType, rootSelector) => { if (blockType?.selectors && Object.keys(blockType.selectors).length > 0) { return blockType.selectors; } const config = { root: rootSelector }; Object.entries(BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS).forEach(([featureKey, featureName]) => { const featureSelector = (0, _getBlockCssSelector.getBlockCSSSelector)(blockType, featureKey); if (featureSelector) { config[featureName] = featureSelector; } }); return config; }; const getBlockSelectors = (blockTypes, getBlockStyles, variationInstanceId) => { const result = {}; blockTypes.forEach(blockType => { const name = blockType.name; const selector = (0, _getBlockCssSelector.getBlockCSSSelector)(blockType); let duotoneSelector = (0, _getBlockCssSelector.getBlockCSSSelector)(blockType, 'filter.duotone'); // Keep backwards compatibility for support.color.__experimentalDuotone. if (!duotoneSelector) { const rootSelector = (0, _getBlockCssSelector.getBlockCSSSelector)(blockType); const duotoneSupport = (0, _blocks.getBlockSupport)(blockType, 'color.__experimentalDuotone', false); duotoneSelector = duotoneSupport && (0, _utils.scopeSelector)(rootSelector, duotoneSupport); } const hasLayoutSupport = !!blockType?.supports?.layout || !!blockType?.supports?.__experimentalLayout; const fallbackGapValue = blockType?.supports?.spacing?.blockGap?.__experimentalDefault; const blockStyleVariations = getBlockStyles(name); const styleVariationSelectors = {}; blockStyleVariations?.forEach(variation => { const variationSuffix = variationInstanceId ? `-${variationInstanceId}` : ''; const variationName = `${variation.name}${variationSuffix}`; const styleVariationSelector = (0, _utils.getBlockStyleVariationSelector)(variationName, selector); styleVariationSelectors[variationName] = styleVariationSelector; }); // For each block support feature add any custom selectors. const featureSelectors = getSelectorsConfig(blockType, selector); result[name] = { duotoneSelector, fallbackGapValue, featureSelectors: Object.keys(featureSelectors).length ? featureSelectors : undefined, hasLayoutSupport, name, selector, styleVariationSelectors: blockStyleVariations?.length ? styleVariationSelectors : undefined }; }); return result; }; /** * If there is a separator block whose color is defined in theme.json via background, * update the separator color to the same value by using border color. * * @param {Object} config Theme.json configuration file object. * @return {Object} configTheme.json configuration file object updated. */ exports.getBlockSelectors = getBlockSelectors; function updateConfigWithSeparator(config) { const needsSeparatorStyleUpdate = config.styles?.blocks?.['core/separator'] && config.styles?.blocks?.['core/separator'].color?.background && !config.styles?.blocks?.['core/separator'].color?.text && !config.styles?.blocks?.['core/separator'].border?.color; if (needsSeparatorStyleUpdate) { return { ...config, styles: { ...config.styles, blocks: { ...config.styles.blocks, 'core/separator': { ...config.styles.blocks['core/separator'], color: { ...config.styles.blocks['core/separator'].color, text: config.styles?.blocks['core/separator'].color.background } } } } }; } return config; } function processCSSNesting(css, blockSelector) { let processedCSS = ''; if (!css || css.trim() === '') { return processedCSS; } // Split CSS nested rules. const parts = css.split('&'); parts.forEach(part => { if (!part || part.trim() === '') { return; } const isRootCss = !part.includes('{'); if (isRootCss) { // If the part doesn't contain braces, it applies to the root level. processedCSS += `:root :where(${blockSelector}){${part.trim()}}`; } else { // If the part contains braces, it's a nested CSS rule. const splitPart = part.replace('}', '').split('{'); if (splitPart.length !== 2) { return; } const [nestedSelector, cssValue] = splitPart; // Handle pseudo elements such as ::before, ::after, etc. Regex will also // capture any leading combinator such as >, +, or ~, as well as spaces. // This allows pseudo elements as descendants e.g. `.parent ::before`. const matches = nestedSelector.match(/([>+~\s]*::[a-zA-Z-]+)/); const pseudoPart = matches ? matches[1] : ''; const withoutPseudoElement = matches ? nestedSelector.replace(pseudoPart, '').trim() : nestedSelector.trim(); let combinedSelector; if (withoutPseudoElement === '') { // Only contained a pseudo element to use the block selector to form // the final `:root :where()` selector. combinedSelector = blockSelector; } else { // If the nested selector is a descendant of the block scope it with the // block selector. Otherwise append it to the block selector. combinedSelector = nestedSelector.startsWith(' ') ? (0, _utils.scopeSelector)(blockSelector, withoutPseudoElement) : (0, _utils.appendToSelector)(blockSelector, withoutPseudoElement); } // Build final rule, re-adding any pseudo element outside the `:where()` // to maintain valid CSS selector. processedCSS += `:root :where(${combinedSelector})${pseudoPart}{${cssValue.trim()}}`; } }); return processedCSS; } /** * Returns the global styles output using a global styles configuration. * If wishing to generate global styles and settings based on the * global styles config loaded in the editor context, use `useGlobalStylesOutput()`. * The use case for a custom config is to generate bespoke styles * and settings for previews, or other out-of-editor experiences. * * @param {Object} mergedConfig Global styles configuration. * @param {boolean} disableRootPadding Disable root padding styles. * * @return {Array} Array of stylesheets and settings. */ function useGlobalStylesOutputWithConfig(mergedConfig = {}, disableRootPadding) { const [blockGap] = (0, _hooks.useGlobalSetting)('spacing.blockGap'); const hasBlockGapSupport = blockGap !== null; const hasFallbackGapSupport = !hasBlockGapSupport; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback styles support. const disableLayoutStyles = (0, _data.useSelect)(select => { const { getSettings } = select(_store.store); return !!getSettings().disableLayoutStyles; }); const { getBlockStyles } = (0, _data.useSelect)(_blocks.store); return (0, _element.useMemo)(() => { var _updatedConfig$styles; if (!mergedConfig?.styles || !mergedConfig?.settings) { return []; } const updatedConfig = updateConfigWithSeparator(mergedConfig); const blockSelectors = getBlockSelectors((0, _blocks.getBlockTypes)(), getBlockStyles); const customProperties = toCustomProperties(updatedConfig, blockSelectors); const globalStyles = toStyles(updatedConfig, blockSelectors, hasBlockGapSupport, hasFallbackGapSupport, disableLayoutStyles, disableRootPadding); const svgs = toSvgFilters(updatedConfig, blockSelectors); const styles = [{ css: customProperties, isGlobalStyles: true }, { css: globalStyles, isGlobalStyles: true }, // Load custom CSS in own stylesheet so that any invalid CSS entered in the input won't break all the global styles in the editor. { css: (_updatedConfig$styles = updatedConfig.styles.css) !== null && _updatedConfig$styles !== void 0 ? _updatedConfig$styles : '', isGlobalStyles: true }, { assets: svgs, __unstableType: 'svg', isGlobalStyles: true }]; // Loop through the blocks to check if there are custom CSS values. // If there are, get the block selector and push the selector together with // the CSS value to the 'stylesheets' array. (0, _blocks.getBlockTypes)().forEach(blockType => { if (updatedConfig.styles.blocks[blockType.name]?.css) { const selector = blockSelectors[blockType.name].selector; styles.push({ css: processCSSNesting(updatedConfig.styles.blocks[blockType.name]?.css, selector), isGlobalStyles: true }); } }); return [styles, updatedConfig.settings]; }, [hasBlockGapSupport, hasFallbackGapSupport, mergedConfig, disableLayoutStyles, disableRootPadding, getBlockStyles]); } /** * Returns the global styles output based on the current state of global styles config loaded in the editor context. * * @param {boolean} disableRootPadding Disable root padding styles. * * @return {Array} Array of stylesheets and settings. */ function useGlobalStylesOutput(disableRootPadding = false) { const { merged: mergedConfig } = (0, _element.useContext)(_context.GlobalStylesContext); return useGlobalStylesOutputWithConfig(mergedConfig, disableRootPadding); } //# sourceMappingURL=use-global-styles-output.js.map