diff --git a/amp.php b/amp.php index 0cfba95132e..036c368fc18 100644 --- a/amp.php +++ b/amp.php @@ -145,6 +145,7 @@ function amp_init() { add_action( 'admin_init', 'AMP_Options_Manager::register_settings' ); add_action( 'wp_loaded', 'amp_editor_core_blocks' ); add_action( 'wp_loaded', 'amp_post_meta_box' ); + add_action( 'wp_loaded', 'amp_editor_core_blocks' ); add_action( 'wp_loaded', 'amp_add_options_menu' ); add_action( 'parse_query', 'amp_correct_query_when_is_front_page' ); diff --git a/assets/css/amp-default.css b/assets/css/amp-default.css index 0cc0e05da5e..c06e0895dbe 100644 --- a/assets/css/amp-default.css +++ b/assets/css/amp-default.css @@ -3,3 +3,13 @@ /** Force the image into a box of fixed dimensions and use object-fit to scale. **/ object-fit: contain; } + +.entry__content amp-fit-text blockquote, +amp-fit-text h1, +amp-fit-text h2, +amp-fit-text h3, +amp-fit-text h4, +amp-fit-text h5, +amp-fit-text h6 { + font-size: inherit; +} \ No newline at end of file diff --git a/assets/css/amp-editor-blocks.css b/assets/css/amp-editor-blocks.css new file mode 100644 index 00000000000..e16b21b8c22 --- /dev/null +++ b/assets/css/amp-editor-blocks.css @@ -0,0 +1,4 @@ +.is-amp-fit-text + .blocks-font-size > .components-font-size-picker__buttons, +.is-amp-fit-text + .blocks-font-size > .components-font-size-picker__custom-input { + display: none; +} diff --git a/assets/js/amp-editor-blocks.js b/assets/js/amp-editor-blocks.js index 2be3f238ef9..4dc2b106bb2 100644 --- a/assets/js/amp-editor-blocks.js +++ b/assets/js/amp-editor-blocks.js @@ -85,7 +85,19 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars 'core/image', 'core/video', 'core/audio' - ] + ], + textBlocks: [ + 'core/paragraph', + 'core/heading', + 'core/code', + 'core/quote', + 'core/subhead' + ], + ampSettingsLabel: __( 'AMP Settings' ), + fontSizes: { + small: 14, + larger: 48 + } } }; @@ -178,6 +190,38 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars }; } + // Fit-text for text blocks. + if ( -1 !== component.data.textBlocks.indexOf( name ) ) { + if ( ! settings.attributes ) { + settings.attributes = {}; + } + settings.attributes.ampFitText = { + type: 'boolean', + default: false + }; + settings.attributes.minFont = { + type: 'number', + default: component.data.fontSizes.small, + source: 'attribute', + selector: 'amp-fit-text', + attribute: 'min-font-size' + }; + settings.attributes.maxFont = { + type: 'number', + default: component.data.fontSizes.larger, + source: 'attribute', + selector: 'amp-fit-text', + attribute: 'max-font-size' + }; + settings.attributes.height = { + type: 'number', + default: 50, + source: 'attribute', + selector: 'amp-fit-text', + attribute: 'height' + }; + } + // Layout settings for embeds and media blocks. if ( 0 === name.indexOf( 'core-embed' ) || -1 !== component.data.mediaBlocks.indexOf( name ) ) { if ( ! settings.attributes ) { @@ -222,6 +266,8 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars } } else if ( -1 !== component.data.mediaBlocks.indexOf( name ) || 0 === name.indexOf( 'core-embed/' ) ) { inspectorControls = component.setUpInspectorControls( props ); + } else if ( -1 !== component.data.textBlocks.indexOf( name ) ) { + inspectorControls = component.setUpTextBlocksInspectorControls( props ); } // Return just inspector controls in case of 'nodisplay'. @@ -314,6 +360,135 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars ); }; + /** + * Setup inspector controls for text blocks. + * + * @todo Consider wrapping the render function to delete the original font size in text settings when ampFitText. + * + * @param {Object} props Props. + * @return {Object|Element|*|{$$typeof, type, key, ref, props, _owner}} Inspector Controls. + */ + component.setUpTextBlocksInspectorControls = function setUpInspectorControls( props ) { + var inspectorPanelBodyArgs, + ampFitText = props.attributes.ampFitText, + minFont = props.attributes.minFont, + maxFont = props.attributes.maxFont, + height = props.attributes.height, + isSelected = props.isSelected, + el = wp.element.createElement, + InspectorControls = wp.editor.InspectorControls, + TextControl = wp.components.TextControl, + FontSizePicker = wp.components.FontSizePicker, + ToggleControl = wp.components.ToggleControl, + PanelBody = wp.components.PanelBody, + label = __( 'Use AMP Fit Text' ), + FONT_SIZES = [ + { + name: 'small', + shortName: __( 'S' ), + size: 14 + }, + { + name: 'regular', + shortName: __( 'M' ), + size: 16 + }, + { + name: 'large', + shortName: __( 'L' ), + size: 36 + }, + { + name: 'larger', + shortName: __( 'XL' ), + size: 48 + } + ]; + + if ( ! isSelected ) { + return null; + } + + inspectorPanelBodyArgs = [ + PanelBody, + { title: component.data.ampSettingsLabel, className: ampFitText ? 'is-amp-fit-text' : '' }, + el( ToggleControl, { + label: label, + checked: ampFitText, + onChange: function() { + props.setAttributes( { ampFitText: ! ampFitText } ); + } + } ) + ]; + + if ( ampFitText ) { + inspectorPanelBodyArgs.push.apply( inspectorPanelBodyArgs, [ + el( TextControl, { + label: __( 'Height' ), + value: height, + type: 'number', + min: 1, + onChange: function( nextHeight ) { + props.setAttributes( { height: nextHeight } ); + } + } ), + maxFont > height && el( + wp.components.Notice, + { + status: 'error', + isDismissible: false + }, + __( 'The height must be greater than the max font size.' ) + ), + el( PanelBody, { title: __( 'Minimum font size' ) }, + el( FontSizePicker, { + fallbackFontSize: 14, + value: minFont, + fontSizes: FONT_SIZES, + onChange: function( nextMinFont ) { + if ( ! nextMinFont ) { + nextMinFont = component.data.fontSizes.small; // @todo Supplying fallbackFontSize should be done automatically by the component? + } + if ( nextMinFont <= maxFont ) { + props.setAttributes( { minFont: nextMinFont } ); + } + } + } ) + ), + minFont > maxFont && el( + wp.components.Notice, + { + status: 'error', + isDismissible: false + }, + __( 'The min font size must less than the max font size.' ) + ), + el( PanelBody, { title: __( 'Maximum font size' ) }, + el( FontSizePicker, { + value: maxFont, + fallbackFontSize: 48, + fontSizes: FONT_SIZES, + onChange: function( nextMaxFont ) { + if ( ! nextMaxFont ) { + nextMaxFont = component.data.fontSizes.larger; // @todo Supplying fallbackFontSize should be done automatically by the component? + } + props.setAttributes( { + maxFont: nextMaxFont, + height: Math.max( nextMaxFont, height ) + } ); + } + } ) + ) + ] ); + } + + return ( + el( InspectorControls, { key: 'inspector' }, + el.apply( null, inspectorPanelBodyArgs ) + ) + ); + }; + /** * Set up inspector controls for shortcode block. * Adds ampCarousel attribute in case of gallery shortcode. @@ -325,7 +500,7 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars var ampCarousel = props.attributes.ampCarousel, isSelected = props.isSelected, el = wp.element.createElement, - InspectorControls = wp.blocks.InspectorControls, + InspectorControls = wp.editor.InspectorControls, ToggleControl = wp.components.ToggleControl, PanelBody = wp.components.PanelBody, toggleControl; @@ -359,7 +534,12 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars * @return {*} Output element. */ component.filterBlocksSave = function filterBlocksSave( element, blockType, attributes ) { - var text; + var text, + fitTextProps = { + layout: 'fixed-height', + children: element + }; + if ( 'core/shortcode' === blockType.name && component.isGalleryShortcode( attributes ) ) { if ( attributes.ampCarousel ) { // If the text contains amp-carousel, lets remove it. @@ -390,6 +570,17 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars {}, text ); + } else if ( -1 !== component.data.textBlocks.indexOf( blockType.name ) && attributes.ampFitText ) { + if ( attributes.minFont ) { + fitTextProps[ 'min-font-size' ] = attributes.minFont; + } + if ( attributes.maxFont ) { + fitTextProps[ 'max-font-size' ] = attributes.maxFont; + } + if ( attributes.height ) { + fitTextProps.height = attributes.height; + } + return wp.element.createElement( 'amp-fit-text', fitTextProps ); } return element; }; diff --git a/includes/admin/class-amp-editor-blocks.php b/includes/admin/class-amp-editor-blocks.php index b847e2362fd..5dfa5bde558 100644 --- a/includes/admin/class-amp-editor-blocks.php +++ b/includes/admin/class-amp-editor-blocks.php @@ -11,6 +11,30 @@ */ class AMP_Editor_Blocks { + /** + * List of AMP scripts that need to be printed when AMP components are used in non-AMP document context ("dirty AMP"). + * + * @var array + */ + public $content_required_amp_scripts = array(); + + /** + * AMP components that have blocks. + * + * @var array + */ + public $amp_blocks = array( + 'amp-mathml', + 'amp-o2-player', + 'amp-ooyala-player', + 'amp-reach-player', + 'amp-springboard-player', + 'amp-jwplayer', + 'amp-brid-player', + 'amp-ima-video', + 'amp-fit-text', + ); + /** * Init. */ @@ -18,6 +42,8 @@ public function init() { if ( function_exists( 'gutenberg_init' ) ) { add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); add_filter( 'wp_kses_allowed_html', array( $this, 'whitelist_block_atts_in_wp_kses_allowed_html' ), 10, 2 ); + add_filter( 'the_content', array( $this, 'tally_content_requiring_amp_scripts' ) ); + add_action( 'wp_print_footer_scripts', array( $this, 'print_dirty_amp_scripts' ) ); } } @@ -40,18 +66,7 @@ public function whitelist_block_atts_in_wp_kses_allowed_html( $tags, $context ) $tag['data-amp-noloading'] = true; } - $amp_blocks = array( - 'amp-mathml', - 'amp-o2-player', - 'amp-ooyala-player', - 'amp-reach-player', - 'amp-springboard-player', - 'amp-jwplayer', - 'amp-brid-player', - 'amp-ima-video', - ); - - foreach ( $amp_blocks as $amp_block ) { + foreach ( $this->amp_blocks as $amp_block ) { if ( ! isset( $tags[ $amp_block ] ) ) { $tags[ $amp_block ] = array(); } @@ -62,6 +77,7 @@ public function whitelist_block_atts_in_wp_kses_allowed_html( $tags, $context ) 'layout', 'width', 'height', + 'class', ), true ), @@ -89,6 +105,14 @@ public function whitelist_block_atts_in_wp_kses_allowed_html( $tags, $context ) */ public function enqueue_block_editor_assets() { + // Styles. + wp_enqueue_style( + 'amp-editor-blocks-style', + amp_get_asset_url( 'css/amp-editor-blocks.css' ), + array(), + AMP__VERSION + ); + // Scripts. wp_enqueue_script( 'amp-editor-blocks-build', @@ -107,4 +131,32 @@ public function enqueue_block_editor_assets() { wp_add_inline_script( 'amp-editor-blocks', sprintf( 'ampEditorBlocks.boot();' ) ); } + + /** + * Tally the AMP component scripts that are needed in a dirty AMP document. + * + * @param string $content Content. + * @return string Content (unmodified). + */ + public function tally_content_requiring_amp_scripts( $content ) { + if ( ! is_amp_endpoint() ) { + $pattern = sprintf( '/<(%s)\b.*?>/s', join( '|', $this->amp_blocks ) ); + if ( preg_match_all( $pattern, $content, $matches ) ) { + $this->content_required_amp_scripts = array_merge( + $this->content_required_amp_scripts, + $matches[1] + ); + } + } + return $content; + } + + /** + * Print AMP scripts required for AMP components used in a non-AMP document (dirty AMP). + */ + public function print_dirty_amp_scripts() { + if ( ! is_amp_endpoint() && ! empty( $this->content_required_amp_scripts ) ) { + wp_scripts()->do_items( $this->content_required_amp_scripts ); + } + } }