diff --git a/includes/class-newspack-newsletters-renderer.php b/includes/class-newspack-newsletters-renderer.php index 20b7c54cf..b58ac2b68 100644 --- a/includes/class-newspack-newsletters-renderer.php +++ b/includes/class-newspack-newsletters-renderer.php @@ -20,6 +20,20 @@ final class Newspack_Newsletters_Renderer { */ protected static $color_palette = null; + /** + * The header font. + * + * @var String + */ + protected static $font_header = null; + + /** + * The body font. + * + * @var String + */ + protected static $font_body = null; + /** * Convert a list to HTML attributes. * @@ -191,6 +205,8 @@ private static function render_mjml_component( $block, $is_in_column = false, $i ) ); + $font_family = 'core/heading' === $block_name ? self::$font_header : self::$font_body; + switch ( $block_name ) { /** * Paragraph, List, Heading blocks. @@ -204,6 +220,7 @@ private static function render_mjml_component( $block, $is_in_column = false, $i 'padding' => '0', 'line-height' => '1.8', 'font-size' => '16px', + 'font-family' => $font_family, ), $attrs ); @@ -267,9 +284,10 @@ private static function render_mjml_component( $block, $is_in_column = false, $i if ( $figcaption ) { $caption_attrs = array( - 'align' => 'center', - 'color' => '#555d66', - 'font-size' => '13px', + 'align' => 'center', + 'color' => '#555d66', + 'font-size' => '13px', + 'font-family' => $font_family, ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $markup .= '' . $figcaption->wholeText . ''; @@ -300,6 +318,7 @@ private static function render_mjml_component( $block, $is_in_column = false, $i 'href' => $anchor->getAttribute( 'href' ), 'border-radius' => $border_radius . 'px', 'font-size' => '18px', + 'font-family' => $font_family, // Default color - will be replaced by get_colors if there are colors set. 'color' => $is_outlined ? '#32373c' : '#fff', ); @@ -517,9 +536,17 @@ private static function render_mjml_component( $block, $is_in_column = false, $i */ private static function render_mjml( $post ) { self::$color_palette = get_post_meta( $post->ID, 'color_palette', true ); - $title = $post->post_title; - $blocks = parse_blocks( $post->post_content ); - $body = ''; + self::$font_header = get_post_meta( $post->ID, 'font_header', true ); + self::$font_body = get_post_meta( $post->ID, 'font_body', true ); + if ( ! in_array( self::$font_header, Newspack_Newsletters::$supported_fonts ) ) { + self::$font_header = 'Arial'; + } + if ( ! in_array( self::$font_body, Newspack_Newsletters::$supported_fonts ) ) { + self::$font_body = 'Georgia'; + } + $title = $post->post_title; + $blocks = parse_blocks( $post->post_content ); + $body = ''; foreach ( $blocks as $block ) { $block_content = self::render_mjml_component( $block ); if ( ! empty( $block_content ) ) { diff --git a/includes/class-newspack-newsletters.php b/includes/class-newspack-newsletters.php index 725cbd20f..de5b14127 100644 --- a/includes/class-newspack-newsletters.php +++ b/includes/class-newspack-newsletters.php @@ -18,6 +18,22 @@ final class Newspack_Newsletters { const NEWSPACK_NEWSLETTERS_CPT = 'newspack_nl_cpt'; + /** + * Supported fonts. + * + * @var array + */ + public static $supported_fonts = [ + 'Arial, Helvetica, sans-serif', + 'Tahoma, sans-serif', + 'Trebuchet MS, sans-serif', + 'Verdana, sans-serif', + 'Georgia, serif', + 'Palatino, serif', + 'Times New Roman, serif', + 'Courier, monospace', + ]; + /** * The single instance of the class. * @@ -103,6 +119,28 @@ public static function register_meta() { 'auth_callback' => '__return_true', ] ); + \register_meta( + 'post', + 'font_header', + [ + 'object_subtype' => self::NEWSPACK_NEWSLETTERS_CPT, + 'show_in_rest' => true, + 'type' => 'string', + 'single' => true, + 'auth_callback' => '__return_true', + ] + ); + \register_meta( + 'post', + 'font_body', + [ + 'object_subtype' => self::NEWSPACK_NEWSLETTERS_CPT, + 'show_in_rest' => true, + 'type' => 'string', + 'single' => true, + 'auth_callback' => '__return_true', + ] + ); /** * The default color palette lives in the editor frontend and is not * retrievable on the backend. The workaround is to set it as post meta @@ -292,6 +330,81 @@ public static function rest_api_init() { ], ] ); + \register_rest_route( + 'newspack-newsletters/v1/', + 'typography/(?P[\a-z]+)', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ __CLASS__, 'api_set_typography' ], + 'permission_callback' => [ __CLASS__, 'api_administration_permissions_check' ], + 'args' => [ + 'id' => [ + 'validate_callback' => [ __CLASS__, 'validate_newsletter_id' ], + 'sanitize_callback' => 'absint', + ], + 'key' => [ + 'validate_callback' => [ __CLASS__, 'validate_newsletter_typography_key' ], + 'sanitize_callback' => 'sanitize_text_field', + ], + 'value' => [ + 'validate_callback' => [ __CLASS__, 'validate_newsletter_typography_value' ], + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ] + ); + } + + /** + * Set typography meta. + * The save_post action fires before post meta is updated. + * This causes newsletters to be synced to the ESP before recent changes to custom fields have been recorded, + * which leads to incorrect rendering. This is addressed through custom endpoints to update the typography fields + * as soon as they are changed in the editor, so that the changes are available the next time sync to ESP occurs. + * + * @param WP_REST_Request $request API request object. + */ + public static function api_set_typography( $request ) { + $id = $request['id']; + $key = $request['key']; + $value = $request['value']; + update_post_meta( $id, $key, $value ); + } + + /** + * Validate ID is a Newsletter post type. + * + * @param int $id Post ID. + */ + public static function validate_newsletter_id( $id ) { + return self::NEWSPACK_NEWSLETTERS_CPT === get_post_type( $id ); + } + + /** + * Validate typography key. + * + * @param String $key Meta key. + */ + public static function validate_newsletter_typography_key( $key ) { + return in_array( + $key, + [ + 'font_header', + 'font_body', + ] + ); + } + + /** + * Validate typography value (font name). + * + * @param String $key Meta value. + */ + public static function validate_newsletter_typography_value( $key ) { + return in_array( + $key, + self::$supported_fonts + ); } /** diff --git a/src/components/select-control-with-optgroup/index.js b/src/components/select-control-with-optgroup/index.js new file mode 100644 index 000000000..040763425 --- /dev/null +++ b/src/components/select-control-with-optgroup/index.js @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { isEmpty } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; +import { BaseControl } from '@wordpress/components'; + +/** + * SelectControl with optgroup support + */ +export default function SelectControlWithOptGroup( { + help, + label, + multiple = false, + onChange, + optgroups = [], + className, + hideLabelFromVision, + ...props +} ) { + const instanceId = useInstanceId( SelectControlWithOptGroup ); + const id = `inspector-select-control-${ instanceId }`; + const onChangeValue = event => { + if ( multiple ) { + const selectedOptions = [ ...event.target.options ].filter( ( { selected } ) => selected ); + const newValues = selectedOptions.map( ( { value } ) => value ); + onChange( newValues ); + return; + } + onChange( event.target.value ); + }; + + // Disable reason: A select with an onchange throws a warning + + if ( isEmpty( optgroups ) ) { + return null; + } + + /* eslint-disable jsx-a11y/no-onchange */ + return ( + + + + ); + /* eslint-enable jsx-a11y/no-onchange */ +} diff --git a/src/editor/style.scss b/src/editor/style.scss index 928bb94d0..35c0273ae 100644 --- a/src/editor/style.scss +++ b/src/editor/style.scss @@ -1,3 +1,8 @@ +:root { + --header-font: arial, sans-serif; + --body-font: georgia, serif; +} + .wp-block { max-width: 600px; padding: 20px; @@ -11,7 +16,27 @@ } *:not( code ) { - font-family: Ubuntu, Helvetica, Arial, sans-serif !important; + font-family: var( --body-font ); + } + + .newspack-posts-inserter__header span, + .components-button, + .block-editor-block-list__block .components-placeholder, + .block-editor-block-list__block .components-placeholder div { + font-family: 'Noto Serif', helvetica, arial, sans-serif; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: var( --header-font ); + } + + a { + font-family: inherit; } code { diff --git a/src/newsletter-editor/index.js b/src/newsletter-editor/index.js index fc62c0138..a3fe3e267 100644 --- a/src/newsletter-editor/index.js +++ b/src/newsletter-editor/index.js @@ -15,6 +15,7 @@ import InitModal from '../components/init-modal'; import Layout from './layout/'; import Sidebar from './sidebar/'; import Testing from './testing/'; +import Typography from './typography/'; import registerEditorPlugin from './editor/'; registerEditorPlugin(); @@ -36,6 +37,12 @@ const NewsletterEdit = ( { layoutId } ) => { > + + + { + const { editPost } = dispatch( 'core/editor' ); + return { editPost }; + } ), + withSelect( select => { + const { getEditedPostAttribute, getCurrentPostId } = select( 'core/editor' ); + const meta = getEditedPostAttribute( 'meta' ); + return { + postId: getCurrentPostId(), + fontBody: meta.font_body || '', + fontHeader: meta.font_header || '', + }; + } ), +] )( ( { editPost, fontBody, fontHeader, postId } ) => { + const updateFontValue = ( key, value ) => { + editPost( { meta: { [ key ]: value } } ); + apiFetch( { + data: { key, value }, + method: 'POST', + path: `/newspack-newsletters/v1/typography/${ postId }`, + } ); + }; + useEffect(() => { + document.documentElement.style.setProperty( '--body-font', fontBody ); + }, [ fontBody ]); + useEffect(() => { + document.documentElement.style.setProperty( '--header-font', fontHeader ); + }, [ fontHeader ]); + return ( + + updateFontValue( 'font_header', value ) } + /> + updateFontValue( 'font_body', value ) } + /> + + ); +} );