|
| 1 | +/** |
| 2 | + * Carry the email's root font size into the canvas on the custom property captions read. |
| 3 | + * |
| 4 | + * The email image renderer wraps an image in `font-size: 0px` tables so mail clients leave no gap |
| 5 | + * under it, and the caption cell beneath would inherit that 0px once the canvas replaces the root |
| 6 | + * size with `inherit` so text can follow the Styles panel. WordPress.com therefore sizes that cell |
| 7 | + * from `var(--wpcom-email-root-font-size, <size at load>)`, and this sets the variable from the |
| 8 | + * live design so a caption follows the panel without a reload. See NL-1075 and NL-951. |
| 9 | + */ |
| 10 | + |
| 11 | +import { storeName as EMAIL_EDITOR_STORE } from '@woocommerce/email-editor'; |
| 12 | +import { store as coreStore } from '@wordpress/core-data'; |
| 13 | +import { dispatch, select, subscribe } from '@wordpress/data'; |
| 14 | + |
| 15 | +// What the panel stores for a preset pick. Rewritten to the custom property rather than resolved: |
| 16 | +// the canvas defines the preset variables above the root container, so the caption then tracks |
| 17 | +// whatever the body text shows. The `var( ... )` form sanitizing rewrites it to passes through. |
| 18 | +const PRESET_SHORTHAND = /^var:preset\|font-size\|([\w-]+)$/; |
| 19 | + |
| 20 | +// What may reach a declaration. A size is the creator's own input, and a `}` would end the rule |
| 21 | +// early; the same set WordPress.com validates this value against. |
| 22 | +const SAFE_VALUE = /^[a-z0-9.%(),+*/ -]+$/i; |
| 23 | + |
| 24 | +// How our entry is recognised in a style array that also carries WordPress.com's email stylesheet. |
| 25 | +// Matched as a prefix, never as a substring, so no other entry declaring the property is dropped. |
| 26 | +const RULE_PREFIX = '.is-root-container{--wpcom-email-root-font-size:'; |
| 27 | + |
| 28 | +/** |
| 29 | + * The email's root font size, as the Styles panel's own merge resolves it. |
| 30 | + * |
| 31 | + * @param {object} theme - The editor theme WordPress.com derived from the site. |
| 32 | + * @param {object} design - The creator's own layer over it. |
| 33 | + * @return {string|null} The size, or null when neither layer sets one. |
| 34 | + */ |
| 35 | +export function rootFontSize( theme, design ) { |
| 36 | + const size = design?.styles?.typography?.fontSize ?? theme?.styles?.typography?.fontSize; |
| 37 | + |
| 38 | + return 'string' === typeof size && size.trim() ? size.trim() : null; |
| 39 | +} |
| 40 | + |
| 41 | +/** |
| 42 | + * The root font size the screen opens on, before the editor has a store to read. |
| 43 | + * |
| 44 | + * @param {object} bundle - The response from the bootstrap route. |
| 45 | + * @return {string|null} The size, or null when the bundle sets none. |
| 46 | + */ |
| 47 | +export function bundleRootFontSize( bundle ) { |
| 48 | + return rootFontSize( bundle?.editor_theme, bundle?.global_styles?.record ); |
| 49 | +} |
| 50 | + |
| 51 | +/** |
| 52 | + * The canvas rule that publishes a root font size, or nothing when there is none to publish. |
| 53 | + * |
| 54 | + * @param {*} fontSize - A stored font size. |
| 55 | + * @return {string|null} The CSS, or null to leave WordPress.com's fallback in force. |
| 56 | + */ |
| 57 | +export function rootFontSizeCss( fontSize ) { |
| 58 | + const value = cssValue( fontSize ); |
| 59 | + |
| 60 | + return value ? `${ RULE_PREFIX }${ value }; }` : null; |
| 61 | +} |
| 62 | + |
| 63 | +/** |
| 64 | + * A stored font size as a value a declaration can hold. |
| 65 | + * |
| 66 | + * @param {*} fontSize - A stored font size. |
| 67 | + * @return {string|null} The value, or null when it is absent or not a size. |
| 68 | + */ |
| 69 | +function cssValue( fontSize ) { |
| 70 | + if ( 'string' !== typeof fontSize ) { |
| 71 | + return null; |
| 72 | + } |
| 73 | + |
| 74 | + const trimmed = fontSize.trim(); |
| 75 | + const preset = PRESET_SHORTHAND.exec( trimmed ); |
| 76 | + const value = preset ? `var(--wp--preset--font-size--${ preset[ 1 ] })` : trimmed; |
| 77 | + |
| 78 | + return SAFE_VALUE.test( value ) ? value : null; |
| 79 | +} |
| 80 | + |
| 81 | +/** |
| 82 | + * A canvas style array holding this root font size and no earlier one. |
| 83 | + * |
| 84 | + * @param {*} styles - The canvas styles as they stand. |
| 85 | + * @param {*} fontSize - The root font size to publish. |
| 86 | + * @return {Array} The new styles. |
| 87 | + */ |
| 88 | +export function withRootFontSize( styles, fontSize ) { |
| 89 | + const kept = ( Array.isArray( styles ) ? styles : [] ).filter( |
| 90 | + entry => 'string' !== typeof entry?.css || ! entry.css.startsWith( RULE_PREFIX ) |
| 91 | + ); |
| 92 | + |
| 93 | + const css = rootFontSizeCss( fontSize ); |
| 94 | + |
| 95 | + return css ? [ ...kept, { css } ] : kept; |
| 96 | +} |
| 97 | + |
| 98 | +/** |
| 99 | + * Republish the root font size whenever the creator changes it. |
| 100 | + * |
| 101 | + * The package appends the editor settings' `styles` to the canvas CSS it generates and reads those |
| 102 | + * settings from its own store, so re-dispatching them is what repaints the canvas; the design the |
| 103 | + * panel edits lives in core-data, which is what this watches. Only a change is dispatched, so the |
| 104 | + * settings the package assembled from everything else are left alone. |
| 105 | + * |
| 106 | + * @param {string|null} [initial] - The size the canvas was built with, which needs no repaint. |
| 107 | + * @return {Function} Stops watching, for a caller that unmounts the editor. |
| 108 | + */ |
| 109 | +export function watchRootFontSize( initial = null ) { |
| 110 | + let previous = initial; |
| 111 | + |
| 112 | + return subscribe( () => { |
| 113 | + const settings = select( EMAIL_EDITOR_STORE )?.getInitialEditorSettings?.(); |
| 114 | + |
| 115 | + // Before the editor has taken the configuration there is nothing to append to, and |
| 116 | + // dispatching would replace it. |
| 117 | + if ( ! settings ) { |
| 118 | + return; |
| 119 | + } |
| 120 | + |
| 121 | + const design = select( EMAIL_EDITOR_STORE ).getGlobalEmailStylesPost(); |
| 122 | + |
| 123 | + // The package answers null until the design record resolves, and the theme alone would |
| 124 | + // republish a size the creator has already replaced. The canvas opened on the right one. |
| 125 | + if ( ! design ) { |
| 126 | + return; |
| 127 | + } |
| 128 | + |
| 129 | + const current = rootFontSize( select( EMAIL_EDITOR_STORE ).getTheme(), design ); |
| 130 | + |
| 131 | + if ( current === previous ) { |
| 132 | + return; |
| 133 | + } |
| 134 | + |
| 135 | + previous = current; |
| 136 | + |
| 137 | + dispatch( EMAIL_EDITOR_STORE ).setEditorSettings( { |
| 138 | + ...settings, |
| 139 | + styles: withRootFontSize( settings.styles, current ), |
| 140 | + } ); |
| 141 | + }, coreStore ); |
| 142 | +} |
0 commit comments